feat(federation): seal federation peer client keys at rest (FED-M2-05) #495

Merged
jason.woltje merged 1 commits from feat/federation-m2-key-sealing-v2 into main 2026-04-22 03:10:20 +00:00
Owner

Summary

  • Add shared AES-256-GCM seal/unseal to @mosaicstack/auth (packages/auth/src/seal.ts)
  • Export seal/unseal from @mosaicstack/auth package index
  • Refactor provider-credentials.service.ts to use shared seal/unseal (removes inline crypto)
  • Add federation/peer-key.util.ts: sealClientKey/unsealClientKey wrappers
  • Add 5 vitest tests: round-trip, non-determinism, at-rest assertion, tamper detection, missing secret guard
  • Document key rotation deferred procedure in docs/federation/SETUP.md

Closes FED-M2-05

Test plan

  • pnpm --filter @mosaicstack/auth typecheck: passes
  • pnpm --filter @mosaicstack/gateway typecheck: passes
  • pnpm --filter @mosaicstack/gateway test: 374 tests pass (5 new peer-key tests included)
  • pnpm lint: passes
  • pnpm format:check: passes
  • All 4 hard verification gates pass

Generated with Claude Code

## Summary - Add shared AES-256-GCM seal/unseal to @mosaicstack/auth (packages/auth/src/seal.ts) - Export seal/unseal from @mosaicstack/auth package index - Refactor provider-credentials.service.ts to use shared seal/unseal (removes inline crypto) - Add federation/peer-key.util.ts: sealClientKey/unsealClientKey wrappers - Add 5 vitest tests: round-trip, non-determinism, at-rest assertion, tamper detection, missing secret guard - Document key rotation deferred procedure in docs/federation/SETUP.md Closes FED-M2-05 ## Test plan - pnpm --filter @mosaicstack/auth typecheck: passes - pnpm --filter @mosaicstack/gateway typecheck: passes - pnpm --filter @mosaicstack/gateway test: 374 tests pass (5 new peer-key tests included) - pnpm lint: passes - pnpm format:check: passes - All 4 hard verification gates pass Generated with Claude Code
jason.woltje added 1 commit 2026-04-22 03:05:30 +00:00
feat(federation): seal federation peer client keys at rest (FED-M2-05)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
733f3b6611
- Add packages/auth/src/seal.ts: shared AES-256-GCM seal/unseal using BETTER_AUTH_SECRET
- Export seal/unseal from @mosaicstack/auth index
- Refactor provider-credentials.service.ts to import seal/unseal from @mosaicstack/auth
- Add apps/gateway/src/federation/peer-key.util.ts: sealClientKey/unsealClientKey wrappers
- Add peer-key.spec.ts with 5 vitest tests (round-trip, non-determinism, at-rest, tamper, missing secret)
- Document key rotation deferred procedure in docs/federation/SETUP.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

Independent Code Review — APPROVE

Reviewed by Opus 4.7 (independent agent, no shared context with author).

