How a file becomes a signed root.
BitSeal is a client-side integrity service. Files are chunked and hashed in the browser, a BLAKE3 Merkle tree is assembled, and only the 32-byte Merkle root is signed by the Authority with Ed25519. Web and CLI produce the same manifest, so a seal of the same bytes yields the same root everywhere.
The three stages
Client-side hashing
The file is streamed in 64 KiB chunks and hashed locally with BLAKE3. Only the resulting 32-byte fingerprints leave the device.
Merkle aggregation
Leaf hashes fold into a binary BLAKE3 tree with the odd-layer-duplicate rule, producing a single 32-byte root that commits to every byte of the file.
Ed25519 signature
The Authority signs the 40 bytes root || LE_f64(timestamp). The signature remains independently verifiable against the published public key.
The unified manifest, merkle-blake3-64k-v1
Both the web service and the BitSeal SDK produce the same Merkle manifest. A seal you created locally with the CLI and a seal the cloud signed for the identical bytes will share the exact same root_hash. You can re-derive that root from the raw file, with any BLAKE3 library, on any platform, no BitSeal code required.
The file is split into fixed 64 KiB windows from offset 0. The final chunk may be shorter, it is hashed as-is with no padding.
Each chunk is hashed with BLAKE3 (32-byte output). Hex digests of the leaves are stored in the merkle_tree array in file order.
A parent is BLAKE3(left || right) over the raw 32-byte child digests. No domain separator, no length prefix. Lowercase hex everywhere.
If a layer has an odd number of nodes, the last node is duplicated (paired with itself) before hashing up. A single-leaf tree's root is simply that leaf.
import { blake3 } from '@noble/hashes/blake3';
const CHUNK_SIZE = 64 * 1024;
function computeRootFromLeaves(leafHexArray) {
let level = leafHexArray.map(h => Buffer.from(h, 'hex'));
while (level.length > 1) {
const next = [];
for (let i = 0; i < level.length; i += 2) {
const left = level[i];
const right = (i + 1 < level.length) ? level[i + 1] : left;
next.push(Buffer.from(blake3(Buffer.concat([left, right]))));
}
level = next;
}
return level[0].toString('hex');
}Anatomy of a root
Three leaves, odd layer, so L3 is paired with itself to form N2. The root commits to every byte. Flip a single bit anywhere in the file and the root changes.
Seal manifest
Every seal, web or CLI, produces a JSON manifest with the fields below. Anything new verifiers need to reproduce the root is in the manifest itself, so the format is self-describing.
{
"seal_mode": "merkle-blake3-64k-v1",
"chunk_size_bytes": 65536,
"root_hash": "6d56f74e63d5dfabbc81cdbfef6d5e782d80df8752fd60c7c747e4c84158371e",
"merkle_tree": ["a1b2c3...", "d4e5f6...", "..."],
"blake3_hash": "...",
"sha3_512_hash": "...",
"size_bytes": 130000,
"entropy": 7.91,
"mime_type": "application/octet-stream",
"timestamp_utc": 1730000000.123,
"signer": "Orygn Authority",
"signature": "...64-byte Ed25519 signature in hex...",
"type": "BitSealManifest",
"@context": "https://w3id.org/security/v1"
}Signing payload
The Authority signs exactly 40 bytes: the 32-byte Merkle root concatenated with an 8-byte little-endian IEEE 754 double of the UTC timestamp. No JSON, no canonicalization, no surprises. Any Ed25519 library can verify the signature given the public key, the root, and the timestamp.
payload = bytes.fromhex(root_hash) + struct.pack("<d", timestamp_utc)
# len(payload) == 40
verify_key.verify(signature_bytes, payload)What never leaves the client
- File bytes. Chunking and hashing happen in your browser before anything is sent.
- Private keys. BitSeal does not generate, receive, or custody user private keys. The Authority's own private key is isolated in a secrets environment.
- Filename ambiguity. The ledger does store filename, size, and MIME type as metadata, but the seal's cryptographic identity is the root. Those fields are advisory, not part of what was signed.