ADR-036 — Local RFC 3161 Timestamp Authority for the air-gapped evidence spine
- Status: Accepted
- Date: 2026-07-05
- Deciders: Evidence-moat program (PLAN_20260704 decision D5), maintainer sign-off 2026-07-04
- Sprint: SPRINT_20260704_009 (SER-1)
- Supersedes/relates: ADR-020 (CGS Merkle tree), ADR-025 (operator-signed decisions), ADR-028 (verification key rotation)
Context
EvidenceLocker seals audit bundles (Merkle-rooted, DSSE-manifested) and is meant to stamp each sealed bundle with a trusted timestamp token so an auditor can prove when the evidence existed, independently of the sealing service’s own clock. RFC 3161 is the interoperable standard for such tokens.
Two facts constrain the choice:
- Offline / air-gap first (repo invariant §1.5). Stella Ops is self-hosted and must run with no outbound internet. Public TSAs (DigiCert, Sectigo,
freetsa.org, Sigstore’s timestamping) require reaching an external host at seal time — a hard non-starter for sovereign and air-gapped installs, and a supply-chain/prompt-injection surface. - Today the deployed EvidenceLocker runs LocalHarness (
EvidenceLocker__LocalHarness__Enabled=true,DOTNET_ENVIRONMENT=Development): in-memory golden fixture, Null RFC3161 TSA, Null timeline. Nothing real is ever sealed or timestamped (PLAN_20260704 finding B2/B3). Flipping the locker to production mode requires a reachable TSA, or the runtime validator refuses to boot (it will not silently fall back to the Null TSA outside the harness).
Decision
Ship a self-hosted, local RFC 3161 Timestamp Authority as a first-class compose service (tsa, alias tsa.stella-ops.local) and point EvidenceLocker’s timestamping client at it:
EvidenceLocker__Signing__Timestamping__Enabled = trueEvidenceLocker__Signing__Timestamping__Endpoint = http://tsa.stella-ops.local:318/(RFC 3161 over HTTP, loopback-only on the compose network)
Implementation: the TSA image is built in-repo from an OpenSSL base using the openssl ts timestamping facility wrapped by a minimal HTTP handler. We do not pull an unvetted third-party TSA image into the default supply chain. OpenSSL is Apache-License-2.0 (OpenSSL 3.x), which is compatible with BUSL-1.1 (see the dependency gate below). The TSA’s signing certificate is operator-provisioned for production (an internal CA / sovereign HSM-backed cert); the dev compose ships a clearly-labelled non-secret dev TSA cert for the golden path, mirroring the EvidenceLocker capsule dev-seed convention.
The in-memory golden fixture and the Null TSA remain test-only (the LocalHarness path is kept for unit/integration tests and explicitly rejected at runtime outside the harness by EvidenceLockerRuntimeConfigurationValidator).
Consequences
- Air-gap time anchor without external dependencies. Sealed bundles carry an RFC 3161 token from a TSA the operator controls; verification of the token uses the TSA cert shipped in the portable bundle’s trust material — no network at verify time.
- Operator responsibility. Production installs must provision a real TSA signing certificate (internal CA or sovereign crypto); the dev cert is not for production. This is documented in the EvidenceLocker runbook alongside the KEK/secret posture.
- Determinism. The timestamp token is wall-clock and therefore not folded into any content hash or Merkle root — it is an additional attestation of existence-time, never an input to the deterministic bundle digest. Replay and offline verification of the bundle content are unaffected by the token.
- Regional crypto. The TSA cert can itself be issued under a regional profile (eIDAS/GOST/SM) via the crypto plugins; the RFC 3161 exchange is algorithm-agnostic. This composes with ADR-030 (region-aware signing) and WS-6.
- Boot coupling. EvidenceLocker in production mode now depends on the TSA service being healthy; the compose
depends_on+ healthcheck make this explicit, and a TSA outage fails sealing visibly rather than silently degrading to an unsigned timestamp.
Dependency / license gate (§2.6)
| Component | Upstream license | BUSL-1.1 compatible? | Notes |
|---|---|---|---|
OpenSSL 3.x (openssl ts) | Apache License 2.0 | Yes (permissive) | Already relied on across the platform; no new copyleft obligation. TSA image built in-repo from an OpenSSL base, not a vendored third-party TSA binary. |
| Alpine Linux base | MIT + assorted permissive | Yes (permissive) | Minimal base for the in-repo image (devops/docker/tsa/); no copyleft obligation on the userland. |
CPython 3 (python3 http wrapper) | PSF License | Yes (permissive) | Minimal http.server wrapping openssl ts -reply; from the Alpine base, no vendored code. |
Implementation status (2026-07-07, CLO-2b): built + wired + proven live. The image lives at devops/docker/tsa/ and listens on :318 (non-root uid 10001 via sysctls: net.ipv4.ip_unprivileged_port_start=0). Two dark product wirings were fixed to make production-mode sealing actually emit a token: the real Rfc3161TimestampAuthorityClient was never DI-registered (only the Null client), and EvidenceSignatureService could not load the durable Ed25519 seed (Algorithm=ED25519). A live POST /evidence/snapshot now returns a Merkle root plus a 4836-byte RFC 3161 token from CN=StellaOps DEV Timestamp Authority.
NOTICE.md and docs/legal/THIRD-PARTY-DEPENDENCIES.md record the OpenSSL-based TSA image. No cloud-managed TSA is introduced (honors the on-prem/sovereign-first invariant).
Alternatives considered
- Public/cloud TSA (Sigstore, DigiCert, freetsa.org): rejected — violates offline/air-gap posture and adds an external trust + availability dependency at seal time.
- No timestamp (Merkle root only): rejected — the Merkle root proves integrity but not existence-time; an auditor cannot bound when the evidence was sealed.
- Reuse the Attestor transparency log’s timestamp: the internal transparency log’s signed checkpoint (SPRINT_20260704_009 SER-5b) anchors inclusion order/time within the log, but is not an RFC 3161 token an external RFC 3161 verifier accepts; the two are complementary.
