Hashing, signing, chaining, checkpoints
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.
1. Canonicalise
Section titled “1. Canonicalise”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.
2. Hash
Section titled “2. Hash”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.
3. Sign
Section titled “3. Sign”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.
4. Chain
Section titled “4. Chain”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.
5. Checkpoint
Section titled “5. Checkpoint”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 ordersignature = ES256 over JCS(statement), by the custodian keytsa = 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 timestamp authority
Section titled “The timestamp authority”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.
What each step defends against
Section titled “What each step defends against”| 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.