Files
certctl/migrations/000041_prelogin_encrypted.up.sql
T
shankar0123 90210c9334 fix(oidc/prelogin): encrypt state/nonce/PKCE-verifier at rest (HIGH-5)
Pre-login rows previously persisted the OIDC state, nonce, and PKCE
verifier as plaintext columns; an operator restoring an unredacted
backup of oidc_pre_login_sessions to a debug environment leaked every
in-flight handshake. If the IdP also leaked the auth code in the same
window (logged at a misconfigured TLS terminator, etc.), the attacker
could exchange code + verifier directly. RFC 7636 §7 requires verifier
confidentiality.

This commit:
- Migration 000041 adds {state,nonce,pkce_verifier}_enc BYTEA columns
  and makes the legacy plaintext columns nullable. A follow-up
  migration drops the plaintext columns once the rolling deploy
  completes.
- internal/repository/postgres/oidc_prelogin.go::Create encrypts the
  three secrets via crypto.EncryptIfKeySet (v3 magic 0x03 + per-row
  salt + nonce + AES-256-GCM tag) and writes only the encrypted
  columns; legacy plaintext stays NULL on the write path.
- LookupAndConsume prefers encrypted columns via materialize(),
  falling back to the legacy plaintext only when _enc is NULL — the
  rolling-deploy compat layer that 000042 will retire.
- NewPreLoginRepository takes encryptionKey; cmd/server/main.go threads
  cfg.Encryption.ConfigEncryptionKey in.
- Encryption key reuses CERTCTL_CONFIG_ENCRYPTION_KEY (same passphrase
  already protecting OIDC client secrets and SessionSigningKey material).
  No new env var.

Why encryption-at-rest, not HMAC: the spec's HMAC approach required
moving plaintext into the cookie (the cookie currently carries only
row ID + HMAC). Re-shaping the cookie wire format would be a larger
refactor; the audit explicitly admits encryption-at-rest is an
acceptable closure (weaker because backups still contain decryptable
ciphertext, but the encryption key is held separately from the DB
backup, and the 10-minute TTL further bounds usable secret window).

Three new regression tests in oidc_prelogin_encryption_test.go pin:
  (a) _enc columns contain v3-format ciphertext, NOT plaintext
      substrings, post-Create
  (b) legacy plaintext columns are NULL post-Create (defends against
      future patches that re-introduce plaintext writes)
  (c) LookupAndConsume round-trips state/nonce/verifier byte-for-byte
A fourth test pins the legacy-row fallback for rolling-deploy compat.

Refs: cowork/auth-bundles-audit-2026-05-10.md HIGH-5
Spec: cowork/auth-bundles-fixes-2026-05-10/09-high-5-prelogin-secret-protection.md
2026-05-10 21:17:55 +00:00

39 lines
2.1 KiB
SQL

-- =============================================================================
-- 2026-05-10 Audit / HIGH-5 closure
-- =============================================================================
--
-- Pre-login rows in oidc_pre_login_sessions used to persist OIDC state, nonce,
-- and the PKCE verifier as plaintext columns. An operator restoring a backup
-- to a debug environment without redacting handshake-table data leaked every
-- in-flight verifier; combined with a separately-leaked authorization code
-- (e.g. logged at a misconfigured TLS terminator), the attacker could exchange
-- code + verifier directly. RFC 7636 §7 requires verifier confidentiality.
--
-- This migration adds {state,nonce,pkce_verifier}_enc BYTEA columns alongside
-- the existing plaintext columns. The new repository write path emits only the
-- encrypted columns (via internal/crypto.EncryptIfKeySet, v3 blob format —
-- magic(0x03) || salt(16) || nonce(12) || ciphertext+tag, AES-256-GCM with
-- per-row salt + nonce). The existing plaintext columns are made nullable so
-- the new write path doesn't have to populate them; in-flight handshakes from
-- pre-deploy code paths still consume the legacy plaintext columns until the
-- 10-minute absolute TTL expires every legacy row.
--
-- A follow-up migration (queued for v2.1.1) drops the plaintext columns once
-- the rolling deploy completes. We do NOT bundle the DROP into 000041 because
-- in-flight handshakes during deploy would break.
--
-- The encryption key reuses CERTCTL_CONFIG_ENCRYPTION_KEY — the same passphrase
-- already protecting OIDC client secrets, session signing keys, and other
-- secret-bearing rows. No new env var.
-- =============================================================================
ALTER TABLE oidc_pre_login_sessions
ADD COLUMN IF NOT EXISTS state_enc BYTEA,
ADD COLUMN IF NOT EXISTS nonce_enc BYTEA,
ADD COLUMN IF NOT EXISTS pkce_verifier_enc BYTEA;
ALTER TABLE oidc_pre_login_sessions
ALTER COLUMN state DROP NOT NULL,
ALTER COLUMN nonce DROP NOT NULL,
ALTER COLUMN pkce_verifier DROP NOT NULL;