A dispute over a digital file is almost never lost over what the file says. It's lost one question earlier:
How do we know that's the file you received, and not the one you edited last night?
If the answer is "trust me," you've already lost. And it doesn't matter how right you are underneath.
This problem isn't unique to a courtroom. The auditor who receives a log dump has it, the team documenting an incident has it, whoever keeps the copy of a contract signed by email has it. In every case the same thing is needed: to prove that a set of bytes hasn't changed since a given moment, and to have it proven by someone who isn't you.
That's why I wrote Tunjo: a Rust tool that walks read-only material, computes its fingerprint, and signs a record anyone can verify.
Why a tree and not a hash
The obvious thing would be to concatenate everything and take a SHA-256. It works, and it's useless in practice.
When someone disputes one file —a specific email among four thousand— with a single hash you can only offer two things: either you hand over the whole set to be recomputed, or you ask to be believed. The first exposes material that has no reason to be exposed; the second isn't proof.
A Merkle tree solves exactly that. Each file is a leaf, each pair of nodes combines upward, and a root remains. To prove a leaf belongs to that root, you only need to show that leaf and the path of hashes to the top: a few kilobytes. The rest of the set is left untouched.
Two details of the tree that aren't optional:
// Domain separation: a leaf can never pass as an internal node.
h.update([0x00]); // leaf
h.update([0x01]); // internal node
// And the root binds the number of leaves.
h.update([0x02]);
h.update(n.to_be_bytes());
Without the first, a leaf hash could be presented as a tree node. Without the second, you get the classic ambiguity of trees with an odd number of leaves: two different sets can produce the same root. It's an old, known bug, and it still shows up in new implementations.
The fingerprint covers state, not just content
The leaf isn't the hash of the file: it's the hash of the whole item —path, size, date, state and content hash—.
The difference matters. If the fingerprint were only of the content, moving a file to another folder, renaming it, or replacing it with a link pointing to the same content would leave the root intact. And those three moves change what the set means: where a document was is part of the fact being documented, not a presentation detail.
Signing for ten years from now
The record is signed with Quipu's triple-hybrid signature: Ed25519 + ML-DSA-87 (FIPS 204) + SLH-DSA-SHA2-256s (FIPS 205), and all three must validate.
It's not algorithm collecting. It's that the shelf life of this isn't measured in months: a case can take years to resolve, and the signature has to keep verifying at the end. The three pieces fail for different reasons —Ed25519 against a quantum computer; ML-DSA for being recent and lattice-based; SLH-DSA only if the hash function breaks— and all three are needed at once for the seal to hold. One falling doesn't take the record down.
The cost is honest: the signature weighs about 34 KB. To seal a set of files, that's noise.
Verifying the signature isn't enough
This is the mistake most easily made when implementing something like this. The signature covers the record's full JSON, including the integrity root. If, when verifying, you only check the signature, you take as valid a root no one recomputed: someone with the key could sign a record whose root doesn't match the items it lists, and it would pass.
That's why verification always recomputes the tree, and only then looks at the signature. There's a test dedicated to that exact case: an authentic signature over a lying root must fail.
$ tunjo verificar acta.json --origen ./evidencia
SELLO VÁLIDO
contenido: 4 elementos, 3 con contenido verificable
raíz: d9f6f68f591c6af087838dc27049a4194ab70525c350b79ec22446f9c12f9e33
1 DISCREPANCIA(S) contra evidencia:
ALTERADO adjuntos/c.pdf
acta: 3fdbaf9c795e22f14e16974c37b62ed381b9c8c4ac7bcbe1a01f13d08ec46643
disco: 9af5d94042eafbf2c335aa874085b263ff0201aee5e5033fd4c432e92de0093d
(Tunjo's CLI is Spanish-first: SELLO VÁLIDO = seal valid, raíz = root, DISCREPANCIA = mismatch, ALTERADO = altered.)
In the absence of a fact, noise
An integrity tool that hides its failures is worse than not having one, because it produces confidence without backing. Three decisions on this:
An unreadable file stops the sealing. It isn't skipped silently. If it really is unreadable, you have to ask for it explicitly, and then the record logs it as an error: of that item it stands that it existed and that reading it failed, and nothing more.
About the clock, the truth is told. The record asks you to declare how it was checked against an external source. If it isn't declared, it writes "NOT VERIFIED" instead of staying quiet. Without a third-party timestamp, this proves relative order, not a certain date —and it says so too.
Symbolic links aren't followed. Where they point is recorded. Following them would pull the acquisition outside the material that was received.
What it deliberately doesn't do
It doesn't detect intrusions, doesn't attribute authorship, and concludes nothing. I could add heuristics that said "there was an attack here," and it would be a mistake: whoever signs a report has to be able to defend every claim line by line, and no one defends a heuristic they didn't write. When that claim falls, it drags the rest of the report with it.
Nor does it prove the past. It certifies from the instant of acquisition: if the material already came altered, the seal faithfully certifies altered material. It's written in the record it generates, not in the fine print.
The verifier is public, and not out of generosity
If the only one who can check a seal is whoever issued it, it isn't proof: it's a claim in technical formatting. That's why the verifier is free software and its code is published.
I checked it the only way that counts: I built the verifier from scratch on a clean machine and with that binary verified a record sealed by another. Valid. Then I altered one byte of an attachment and the same binary flagged that file and only that one.
It is free software (AGPL-3.0), in Rust. Since August 2026 the code is no longer hosted on public registries or on GitHub — for security: we work in cryptography, and having our source extracted would be an argument against the product, not an incident. It is delivered on request, signed and with its SHA-256, at contacto@xiliux.com. Whether the verifier is auditable does not depend on it sitting at a public URL: it depends on anyone who receives the code being able to compile it and check the seal without trusting whoever issued it. That is what makes it proof and not a claim.
And what actually shows it discriminates is not a git clone line, it is the tests: a simulation of 240 comparisons — one byte is altered in each of 120 files and it must flag that one and only that one, and leave no false positives once restored. Detecting is easy; discriminating is the work.
Rust, AGPL-3.0, and the tests include a simulation of 240 checks: one byte is altered in each of 120 files and it's required to flag that one and only that one, and that restoring it leaves no false positives. Detecting is easy; discriminating is the work.
Xiliux