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:
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.jsonDivergence detection
Section titled “Divergence detection”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.
Sealing through an HSM (PKCS#11)
Section titled “Sealing through an HSM (PKCS#11)”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.
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-prodPKCS#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 inpsand 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
Sealertrait 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
runmode the PIN is presented exactly once, at startup: a retry loop re-presenting a wrong PIN would walk the token toCKR_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.
RFC 3161 anchoring
Section titled “RFC 3161 anchoring”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:
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.