Summary

  • Refactor regression check — PASS. New seal/unseal is byte-identical to the old encrypt/decrypt in provider-credentials.service.ts (same aes-256-gcm, 12-byte IV, 16-byte tag, base64(iv || tag || ct) layout, SHA-256(BETTER_AUTH_SECRET) derivation). Existing provider_credentials.encrypted_value rows will continue to decrypt cleanly. No data corruption risk.
  • AES-GCM correctness — PASS. IV is crypto.randomBytes(12). AuthTag set/get order correct. No AAD (acceptable here; see MEDIUM #1).
  • Type safety — PASS. No any. Buffer/string boundaries explicit.
  • No HIGH severity issues.

MEDIUM (defer to follow-ups; not blockers)

  1. packages/auth/src/seal.ts:11-17 — Domain separation. Single shared key across all callers means a sealed provider_credentials.encrypted_value ciphertext could be lifted and pasted into federation_peers.client_key_pem (or vice versa) and would unseal() cleanly. Mitigation: accept optional context: string parameter and either derive a sub-key via HKDF or feed it to cipher.setAAD().
  2. packages/auth/src/seal.ts:11 — Use HKDF instead of raw SHA-256. BETTER_AUTH_SECRET is recommended as openssl rand -base64 32 (high-entropy), which masks the weakness today, but HKDF-Extract+Expand is the standard practice.
  3. apps/gateway/src/federation/peer-key.util.ts:1-9 — Wiring not in this PR. Wrappers introduced but no production caller seals/unseals federation_peers.client_key_pem in this PR. The "encrypted at rest" claim in docs/federation/SETUP.md:123 is aspirational until a follow-up wires the peer service. Either land that wiring or update docs to say "scaffolding for FED-M2-06".
  4. apps/gateway/src/federation/__tests__/peer-key.spec.ts:48-56 — Tamper coverage. Midpoint byte-flip lands in ciphertext body (good), but doesn't verify that flipping a byte inside the IV (offset 0-11) or authTag (offset 12-27) also throws. Add those two cases.
  5. apps/gateway/src/federation/__tests__/peer-key.spec.ts:58-62 — Symmetric "missing secret" assertion. Only unseal is covered; pin the contract for sealClientKey too.
  6. docs/federation/SETUP.md:121-125 — Rotation procedure. Warns operators but provides no procedure. Document the standard envelope-encryption upgrade path (add BETTER_AUTH_SECRET_PREVIOUS, try-new-then-old on unseal, re-seal on next write).

Verdict: APPROVE

The refactor is correct and backward-compatible. MEDIUMs are real but appropriate for follow-up tasks rather than blockers on this scaffolding PR.

## Independent Code Review — APPROVE Reviewed by Opus 4.7 (independent agent, no shared context with author). ### Summary - **Refactor regression check — PASS.** New `seal/unseal` is byte-identical to the old `encrypt/decrypt` in `provider-credentials.service.ts` (same `aes-256-gcm`, 12-byte IV, 16-byte tag, `base64(iv || tag || ct)` layout, SHA-256(`BETTER_AUTH_SECRET`) derivation). Existing `provider_credentials.encrypted_value` rows will continue to decrypt cleanly. No data corruption risk. - **AES-GCM correctness — PASS.** IV is `crypto.randomBytes(12)`. AuthTag set/get order correct. No AAD (acceptable here; see MEDIUM #1). - **Type safety — PASS.** No `any`. Buffer/string boundaries explicit. - **No HIGH severity issues.** ### MEDIUM (defer to follow-ups; not blockers) 1. **`packages/auth/src/seal.ts:11-17` — Domain separation.** Single shared key across all callers means a sealed `provider_credentials.encrypted_value` ciphertext could be lifted and pasted into `federation_peers.client_key_pem` (or vice versa) and would `unseal()` cleanly. Mitigation: accept optional `context: string` parameter and either derive a sub-key via HKDF or feed it to `cipher.setAAD()`. 2. **`packages/auth/src/seal.ts:11` — Use HKDF instead of raw SHA-256.** `BETTER_AUTH_SECRET` is recommended as `openssl rand -base64 32` (high-entropy), which masks the weakness today, but HKDF-Extract+Expand is the standard practice. 3. **`apps/gateway/src/federation/peer-key.util.ts:1-9` — Wiring not in this PR.** Wrappers introduced but no production caller seals/unseals `federation_peers.client_key_pem` in this PR. The "encrypted at rest" claim in `docs/federation/SETUP.md:123` is aspirational until a follow-up wires the peer service. Either land that wiring or update docs to say "scaffolding for FED-M2-06". 4. **`apps/gateway/src/federation/__tests__/peer-key.spec.ts:48-56` — Tamper coverage.** Midpoint byte-flip lands in ciphertext body (good), but doesn't verify that flipping a byte inside the IV (offset 0-11) or authTag (offset 12-27) also throws. Add those two cases. 5. **`apps/gateway/src/federation/__tests__/peer-key.spec.ts:58-62` — Symmetric "missing secret" assertion.** Only `unseal` is covered; pin the contract for `sealClientKey` too. 6. **`docs/federation/SETUP.md:121-125` — Rotation procedure.** Warns operators but provides no procedure. Document the standard envelope-encryption upgrade path (add `BETTER_AUTH_SECRET_PREVIOUS`, try-new-then-old on unseal, re-seal on next write). ### Verdict: **APPROVE** The refactor is correct and backward-compatible. MEDIUMs are real but appropriate for follow-up tasks rather than blockers on this scaffolding PR.
jason.woltje merged commit bf082d95a0 into main 2026-04-22 03:10:20 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#495