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

60 lines
2.7 KiB
Markdown

# 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.