How to detect deleted or truncated audit log entries
A hash chain with signatures exposes an entry edited or removed in the middle of a log. It does not expose the newest entries being cut off, or a rewrite re-signed with a stolen key. Those need a reference the log's holder cannot change: a tree head (size and root) kept by someone else, checked against a key you hold out-of-band.
A hash chain proves order, not completeness
A log that hashes each entry into the next and signs the result makes some edits visible. Which ones depends on where the attacker cuts and what they hold. Four cases are worth telling apart:
| What was done to the log file | What the file shows on its own | What catches it |
|---|---|---|
| One entry in the middle edited | The chain breaks at that entry | Re-hashing the chain |
| Entries in the middle deleted or reordered | A gap or a broken link | Re-hashing the chain |
| Newest entries removed | A shorter chain that is internally valid | A head recorded earlier, with a larger size |
| One entry edited, every later hash and signature recomputed with the real key | A chain where every hash and signature checks out | A head recorded earlier, which the new tree does not extend |
The last two rows are the reason a log needs a reference outside itself. A verifier that reads only the file cannot see them, because the file is the thing under suspicion. A signature proves that someone holding the key wrote the history. It does not prove that the history is complete or that it is the one originally written.
A recorded head turns "valid" into "consistent with what was seen"
A head is a pair: the number of entries and one root hash over all of them. RFC 6962 defines the tree and names the way violations get caught. Section 7.3 reads: "Violation of the append-only property is detected by global gossiping, i.e., everyone auditing logs comparing their versions of the latest Signed Tree Heads." It adds: "As soon as two conflicting Signed Tree Heads for the same log are detected, this is cryptographic proof of that log's misbehavior."
Applied to one log and one recorded head, the comparison has three outcomes:
| Recorded head against the log as it is now | Meaning |
|---|---|
| The log has fewer entries than the recorded size | Entries were removed |
| The log has as many entries or more, and its tree does not contain the recorded root as its earlier part | History was rewritten |
| The log's tree extends the recorded one | Consistent: entries were appended, none changed |
The mechanics of the tree and of the consistency proof, with worked hashes, are in RFC 6962 Merkle trees and consistency proofs, explained. The operating rule that follows is simple: record the head after every verification run, and keep it where the log's operator cannot edit it. A head written next to the log file protects against accidents. A head recorded after the damage says nothing about the damage.
CloudTrail ships this for AWS API activity
AWS CloudTrail is the managed version of the idea, and a good place to start if the activity you need to prove is what CloudTrail records. Its documentation, read on 13 October 2026, describes the mechanism: "Every hour, CloudTrail also creates and delivers a file that references the log files for the last hour and contains a hash of each. This file is called a digest file." CloudTrail signs each digest file. "Each digest file also contains the digital signature of the previous digest file if one exists." That is a chain of signed hourly heads.
The validation command takes a trail and a time range:
aws cloudtrail validate-logs --trail-arn <trailARN> --start-time <start-time> [--end-time <end-time>] [--verbose]
The CLI page lists what it can detect: "Modification or deletion of CloudTrail log files", "Modification or deletion of CloudTrail digest files", and "Modification or deletion of both of the above". A failing run looks like this in the documentation's sample output:
Digest file s3://…/CloudTrail-Digest/…20150901T101728Z.json.gz INVALID: signature verification failed
…
22/23 digest files valid, 1/23 digest files INVALID
63/63 log files validThree limits are stated in the same documentation, and they are the useful part for a reader choosing a design:
- "The AWS CLI validates only log files that are referenced by digest files." A file nobody referenced is not checked.
- A missing digest does not pass. For
INVALID: not foundthe documentation says: "The log files corresponding to the time range that the digest file represents cannot be validated." - Validation needs online access to AWS, read access to the bucket, and files that "must not have been moved from the original Amazon S3 location". Downloaded files cannot be validated with the CLI.
For hardening the digests, the overview page suggests Amazon S3 MFA Delete and keeps digests in a separate folder "to enforce granular security policies". CloudTrail is the better choice when the records you care about are the ones CloudTrail delivers: it is managed and signed by CloudTrail. It does not cover logs that your own gateway or application writes.
A witness does the recording for logs you run yourself
For a log you operate, the recorded head has to come from somewhere. The C2SP witness protocol specifies a service that does exactly this. Its introduction says the protocol "allows clients, usually logs, to obtain cosignatures from witnesses". The rule that matters: "Witnesses verify that the checkpoint is consistent with their previously recorded state of the log (if any), and return a timestamped cosignature."
The failure cases are explicit. "The witness MUST check that the old size matches the size of the latest checkpoint it cosigned for the checkpoint's origin (or zero if it never cosigned a checkpoint for that origin). If it doesn't match, the witness MUST respond with a "409 Conflict" HTTP status code." And: "If a Merkle Consistency Proof doesn't verify, the witness MUST respond with a "422 Unprocessable Entity" HTTP status code." A rewrite is therefore refused when it is submitted, not only noticed later.
The spec names the property being protected: "it must not be possible to partition clients from monitors, either by splitting the tree or by serving a stale view to monitors."
A witness that the log's operator also runs adds little, because whoever can rewrite the log can rewrite the witness's records. Witnesses run by other parties, and more than one, are the stronger arrangement for a log that outsiders have to trust. A single self-run witness on a separate machine is weaker, and still closes the gap that a file-only check leaves.
The verifier must choose the reference, not the file
The C2SP protocol protects the witness by configuration: "Each witness is configured with a list of supported log public keys", it "MUST ignore signatures from unknown keys", and an unknown origin gets "404 Not Found". That leaves the verifier's side. A witness holds history per log identity. If the verifier takes the identity from the file under test, an attacker can point the file at an identity whose recorded history is short and clean. On any witness that accepts new log identities, the truncated tree can itself be submitted as a new log. The truncated file then agrees with its own copy.
Two rules close that:
- The verifier looks the history up under a key or origin it obtained out-of-band, never the one the file declares.
- An unreachable witness is a failure. "Cannot rule out truncation" must not print as success.
The rest of this entry shows what that looks like in one implementation.
Where Mandare fits, and what it does not cover
Mandare's open-source door streams each RFC 6962 tree head of its ledger to an external witness as the ledger grows. Per the witnessing document, a submission is a tree size and a 32-byte root, and the witness "REFUSES anything that is not a provable append-only extension". The check is one command, with the door key passed out-of-band so the lookup is bound to your door:
mandare verify --db ledger.db --witness https://witness.example --witness-key <hex> \
--door-key <door-public-key-hex>
The CLI reference lists the outcomes: CONSISTENT, TRUNCATION DETECTED, FORK DETECTED, SOURCE MISMATCH, TIMELINE VIOLATION and UNAVAILABLE. The demo transcript stages the worst local case, an attacker who has the ledger file and the door key. This is the rewrite (copy B) and the truncation re-witnessed under a fresh source (copy C):
[attacker] copy B: entry 4 cost rewritten €1.00 → €0.000001, chain re-signed
…
[verify] rewritten copy, self-anchored: chain VALID — the lie is locally perfect
[verify] rewritten copy, --witness: FORK DETECTED (exit 1)
[attacker] copy C: newest 2 DROPPED (no key needed), the truncated tree re-witnessed
under a FRESH source, and the file's door_key_id repointed at it
[verify] copy C, --door-key + --witness: SOURCE MISMATCH + TRUNCATION DETECTED (exit 1)The transcript adds that in copy C "the witnessed history consulted is the out-of-band door key's", not the file's. The same run checks an honest copy with the same flags and gets CONSISTENT with exit code 0, so the check does not cry wolf.
This applies to CLI v0.1.0, which npm view @mandarelabs/cli version reported on 13 October 2026, and to the public repository at commit 327442e (3 October 2026). The limits, next to the claims they limit:
- Without a witness or a saved head, truncation is invisible. The README says
pnpm demo"runs without a witness, somandare verifythere can't see entries dropped from the end of the ledger" unless a saved--prev-headis passed. - The witness has to be somewhere the operator cannot rewrite. The threat model says that in the single-host compose stack "a host-level attacker owns both sides".
- There is a window. Streaming runs at about one second by default, so entries after the last witnessed head are covered from the next head onward. Actions above the mandate's approval threshold wait for a witness ack, default timeout 1,500 ms, and are refused without one. A dead witness closes those doors and never blocks
mandare kill. - One witness is one party. The witnessing document says a malicious witness "can lie by omission, not by forgery" and names a second witness as the remedy, which the protocol permits.
- A machine-root attacker acting entirely outside the doors was never in the claimed coverage.
- Anchoring is not part of this check. The demo anchors with a mock adapter and says so. OpenTimestamps receipts stay pending for hours.
- Mandare's witness speaks its own protocol. No compatibility with the C2SP witness protocol is claimed, and none has been tested.
- There has been no external audit yet. The repository documents two AI-assisted review passes.
For why the witness never sees log content, see proofs, not data.
Questions
How do you detect deleted entries in an append-only log?
Compare the log against a head recorded earlier by a party who cannot rewrite the log. A smaller size than the recorded one means entries were removed. A tree that does not contain the recorded root as its earlier part means history was rewritten. The log file alone cannot show either.
Does a hash chain detect that the newest entries were removed?
No. Removing the newest entries leaves a shorter chain in which every hash and signature still checks out. Nothing inside the file records that more entries existed, so detection needs a size or root recorded somewhere else.
Can AWS CloudTrail detect deleted log files?
Yes, for files it delivered and referenced in a digest file. Its documentation lists modification or deletion of log files, of digest files, or of both. The AWS CLI validates only log files that are referenced by digest files, and a missing digest leaves its time range unvalidated.
What is a transparency log witness?
A witness is a service that keeps the latest checkpoint it cosigned for each log and cosigns a new checkpoint only if it is consistent with that record. The C2SP witness protocol specifies the HTTP interface. The witness has to be run by someone other than the log operator to add anything.
Should a verifier pass when the witness cannot be reached?
No. If the reference is unreachable, truncation cannot be ruled out, so the result is a failure, not a pass. CloudTrail's validator does the same for a missing digest file: the time range it covered is reported as not validated.
Sources
- Validating CloudTrail log file integrity · Amazon Web Services
- Validating CloudTrail log file integrity with the AWS CLI · Amazon Web Services
- Transparency Log Witness Protocol · C2SP
- RFC 6962: Certificate Transparency · IETF / RFC Editor · 2013-06-01
Run it yourself: 3 commands, no API keys.
github.com/mandarelabs/mandare →