If the Merkle spine catches record tampering, what happens when someone edits the spine file too? An insider with full disk access could delete the entire store — segments, spine, and checkpoint file — and replace it all with a self-consistent tree rebuilt from forged records. Checkpoints and external anchoring exist precisely to stop that scenario: planting an "unforgeable reference point" in a different trust domain that even an insider cannot reach.
A checkpoint seals (segment position, record count, Merkle root) with an RSA signature, and the AnchorHook exports that root outside the store — so even if an insider rewrites the entire log, the anchored past root will fail a consistency proof against the new one.
Why the Merkle spine alone isn't enough
Let's take stock of the defenses built so far:
- In-place record edit → leaf hash changes → root changes ✅
- Segment file substitution → leaf order changes → root changes ✅
- Tail truncation (segment deletion) → tree size shrinks → inconsistent with previous checkpoint ✅
One scenario remains: what if an attacker deletes the entire store (segments + spine + checkpoint file) and builds a new, internally self-consistent store from forged records? There is nothing inside to compare against the original.
Checkpoint: a signed snapshot
A checkpoint captures "the state of the tree at a given moment" and signs it with an RSA private key, then writes it to a file. Let's look at the key fields:
crates/quipu-core/src/checkpoint.rs — Checkpoint structpub struct Checkpoint {
pub created_at: u64, // UTC microseconds
pub segment_seq: u64, // active segment number at checkpoint time
pub record_count: u64, // on-disk record count (can decrease after retention)
pub tree_size: u64, // Merkle tree size (never decreases; spine is never purged)
pub merkle_root: Hash, // Merkle root at that tree_size — this is the "seal"
pub key_version: u32, // RSA key version used for signing
pub signature: Vec<u8>, // RSA PKCS#1 v1.5 / SHA-256 signature
}
Notice that tree_size and record_count are separate. When retention deletes old records, record_count drops — but because the Merkle spine is never deleted, tree_size never decreases. This means that even after retention runs, a consistency proof can still verify that the current tree is a valid extension of the past checkpoint.
The signature is simply RSA PKCS#1 v1.5 applied to SHA-256(domain_separation_prefix || fields):
crates/quipu-core/src/checkpoint.rs — signing bytes constructionconst SIGNING_DOMAIN: &[u8] = b"quipu-checkpoint-v2\0";
fn signing_bytes(created_at, segment_seq, record_count, tree_size, merkle_root) {
// Domain separation: this signature can never be confused with an RSA signature
// from any other context.
out.extend_from_slice(SIGNING_DOMAIN);
out.extend_from_slice(&created_at.to_le_bytes());
// ... remaining fields ...
out.extend_from_slice(merkle_root);
}
When are checkpoints created
RSA signing takes milliseconds; checkpointing on every record would hurt performance. Quipu-Log creates a checkpoint in only three situations:
- Segment seal (rollover) — when a segment exceeds
max_segment_bytesand rolls over to a new file - Immediately after retention runs — old segments were deleted, so the current state is sealed to record "still consistent after deletion"
- Manual request — when an operator explicitly requests a checkpoint
RSA signing is not on the hot path (the emit path), so writing a single record incurs no signing latency.
AnchorHook: exporting to the outside
The checkpoint file itself lives inside the store, so an attacker can delete it. That's why StoreConfig provides an anchor hook:
crates/quipu-core/src/store.rs — AnchorHook type/// Callback invoked whenever a checkpoint is written.
/// Arc<dyn Fn> — Arc required because StoreConfig implements Clone.
pub type AnchorHook = Arc<dyn Fn(&Checkpoint) + Send + Sync>;
// How to configure it:
StoreConfig::new(root)
.anchor(|cp: &Checkpoint| {
// Send cp.merkle_root_hex() to an external system
// (ticketing system, separate DB, timestamping service)
// Panics / errors are swallowed — availability > anchoring
})
If the hook fails or panics, the write path is unaffected. A failed anchor delivery is a security concern, but halting audit log writes because of it would be an even larger problem. Retry logic is the hook's responsibility.
In a DB, "backup verification" plays a similar role — checksumming a backup against the original to confirm they match. Quipu-Log goes one step further: DB backup verification is "consistency check within the same trust domain," but external anchoring places the reference point in a different trust domain (a different server or service), making it impossible for even an insider to deceive it.
Verification: is the current tree consistent with a checkpoint?
During an audit or monitoring run, the procedure for verifying that the current spine is consistent with a past checkpoint is:
- Retrieve checkpoint root
R_mandtree_size_mfrom the external anchor - Read the current spine's
tree_size_nand current rootR_n - Confirm
tree_size_m ≤ tree_size_n(the log only grows) - Call
prove_consistency(first_size=tree_size_m)to obtain the proof verify_consistency(tree_size_m, tree_size_n, R_m, R_n, proof)→ if true, no anomaly detected
If an attacker deleted the entire store and rebuilt it, the new store's root cannot produce a valid consistency proof against the anchored R_m — because the original records are gone.
The signing key (RSA private key) must live somewhere other than the data node (a separate host, HSM, or KMS) for checkpoints to be meaningful. If the private key is also on the data node, an attacker can rewrite the store and re-sign the new checkpoints. The README states this explicitly: "Separate the signing key for anchoring."
Checkpoints in write-only deployments
In the write-only deployment covered in Ch. 27 — where only a public key lives on the server — the private key needed to sign checkpoints is not present. In this case Quipu-Log silently skips the checkpoint without raising an error: if KeyRing::can_sign() is false, the checkpoint is skipped. If you ever wonder "why aren't checkpoints accumulating?" — check your key configuration first.
Recap
- A checkpoint = RSA signature over (segment number + record count + tree_size + Merkle root). Created on segment seal, after retention, or manually.
- The
AnchorHookexports each checkpoint's root to an external trust domain immediately after it is written. - Anchored root + consistency proof → even a full-rewrite attack is detectable.
- The signing key must be kept off the data node. In write-only deployments, checkpoints are silently skipped.
① An attacker successfully rewrites the entire store. What conditions must be in place for this attack to be detected by the external anchor?
② Retention ran and deleted old segments. An external auditor then requests a consistency proof between "the past root when those deleted records existed" and the current root. Which file must still be alive for this proof to succeed?
③ If the AnchorHook panics, does the record write fail? Explain why or why not from a design perspective.