BitSeal
Architecture

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

01

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.

02

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.

03

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.

Chunking

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.

Leaf hash

Each chunk is hashed with BLAKE3 (32-byte output). Hex digests of the leaves are stored in the merkle_tree array in file order.

Internal node

A parent is BLAKE3(left || right) over the raw 32-byte child digests. No domain separator, no length prefix. Lowercase hex everywhere.

Odd layer rule

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.

Reference fold, JavaScript
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.

Root, signed by Ed25519
BLAKE3( N1 || N2 )
Internal
N1 = BLAKE3( L1 || L2 )N2 = BLAKE3( L3 || L3 )
Leaves
L1 = BLAKE3( chunk[0..64KiB] )L2 = BLAKE3( chunk[64..128KiB] )L3 = BLAKE3( chunk[128..end] )

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.

Manifest shape
{
    "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.

Verification payload
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.