ADR-0029: Non-forgeable checkpoint stamper: RFC 3161 timestamp authority (2026-06-14)¶
Status¶
Proposed
Date¶
2026-06-14
Authors¶
praxis maintainers (design decision for BL-095, requested before implementation)
Context¶
The evidence layer stamps each Merkle checkpoint with a Stamper
(src/praxis/audit/rfc3161.py). The default LocalStamper is self-contained and
offline: its token is {"tsa": "local", "digest": <root>, "ts": <utc>}. That token is
forgeable by anyone who can write the evidence file: there is no secret, so an
attacker who rewrites the audit log, recomputes the Merkle root, and re-emits a
matching local token defeats verify_evidence. BL-076 closed runtime evidence
production and the anchored high-water mark (ADR-0019), but explicitly left the
non-forgeable stamper open as BL-095; until it lands, OS append-only storage
(chattr +a / WORM) on the audit, evidence, and anchor files is the documented
required control (SECURITY.md, ADR-0019).
The pieces are already in place: the Stamper Protocol (stamp/verify, both
returning JSON-serializable values and fail-closed) is stable; Rfc3161Stamper exists
as a stub that raises; the SSRF egress primitive resolve_and_assert_egress_allowed
(ADR-0025, BL-046) is ready and has no consumer yet; the optional-extra pattern is
established (postgres). The constraints are the project's: the execution core stays
dependency-free, third-party libraries are minimal and license-vetted (ADR-0014), the
default install must keep working offline with zero new dependencies, the egress path
must be SSRF-filtered with no token passthrough, and any verification must be
fail-closed. The deployment posture is EU-sovereign and single-operator-operable.
This ADR decides the approach so the implementation (the remaining BL-095 work) can proceed against a ratified design. It is recorded Proposed for ratification because it adds a third-party dependency (an ADR-0014 posture decision) and chooses between two trust anchors with real trade-offs.
Decision (proposed)¶
-
Implement RFC 3161 timestamping (a qualified external timestamp authority) as the non-forgeable
Rfc3161Stamper, keepingLocalStamperthe default and the execution core dependency-free. An RFC 3161 token is a CMSSignedDataover aTSTInfosigned by the TSA's private key, so it cannot be forged by someone with write access to the evidence file (the BL-095 threat), and it is verifiable offline by any auditor against the TSA certificate with standard tools (openssl ts). RFC 3161 fits the EU-sovereign, single-operator posture: the operator points at an eIDAS-qualified or self-hosted TSA, with no dependency on a public transparency-log ecosystem. -
Dependency: a new optional
tsaextra. Proposed members:asn1crypto(MIT; pure-Python ASN.1 withtsp/cmsmodels forTimeStampReq/TimeStampResp/TSTInfo) andcryptography(Apache-2.0/BSD; the signature and certificate verification). Both are widely used and license-clean. The core and the default install gain nothing;Rfc3161Stamperis selected only when the operator installspraxis[tsa]and configures a TSA, exactly aspostgresgates psycopg. -
stamp(digest_hex): build a DERTimeStampReqfor the SHA-256 message imprint with a random nonce andcertReq=true; POST it (application/timestamp-query) to the configured TSA URL throughresolve_and_assert_egress_allowed(HTTPS only, the host vetted and the connection pinned to a vetted IP, a bounded response size and timeout, no credentials in the URL). Parse theTimeStampResp, requirestatus=granted, the response nonce to equal the request nonce, and theTSTInfomessage imprint to equaldigest_hex. Store the token as{"tsa": "rfc3161", "digest": digest_hex, "token_b64": <base64 DER token>, "gen_time": <TSTInfo genTime>}, keeping the dict JSON-serializable for the checkpoint. On any network, status, nonce, or imprint failure,stampraises (the caller already contains failures). -
verify(digest_hex, token): fail-closed. Decodetoken_b64, parse the CMSSignedData/TSTInfo, require the imprint to equaldigest_hex, and verify the TSA signature over theTSTInfoagainst the operator-configured TSA certificate (and thatgen_timeis within the certificate validity). Any missing field, parse error, imprint mismatch, or signature failure returnsFalse. Without thetsaextra or a configured certificate,verifyreturnsFalseand the stamper is simply not selected (LocalStamperremains the default). -
Egress wiring: this is the first server-initiated egress consumer, so it wires
resolve_and_assert_egress_allowedinto a live path, advancing BL-046's open "wire into the egress path" half. The TSA URL is operator-configured (PRAXIS_TSA_URL); a non-HTTPS or unresolvable/blocked host fails closed and the stamp is refused (the operator falls back toLocalStamperplus OS append-only). -
Interim control unchanged: until a TSA is configured, OS append-only storage on the audit, evidence, and anchor files remains the documented required control (SECURITY.md, ADR-0019). This ADR does not weaken any default; it adds an opt-in, stronger anchor.
-
Offline test strategy (so the change meets the render-before-claim bar without a live TSA): unit-test the
TimeStampReqDER encoding and theTimeStampRespparsing against captured fixtures; testverifywith a fixture TSA certificate and a token it signed (a real imprint match passes, a flipped digest and a truncated token both fail closed); test the network path with a fake transport and a fake resolver (a blocked host refuses; a 200 with a granted response succeeds). No live TSA in CI.
Consequences¶
Positive: checkpoints gain a timestamp an evidence-file writer cannot forge, closing the BL-095/BL-076 residual; the token is independently verifiable offline against the TSA certificate; the default install is unchanged and still offline; the optional extra keeps the core dependency-free (ADR-0014); BL-046's resolver gets its first real consumer with the SSRF filter on the path.
Negative: a real anchor now depends on an external TSA being reachable and trusted, and
on the operator installing praxis[tsa] and configuring a certificate; a stamp made
while the TSA is unreachable falls back to the forgeable local path (the interim
control still applies). RFC 3161 verification pulls cryptography, a compiled
dependency, into the optional extra. The implementation parses untrusted TSA responses,
so the parser is an attack surface (mitigated by using asn1crypto's vetted models, a
bounded response size, and fail-closed verification).
Neutral: the Stamper Protocol and the LocalStamper default are unchanged; this only
makes the existing Rfc3161Stamper real behind the extra. Rekor remains a viable
alternative anchor if a transparency-log model is later preferred; the Protocol admits
a RekorStamper beside Rfc3161Stamper without further change.
Alternatives considered and rejected¶
- Rekor transparency log (sigstore). Rejected as the default for this context: it binds the anchor to the public sigstore ecosystem (or a self-hosted Rekor plus its operational burden), which is a weaker fit for an EU-sovereign, single-operator tool than pointing at an eIDAS-qualified or internal TSA. RFC 3161 tokens are also offline-verifiable with ubiquitous tooling. Rekor stays admissible behind the same Protocol if wanted later.
- Hand-rolled ASN.1 with no library. Rejected: hand-parsing an untrusted TSA
TimeStampResp/CMSSignedDatais a needless, error-prone attack surface whenasn1cryptoprovides vetted models; the project is dependency-minimal, not anti-PyPI (ADR-0014), and the parser only ships in the optional extra. - A keyed local stamper (HMAC with a key outside the evidence file, or a KMS).
Rejected as the BL-095 answer: it is better than the keyless
LocalStamperbut the time is still self-asserted, not qualified external time, and a key co-located with the operator is a weaker non-repudiation story than a TSA signature. It remains a reasonable futureStamperfor an air-gapped site with a hardware key, but it is not what BL-095/BL-076 asked for. - Verify the signature only offline, store the token unverified at stamp time.
Rejected:
verify_evidencemust be able to fail closed on a bad token during a self-audit, soverifymust do real signature verification when the extra and the certificate are present, not merely re-check the imprint.
Revisit triggers¶
- Ratification of this ADR (then the implementing change closes BL-095).
- A decision to prefer a transparency-log anchor (add a
RekorStamperbeside the RFC 3161 one under the same Protocol). - An eIDAS or TSA-certificate-handling requirement that needs full path validation to a configured trust root beyond a single configured TSA certificate.
cryptographyorasn1cryptoposture concerns (size, build, advisories) that would push toward a lighter ASN.1 path.
Ratification note (2026-06-14)¶
Ratified (RFC 3161 plus the tsa extra) and implemented by ADR-0030 (BL-095). This
design is unchanged; ADR-0030 carries the accepted implementation, the parallel to how
ADR-0028 implemented the Proposed ADR-0024.