Skip to content
Open the portal
Developer docs

Hashing, signing, chaining, checkpoints

Engineers10 min read

Everything a verifier checks comes from five operations, each a public standard, each reproducible with a common library. Nothing here is Stratl-specific except the order.

Take the wire form of the record without its integrity block (unsigned = wire(record) − integrity) and serialise it with RFC 8785, JSON Canonicalization Scheme: sorted keys, no whitespace, a fixed number format, UTF-8. Python uses the jcs package, TypeScript the canonicalize package, Go its own implementation; the conformance fixtures prove the three agree byte for byte.

record_hash = hex(SHA-256(canonical)). Sixty-four hex characters. This is what the chain links, what the Merkle tree hashes, and what a bundle’s manifest lists.

signature = ES256(canonical): ECDSA on the P-256 curve with SHA-256, over the canonical bytes, encoded base64url as r || s (64 raw bytes, the JWS convention, not DER). The signing key lives in a key service that never releases it; the service asks the key service to sign and verifies the returned signature against the public key before writing anything, so a misconfigured key cannot produce a bad record silently.

key_id is the RFC 7638 JWK thumbprint of the public key: a hash of the key’s own parameters, so it can be recomputed from the key in any bundle. Every record and checkpoint names the key that signed it.

Each workspace has one stream. When a record is written, it takes the next position (chain_seq) and the hash of the previous record (prev_hash). The chain head is kept under a lock, so two records written at the same instant still get consecutive positions. Remove a record from the middle and the next record’s prev_hash no longer matches.

The signature covers the unsigned payload, not the chain position, so the slow remote signing call happens outside the chain lock. The chain is verified separately from the signature, and the verifier reports the two separately.

Every hour, or when a thousand records are pending, the custodian seals every record not yet covered:

statement = { type: "stratl_checkpoint", version: "0.1", tenant, seq, prev_root?, root,
leaf_alg: "rfc6962-sha256", leaf_count, first_chain_seq, last_chain_seq,
window: { start, end }, created_at }
root = RFC 6962 Merkle tree hash over the raw 32 bytes of each record_hash, in chain order
signature = ES256 over JCS(statement), by the custodian key
tsa = RFC 3161 TimeStampToken whose messageImprint is SHA-256(JCS(statement))

The RFC 6962 construction is the one Certificate Transparency uses: leaves are hashed with a 0x00 prefix and interior nodes with 0x01, which prevents a leaf from being mistaken for a node. spec/fixtures/merkle/vectors.json pins the construction across the three implementations. Checkpoints chain through prev_root.

A bundle carries checkpoint.json: the statement, its signature, the timestamp token, and the record’s inclusion proof (the audit path from the record’s leaf to the root, innermost first, each step { hash, position }). The verifier recomputes the root from the record hash and the path, checks the statement signature, and checks the token’s imprint and time.

The token is the one integrity fact Stratl cannot fabricate: an outside party, following RFC 3161, hashed the statement and signed the hash together with the time. Stratl asks two authorities in order (a public one and a commercial CA’s public server by default; the list is configurable) and records which one answered. When neither answers, the checkpoint is still signed and stored, marked failed, and retried on later runs; a verifier reports a missing timestamp as a failed check, never a warning.

Threat Defeated by
Editing a record after the fact The hash and the signature no longer match
Deleting a record from the middle The chain no longer links; the checkpoint root no longer includes it
Adding a record with a back-dated timestamp It is not in any checkpoint sealed before that time, and the checkpoint’s token dates the set
Substituting a key The key id in the record does not match the thumbprint of the substituted key
Stratl itself tampering The customer holds the key in their own key service, the timestamp comes from a third party, and every key use is itself a record

Next: running the verifier.