Skip to content

Sealing & the ledger

As long as sealing happens inside the gateway, the signing key and the log cohabit on one host: whoever compromises it can rewrite the log and re-seal it, and the checkpoints then certify the attacker’s version of history.

obsign-ledger runs elsewhere (another machine, or a cron under another identity), reads the WAL without ever writing to it, and seals with a key the gateway never holds:

Terminal window
openssl rand -hex 32 > /tmp/demo/seal-seed.hex
obsign-ledger seal \
--wal /tmp/demo/wal --chain-id demo \
--store /tmp/demo/ledger \
--key /tmp/demo/seal-seed.hex --key-id seal-prod
obsign-ledger export \
--wal /tmp/demo/wal --chain-id demo \
--store /tmp/demo/ledger --out /tmp/demo/evidence.json

Before sealing anything new, the ledger re-hashes the record at the sealed boundary and compares it to the sealed head. A rewritten WAL is refused with DivergedLog even when its chain has been entirely recomputed and is internally consistent, and run mode exits non-zero on it: divergence never self-heals, and looping over it would turn an incident into a heartbeat. Exiting is only half of alerting. Who gets told is the supervisor’s job: Alerting on divergence.

The hex key file is development-grade by construction. Signing goes through the Sealer trait, which is the KMS/HSM boundary. The production implementation is Pkcs11Sealer: the key lives in an HSM behind the vendor’s PKCS#11 module, and never enters the ledger process either.

Terminal window
obsign-ledger seal \
--wal /tmp/demo/wal --chain-id demo \
--store /tmp/demo/ledger \
--hsm-module /usr/lib/pkcs11/vendor.so \
--hsm-key-label seal-prod \
--hsm-pin-file /etc/obsign/hsm-pin \
--key-id seal-prod

PKCS#11 is the interface the target deployments actually have: on-prem HSMs (Trustway, Luna, YubiHSM), smartcard middleware, SoftHSM in development. It is also a local library call, so the ledger itself still makes no network call. A cloud KMS would be another implementation of the same trait; it is deliberately not this one. The bindings are hand-rolled over dlopen and cover the seven calls sealing needs; a binding crate would have dragged in the other sixty-one.

Operational properties:

  • The PIN comes from a file or OBSIGN_HSM_PIN, never from an argument (arguments end up in ps and shell history).
  • Everything that can be misconfigured fails at startup with the vendor’s error code in clear text: wrong PIN, absent key, a P-256 key under a label that should be Ed25519.
  • The Sealer trait self-verifies every signature before it is persisted, so a misconfigured HSM key slot fails at sealing time. The alternative is discovering it twenty-four months later in front of an auditor.
  • In run mode the PIN is presented exactly once, at startup: a retry loop re-presenting a wrong PIN would walk the token to CKR_PIN_LOCKED.

What the HSM buys: a compromised ledger host can sign now, but cannot exfiltrate the key and re-seal history later, offline, at leisure. What it does not buy: the HSM cannot know whether a checkpoint honestly summarizes the WAL. That remains the ledger’s divergence detection, one host over from the gateway.

A checkpoint signature proves who sealed, not when: the key holder could backdate ts_ms. Anchoring the checkpoint hash at a timestamping authority makes the date enforceable against a third party. The exchange is by file, with no HTTP client anywhere; air-gapped deployments come first:

Terminal window
obsign-ledger anchor request \
--store /tmp/demo/ledger --chain-id demo --out /tmp/demo/checkpoint.tsq
# carry the .tsq to your TSA (openssl ts reads and produces these), then:
obsign-ledger anchor attach \
--store /tmp/demo/ledger --chain-id demo \
--response /tmp/demo/checkpoint.tsr --tsa "tsa.internal.acme.fr"

The response is only attached if the TSA granted it and the token imprints exactly the checkpoint hash. The token names its own checkpoint, so there is no flag to get wrong. One anchor per chain suffices: the latest checkpoint transitively covers every earlier one through the checkpoint chain.