dorfteich/docs/architecture/adr/0020-token-crypto-key-separation.md
Claude Opus 5 fd07f716f6
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m42s
CI / Build container images (pull_request) Successful in 1m11s
CI / Auth e2e pack (pull_request) Successful in 7m47s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 18s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m50s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m38s
CI / Import/export fidelity gate (push) Successful in 56s
docs: VS-NfD readiness planning (ist-aufnahme, plan, ADRs 0019-0026, issue drafts)
Add docs/vs-nfd/: the analysis brief, the as-is assessment (42 findings,
all verified against the code), the prioritized action plan rev. 2 with
issue references written back to every checkbox, the two-stage issue/ADR
brief, and the full reviewed draft used to create the forge state.

Add eight proposed ADRs 0019-0026 covering the VS-NfD architecture
decisions: no security base functions (par. 52 VSA anchor), HKDF token
key separation, external authentication, page classification, read-access
audit trail (variant A), reproducible offline deployment, plugin trust
model, and backup target restriction.

Forge state created alongside this commit: 11 labels, milestones M24-M31,
issues #188-#236 (docs-only change, no code touched).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 01:48:05 +02:00

2.7 KiB

ADR 0020: Token crypto — HKDF key separation and a vetted JWT library

  • Status: proposed
  • Date: 2026-07-29

Context

COLLAB_TOKEN_SECRET currently signs two unrelated kinds of token: short-lived collaboration tokens (issue #34) and long-lived unsubscribe tokens in outgoing mail. A single secret across purposes means a compromise in one path is transferable to the other.

packages/shared/src/token-crypto.ts implements the compact HS256 JWT by hand on node:crypto. The reason is documented in the file and is a good one: the identical code has to run in the CommonJS api and the ESM collab server without module-interop or dependency-version drift. The implementation is careful — HS256 only, constant-time comparison before any untrusted field is read. It is nevertheless hand-written crypto in the trust boundary, which is a finding in any assessment regardless of its quality.

ADR 0019 states the application implements no security base function. Token signing is not one — but it is crypto we do perform, so it has to be minimal, purpose-bound and delegated to a vetted implementation.

Decision

  1. Purpose-bound subkeys via HKDF. The configured secret becomes a root key from which each purpose derives its own subkey (collaboration tokens, unsubscribe tokens, any future purpose). No code path signs with the root key.
  2. jose replaces the homegrown JWT. It is maintained, audited, works in both module systems, and is dependency-free — which matters for the supply-chain argument. HS256 stays the only accepted algorithm, as an explicit allowlist rather than an implicit default.
  3. Purpose separation is structural, not textual. The existing PURPOSE string prefix in unsubscribe-token.ts is superseded by key separation; a token signed for one purpose cannot verify under another because the key differs.
  4. Long-lived tokens get a documented dual-verify window. Unsubscribe links live in mail that has already been sent, so both derivations are accepted for a stated period with a stated expiry date. The window is a documented fact, not an accident.

Consequences

  • The cross-runtime property that motivated the hand-written code must be proven by a test, not assumed — otherwise the reason for the original decision is lost silently.
  • One new runtime dependency. Accepted: jose has no transitive dependencies, so the supply-chain delta is one package.
  • Rotating the root key invalidates all derived subkeys at once, which is the desired behaviour and needs documenting in the operations manual.
  • The key hierarchy becomes part of the security documentation (#228) and the delimitation statement's crypto section (#226).

Implementing issues

#188.