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
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
60 lines
2.7 KiB
Markdown
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.
|