From 302750f2c141cab7bfd91a64767d800ab68f4f83 Mon Sep 17 00:00:00 2001 From: Lior Date: Wed, 27 May 2026 15:42:58 -0400 Subject: [PATCH 1/2] =?UTF-8?q?fix(injection-points):=20KDF=20chain=20docu?= =?UTF-8?q?mentation=20=E2=80=94=20restructure=20for=20accurate=20markdown?= =?UTF-8?q?=20rendering=20+=20work-factor-not-entropy=20wording=20(address?= =?UTF-8?q?es=20Copilot=20findings=20on=20PR=20#5608;=20supersedes=20#5608?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supersedes PR #5608. Two valid Copilot findings on that PR: Finding 1 (markdown rendering): My earlier fix put backslash-escaped pipes (\|\| and "\|") inside inline code spans in a table cell. Markdown code spans render backslashes literally, so readers saw "\|\|" / "\"\|\"" instead of intended "||" / "|". Finding 2 (entropy wording): "stretches low-entropy passphrase into high-entropy intermediate" is misleading — scrypt does NOT increase the underlying entropy of a weak passphrase (in information-theoretic terms, a weak passphrase remains weak). What scrypt provides is a tunable work-factor cost per guess, making brute-force memory- prohibitively expensive on GPU/ASIC. Both addressed by restructuring: - Table cell at line 116 simplified to: "AES-256-GCM; key derived via 2-layer scrypt → HKDF chain (full mechanism + parameters below)" — no pipe-in-inline-code issue - New "KDF chain detail (mechanism + parameters)" sub-section below the table with full mechanism in code blocks (markdown code blocks don't have the pipe-escaping issue inline code in table cells has) - Wording corrected: scrypt provides "tunable work-factor cost per guess" + "makes brute-force memory-prohibitively expensive on GPU/ASIC" + "scrypt is the layer that makes the IKM cryptographically suitable for HKDF input" (per OWASP guidance; per the 2026-05-27 security-review HIGH finding rationale documented in the zeta-creds-crypto.ts source) LAYER 1 — scrypt: stretched = scrypt(passphrase, salt, length=32, N=2^17, r=8, p=1, maxmem=256MB) LAYER 2 — HKDF-SHA256: ikm = concat(usbUuid_utf8, "|", stretched) key = HKDF-SHA256(ikm, salt, info="zeta-b0852-cred-persistence-v1", length=32) Operator-named threat preserved verbatim: "we can put a key on the usb too if wnated tied to the uuid so it can't be copied to uuid" — the HKDF UUID-binding defense. Why supersedes (not fix-fwd): PR #5608 used force-push-restricted branch path; new-branch path is policy-respected alternative. Pattern matches PR #5620 superseding PR #5606. Copilot thread IDs to resolve when closing #5608: - PRRT_kwDOSF9kNM6FNUvq (line 116, backslash escaping) - PRRT_kwDOSF9kNM6FNUwj (line 116, entropy wording) Co-Authored-By: Claude --- full-ai-cluster/INJECTION-POINTS.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/full-ai-cluster/INJECTION-POINTS.md b/full-ai-cluster/INJECTION-POINTS.md index 385f2067a8..8decb19735 100644 --- a/full-ai-cluster/INJECTION-POINTS.md +++ b/full-ai-cluster/INJECTION-POINTS.md @@ -113,12 +113,37 @@ Allowlist from `zflash.ts`: | **Stage** | Cluster console at install time → encrypted blob persisted to USB ESP after successful auth (post-install service trigger) | | **Content class** | **Secret material** (encrypted-at-rest; key never hits disk) | | **Operator-driven via** | Boot-sequence auth-method picker (4 options: restore-from-blob / fresh-device-flow / operator-PAT / skip) + operator passphrase | -| **Encryption** | AES-256-GCM with key derived from `HKDF(USB-UUID \|\| operator-passphrase, salt, info)` | +| **Encryption** | AES-256-GCM; key derived via 2-layer scrypt → HKDF chain (full mechanism + parameters below) | | **ESP filenames** | `/esp/zeta-creds.enc` (encrypted) + `/esp/zeta-creds-manifest.yaml` (declarative + operator-readable) | | **Backlog** | [B-0852](../docs/backlog/P1/B-0852-credential-persistence-on-usb-esp-plus-boot-sequence-auth-method-picker-encrypted-blob-bound-to-usb-uuid-plus-operator-passphrase-aaron-2026-05-27.md) (P1, open, M-effort) | | **Covers credentials** | per declarative manifest: `gh-cli` (`~/.config/gh/hosts.yml`), `claude` (per-persona), `gemini` (per-persona), `codex` (per-persona), `ssh-host-keys`, `ssh-operator-pubkey` | | **Constitutional-rail compliance** | Secret material; encrypted-at-rest on ESP IS allowed because the operator-passphrase + USB-UUID binding means the ESP-stored blob is useless without operator presence — the consent floor stays at operator-typed passphrase, not at USB-physical possession alone | +#### KDF chain detail (mechanism + parameters) + +The 32-byte AES-256-GCM key is derived in two layers; full implementation at `tools/installer/zeta-creds-crypto.ts:80-125`. + +**Layer 1 — scrypt** (memory-hard work-factor KDF): + +```text +stretched = scrypt(passphrase, salt, length=32, N=2^17, r=8, p=1, maxmem=256MB) +``` + +scrypt does NOT increase the underlying entropy of the operator passphrase (a weak passphrase remains weak in information-theoretic terms). What scrypt provides is a tunable **work-factor cost** per guess: each candidate passphrase requires ~128MB of memory and ~1-2 seconds of CPU per derivation. This makes brute-force attacks memory-prohibitively expensive on GPU/ASIC (per the 2026-05-27 security-review HIGH finding: HKDF alone assumes high-entropy IKM, which user-typed passphrases violate; scrypt is the layer that makes the IKM cryptographically suitable for HKDF input). + +OWASP 2026 recommended parameters: `N=2^17`, `r=8`, `p=1`. + +**Layer 2 — HKDF-SHA256** (binds key to USB UUID): + +```text +ikm = concat(usbUuid_utf8, "|", stretched) +key = HKDF-SHA256(ikm, salt, info="zeta-b0852-cred-persistence-v1", length=32) +``` + +HKDF binds the stretched secret to the USB UUID via IKM concatenation. Wrong USB → different IKM → different HKDF output → AES-GCM auth tag verification fails → structured error returned (not garbled plaintext). Defends against copy-blob-to-different-USB attack (operator-named threat 2026-05-27: *"we can put a key on the usb too if wnated tied to the uuid so it can't be copied to uuid"*). + +Both layers must reproduce identically at decrypt time for the AES-GCM auth tag to verify; salt is per-blob (generated at encrypt time; stored in envelope; required at decrypt). + ### 6. GitHub-creds-at-flash-time variants (B-0852 picker options 1 + 3) Per operator 2026-05-27 verbatim: *"the current ones on my machine OR a token i generate on the website."* From 9cd90170aee16410d1738e10220324a96707cd3f Mon Sep 17 00:00:00 2001 From: Lior Date: Wed, 27 May 2026 15:47:01 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix(injection-points):=20address=202=20Copi?= =?UTF-8?q?lot=20findings=20on=20PR=20#5621=20=E2=80=94=20replace=20brittl?= =?UTF-8?q?e=20line-range=20reference=20with=20symbol+constants=20referenc?= =?UTF-8?q?e;=20cite=20OWASP=20cheat=20sheet=20for=20scrypt=20parameters?= =?UTF-8?q?=20+=20soften=20operational-cost=20claim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot caught 2 P1 findings on PR #5621 (the supersedes-#5608 fix). Both valid. Finding 1 (P1, line 124, brittle line reference): Was: `tools/installer/zeta-creds-crypto.ts:80-125` (line numbers drift; doesn't cover SCRYPT_N + SCRYPT_MAXMEM constants declared higher in the file) Fixed: `tools/installer/zeta-creds-crypto.ts` (the `deriveKey` function + the `SCRYPT_*` + `KEY_LEN` + `SALT_LEN` + `HKDF_INFO` constants declared near the top of the file) — symbol-based + constants-named; survives line-number drift. Finding 2 (P1, line 134, uncited operational + standard claims): Was: "OWASP 2026 recommended parameters: N=2^17, r=8, p=1." + "~1-2 seconds of CPU per derivation" (no citation; generalized across-machines) Fixed: - Concrete citation: [OWASP Password Storage Cheat Sheet] (https://cheatsheetseries.owasp.org/cheatsheets/ Password_Storage_Cheat_Sheet.html#scrypt) at parameter- selection date 2026-05-27 - Bump procedure named: visit cheat sheet at next security-review cadence; update both the cheat-sheet-citation date here AND the SCRYPT_N/SCRYPT_R/SCRYPT_P constants in zeta-creds-crypto.ts - Operational cost claim softened: "per the source-code comment's empirical timing context, on the maintainer's modern CPU at parameter-selection time, ~1-2 seconds of CPU per derivation" + "per-machine operational cost will vary with CPU + memory bandwidth" — substrate-honest; no across-machines generalization. Per .claude/rules/blocked-green-ci-investigate-threads.md verify- before-fix: both findings inspected via direct line-level reading; both confirmed real + fixed. Copilot thread IDs to resolve after merge: - PRRT_kwDOSF9kNM6FNhdV (line 124, brittle line reference) - PRRT_kwDOSF9kNM6FNhd3 (line 134, uncited OWASP + operational cost) Pre-existing MD060 IDE warnings (compact table column-style without spaces around pipes) NOT addressed in this commit — same warnings on all prior-merged commits to this file; not CI-blocking. Co-Authored-By: Claude --- full-ai-cluster/INJECTION-POINTS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/full-ai-cluster/INJECTION-POINTS.md b/full-ai-cluster/INJECTION-POINTS.md index 8decb19735..73e8675240 100644 --- a/full-ai-cluster/INJECTION-POINTS.md +++ b/full-ai-cluster/INJECTION-POINTS.md @@ -121,7 +121,7 @@ Allowlist from `zflash.ts`: #### KDF chain detail (mechanism + parameters) -The 32-byte AES-256-GCM key is derived in two layers; full implementation at `tools/installer/zeta-creds-crypto.ts:80-125`. +The 32-byte AES-256-GCM key is derived in two layers; full implementation in `tools/installer/zeta-creds-crypto.ts` (the `deriveKey` function + the `SCRYPT_*` + `KEY_LEN` + `SALT_LEN` + `HKDF_INFO` constants declared near the top of the file). **Layer 1 — scrypt** (memory-hard work-factor KDF): @@ -129,9 +129,9 @@ The 32-byte AES-256-GCM key is derived in two layers; full implementation at `to stretched = scrypt(passphrase, salt, length=32, N=2^17, r=8, p=1, maxmem=256MB) ``` -scrypt does NOT increase the underlying entropy of the operator passphrase (a weak passphrase remains weak in information-theoretic terms). What scrypt provides is a tunable **work-factor cost** per guess: each candidate passphrase requires ~128MB of memory and ~1-2 seconds of CPU per derivation. This makes brute-force attacks memory-prohibitively expensive on GPU/ASIC (per the 2026-05-27 security-review HIGH finding: HKDF alone assumes high-entropy IKM, which user-typed passphrases violate; scrypt is the layer that makes the IKM cryptographically suitable for HKDF input). +scrypt does NOT increase the underlying entropy of the operator passphrase (a weak passphrase remains weak in information-theoretic terms). What scrypt provides is a tunable **work-factor cost** per guess: with the parameters below, each candidate passphrase requires ~128MB of memory and (empirically per `zeta-creds-crypto.ts` Layer 1 source comments, on the maintainer's modern CPU at parameter-selection time) ~1-2 seconds of CPU per derivation. This makes brute-force attacks memory-prohibitively expensive on GPU/ASIC (per the 2026-05-27 security-review HIGH finding documented in the source: HKDF alone assumes high-entropy IKM, which user-typed passphrases violate; scrypt is the layer that makes the IKM cryptographically suitable for HKDF input). -OWASP 2026 recommended parameters: `N=2^17`, `r=8`, `p=1`. +Parameter selection: `N=2^17`, `r=8`, `p=1` — per [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#scrypt) recommended scrypt parameters (current at parameter-selection time 2026-05-27; bump procedure: visit the cheat sheet at next security-review cadence, update both the cheat-sheet-citation date here AND the `SCRYPT_N`/`SCRYPT_R`/`SCRYPT_P` constants in `zeta-creds-crypto.ts`). Per-machine operational cost will vary with CPU + memory bandwidth; the ~1-2s figure is anchored to the source-code comment's empirical timing context. **Layer 2 — HKDF-SHA256** (binds key to USB UUID):