#188: purpose-bound token keys via HKDF + jose #239

Merged
stwaidele merged 1 commits from feat/188-token-key-separation into main 2026-07-30 08:47:21 +02:00
Collaborator

STACKED on PR #238 (contains its commit; merge #238 first, then this fast-forwards).

Implements ADR 0020:

  • COLLAB_TOKEN_SECRET becomes a root key; each purpose derives a 32-byte HKDF-SHA-256 subkey (deriveTokenKey). No code path signs with the root key - proven by a test.
  • Collaboration tokens signed/verified by jose, HS256 as an explicit allowlist (algorithm-allowlist test, RS256 and alg:none rejected). Sign/verify turn async at their three call sites.
  • Unsubscribe tokens: structural subkey replaces the purpose-prefix string; documented dual-verify window until 2026-11-01 (LEGACY_VERIFY_UNTIL) covering the 90-day TTL of links in already-sent mail, with tests for both sides of the window.
  • Cross-runtime test round-trips tokens between the built CJS and ESM dist artefacts in child processes - the property that justified the homegrown JWT (jose v6 reaches CJS via require(esm); Node 22.15 images).
  • Docs: key hierarchy in security.md, root-key role in .env.example. One new dependency (jose, dependency-free).

Tested: shared 185 (incl. 12 collab-token + 3 cross-runtime), collab full suite 40/40 vs fresh DB, api full suite 431 green vs fresh DB (incl. 7 new unsubscribe tests), typecheck + lint green.

Refs #188 (close manually after green CI + merge).

Generated with Claude Code (Claude Fable 5)

STACKED on PR #238 (contains its commit; merge #238 first, then this fast-forwards). Implements ADR 0020: - COLLAB_TOKEN_SECRET becomes a root key; each purpose derives a 32-byte HKDF-SHA-256 subkey (deriveTokenKey). No code path signs with the root key - proven by a test. - Collaboration tokens signed/verified by jose, HS256 as an explicit allowlist (algorithm-allowlist test, RS256 and alg:none rejected). Sign/verify turn async at their three call sites. - Unsubscribe tokens: structural subkey replaces the purpose-prefix string; documented dual-verify window until 2026-11-01 (LEGACY_VERIFY_UNTIL) covering the 90-day TTL of links in already-sent mail, with tests for both sides of the window. - Cross-runtime test round-trips tokens between the built CJS and ESM dist artefacts in child processes - the property that justified the homegrown JWT (jose v6 reaches CJS via require(esm); Node 22.15 images). - Docs: key hierarchy in security.md, root-key role in .env.example. One new dependency (jose, dependency-free). Tested: shared 185 (incl. 12 collab-token + 3 cross-runtime), collab full suite 40/40 vs fresh DB, api full suite 431 green vs fresh DB (incl. 7 new unsubscribe tests), typecheck + lint green. Refs #188 (close manually after green CI + merge). Generated with Claude Code (Claude Fable 5)
fable-5 added 2 commits 2026-07-30 06:41:32 +02:00
#226: add the §52 VSA delimitation statement
Some checks failed
CI / Build container images (pull_request) Successful in 1m10s
CI / Auth e2e pack (pull_request) Successful in 7m46s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Deploy to Test (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (pull_request) Successful in 4m43s
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CI / Lint, typecheck, test (push) Has been cancelled
3e377aaa57
Reviewer-facing document derived from ADR 0019: per base function
(encryption, media protection, network termination, authentication,
integrity) what the application does, what it deliberately does not,
and which party provides it — every claim traceable to code via the
ist-aufnahme. Includes the operator-duty handover (with the IndexedDB
endpoint copy named explicitly, I-25) and the delta list mapping every
divergence from the target state to its closing issue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
#188: purpose-bound token keys via HKDF, jose replaces the homegrown JWT
All checks were successful
CI / Build container images (pull_request) Successful in 3m51s
CI / Auth e2e pack (pull_request) Successful in 7m49s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CI / Lint, typecheck, test (pull_request) Successful in 4m43s
CD / Build and push images (push) Successful in 20s
CD / Deploy to Test (push) Successful in 16s
CD / Smoke tests against Test (push) Successful in 1m19s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m54s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m39s
CI / Import/export fidelity gate (push) Successful in 59s
3d1f4fda53
COLLAB_TOKEN_SECRET becomes a root key: every purpose derives its own
HKDF-SHA-256 subkey (deriveTokenKey), and no code path signs with the
root key directly. Collaboration tokens are signed and verified by jose
with HS256 as an explicit allowlist; the sign/verify API turns async at
its three call sites. Unsubscribe tokens move from a purpose-prefix
string to the structural subkey, with a documented dual-verify window
(legacy derivation accepted until 2026-11-01, covering the 90-day TTL
of links in already-sent mail).

The cross-runtime property that justified the homegrown implementation
is now proven by a test: the built CJS and ESM dist artefacts round-trip
tokens in both directions in child processes (jose v6 reaches CJS via
Node's require(esm), pinned Node 22 images). Negative tests cover
cross-purpose subkeys, root-key-signed tokens, alg:none and RS256.

Refs #188 (ADR 0020)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
stwaidele merged commit 3d1f4fda53 into main 2026-07-30 08:47:21 +02:00
Sign in to join this conversation.
No description provided.