[VS-NfD] Separate token keys with HKDF and replace the homegrown JWT with jose #188

Closed
opened 2026-07-30 01:43:27 +02:00 by fable-5 · 1 comment
Collaborator

Plan reference: docs/vs-nfd/20-massnahmenplan.md -> Phase 2, lines 1+2
ADR: ADR 0020
Effort: L (3–5 AT)
Depends on:

Deliberately merged from two plan checkboxes — the plan itself justifies
it: both touch packages/shared/src/token-crypto.ts, and doing them
separately costs 5–6 AT instead of 3–5.

Context

One secret currently serves two unrelated purposes. Purpose-bound subkeys
derived via HKDF make a compromise of one path non-transferable, and a
vetted JWT implementation removes hand-written crypto from the trust
boundary. This does not introduce a new security base function
(§52 VSA) — it narrows crypto the application already performs.

Current state

  • COLLAB_TOKEN_SECRET (packages/shared/src/env.ts:55,142) signs
    collaboration tokens (apps/collab/src/index.ts:46,
    apps/api/src/pages/pages.service.ts:383) and unsubscribe tokens
    (apps/api/src/notifications/digest.service.ts:161,
    apps/api/src/notifications/notifications.controller.ts:51).
  • packages/shared/src/token-crypto.ts:1,37 implements a compact HS256 JWT
    with node:crypto (createHmac, timingSafeEqual). The file header
    documents the reason: identical code in the CommonJS api and the ESM
    collab server.
  • Neither jose nor jsonwebtoken is a dependency of any workspace
    package.
  • Partial separation already exists: apps/api/src/notifications/unsubscribe-token.ts:14
    prefixes a PURPOSE constant into the HMAC input.

Acceptance criteria

  • Subkeys are derived per purpose via HKDF from the configured root
    secret; no code path signs with the root secret directly.
  • Collaboration tokens are produced and verified by jose, HS256 only,
    with the algorithm allowlist asserted by a test.
  • A cross-runtime test proves the same token verifies in the api
    (CommonJS) and the collab server (ESM) — the reason the homegrown
    implementation existed.
  • Unsubscribe tokens keep a documented dual-verify window (old and
    new derivation accepted) long enough to cover links already in sent
    mail; the window's length and expiry date are documented.
  • Negative tests: token signed with a different purpose's subkey is
    rejected; alg: none and RS256 are rejected.
  • docs/architecture/security.md §"Secrets & configuration" documents
    the key hierarchy; deploy/compose/.env.example documents the root
    secret's role.

Out of scope

Rotation automation, moving secrets into an external KMS, and any change to
session cookies (see #190).

**Plan reference:** `docs/vs-nfd/20-massnahmenplan.md` -> Phase 2, lines 1+2 **ADR:** ADR 0020 **Effort:** L (3–5 AT) **Depends on:** — *Deliberately merged from two plan checkboxes — the plan itself justifies it: both touch `packages/shared/src/token-crypto.ts`, and doing them separately costs 5–6 AT instead of 3–5.* ## Context One secret currently serves two unrelated purposes. Purpose-bound subkeys derived via HKDF make a compromise of one path non-transferable, and a vetted JWT implementation removes hand-written crypto from the trust boundary. This does **not** introduce a new security base function (§52 VSA) — it narrows crypto the application already performs. ## Current state - `COLLAB_TOKEN_SECRET` (`packages/shared/src/env.ts:55,142`) signs collaboration tokens (`apps/collab/src/index.ts:46`, `apps/api/src/pages/pages.service.ts:383`) **and** unsubscribe tokens (`apps/api/src/notifications/digest.service.ts:161`, `apps/api/src/notifications/notifications.controller.ts:51`). - `packages/shared/src/token-crypto.ts:1,37` implements a compact HS256 JWT with `node:crypto` (`createHmac`, `timingSafeEqual`). The file header documents the reason: identical code in the CommonJS api and the ESM collab server. - Neither `jose` nor `jsonwebtoken` is a dependency of any workspace package. - Partial separation already exists: `apps/api/src/notifications/unsubscribe-token.ts:14` prefixes a `PURPOSE` constant into the HMAC input. ## Acceptance criteria - [ ] Subkeys are derived per purpose via HKDF from the configured root secret; no code path signs with the root secret directly. - [ ] Collaboration tokens are produced and verified by `jose`, HS256 only, with the algorithm allowlist asserted by a test. - [ ] A cross-runtime test proves the same token verifies in the api (CommonJS) and the collab server (ESM) — the reason the homegrown implementation existed. - [ ] Unsubscribe tokens keep a documented **dual-verify window** (old and new derivation accepted) long enough to cover links already in sent mail; the window's length and expiry date are documented. - [ ] Negative tests: token signed with a different purpose's subkey is rejected; `alg: none` and `RS256` are rejected. - [ ] `docs/architecture/security.md` §"Secrets & configuration" documents the key hierarchy; `deploy/compose/.env.example` documents the root secret's role. ## Out of scope Rotation automation, moving secrets into an external KMS, and any change to session cookies (see #190).
fable-5 added this to the M24 — VS-NfD: security quick wins milestone 2026-07-30 01:43:27 +02:00
fable-5 added the
vs-nfd
effort:L
area:auth
labels 2026-07-30 01:43:27 +02:00
Author
Collaborator

Delivered by PR #239, merged to main as 3d1f4fd. CI run 472 green; shared/collab/api suites green locally. Closing.

Delivered by PR #239, merged to main as 3d1f4fd. CI run 472 green; shared/collab/api suites green locally. Closing.
Sign in to join this conversation.
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: stwaidele/dorfteich#188
No description provided.