diff --git a/apps/api/src/routes/setup.ts b/apps/api/src/routes/setup.ts index 737c597..e6c7b01 100644 --- a/apps/api/src/routes/setup.ts +++ b/apps/api/src/routes/setup.ts @@ -193,10 +193,17 @@ setupRouter.post('/validate/oidc', async (c) => { // --------------------------------------------------------------------------- // POST /api/setup/validate/vapid // -// Validates VAPID key pair structure by calling webpush.setVapidDetails(). -// Reads BOTH keys ONLY from process.env — NEVER from app_config (T-12-06 / D-01 / SC-3). -// VAPID_PRIVATE_KEY is never returned in any response. -// Returns 200 { ok: true } on structural validity, 400 { ok: false } on failure. +// Validates the operator-entered VAPID public key (app_config.vapid_public_key) +// against the configured key pair: +// 1. Equality (gap 2): the submitted public key MUST equal process.env.VAPID_PUBLIC_KEY. +// A wrong/typoed key (e.g. "BH123") now fails the row and gates Continue — push +// would silently break in production otherwise (SETUP-02). +// 2. Structural: webpush.setVapidDetails() validates the env pair's byte structure. +// +// VAPID_PRIVATE_KEY is read ONLY from process.env — NEVER from app_config or returned +// (T-12-06 / D-01 / SC-3). The equality check compares the submitted PUBLIC key to the +// env PUBLIC key only — the private key is never compared or echoed. +// Returns 200 { ok: true } on success, 400 { ok: false } on any failure. // --------------------------------------------------------------------------- setupRouter.post('/validate/vapid', async (c) => { @@ -210,6 +217,23 @@ setupRouter.post('/validate/vapid', async (c) => { return c.json({ ok: false, error: 'VAPID_PRIVATE_KEY and VAPID_PUBLIC_KEY env vars must be set' }, 400); } + // Gap 2: assert the operator-submitted public key matches the env public key BEFORE + // the structural check. Read the submitted key from app_config (same idiom as validate/oidc). + const [submittedRow] = await db + .select({ value: appConfig.value }) + .from(appConfig) + .where(eq(appConfig.key, 'vapid_public_key')) + .limit(1); + + const submittedPublicKey = submittedRow?.value; + if (!submittedPublicKey || submittedPublicKey !== publicKey) { + return c.json({ + ok: false, + error: + 'VAPID public key does not match the configured key pair. Paste the exact VAPID_PUBLIC_KEY printed by `npm run generate-secrets`.', + }, 400); + } + try { // setVapidDetails runs validatePrivateKey (32-byte check) and validatePublicKey (65-byte check) // internally — this is the library's own structural validation (Pattern 7).