From: Chris Duncan Date: Sat, 19 Sep 2026 06:27:29 +0000 (-0700) Subject: Update agent file. X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=705e7007294824298b8fb04142285719f8cc00c5;p=nano25519.git Update agent file. --- diff --git a/AGENTS.md b/AGENTS.md index 50e8a75..0a8beb0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -297,8 +297,25 @@ public exports, so a caller can write more bytes than the length it then declares, and a scoped fill leaves the rest behind. A 32 KiB fill costs ~0.1 µs — roughly 0.4% of one signature — so this is never worth optimising away. +**Internal scratch holds key material too, and in shapes a scan will miss.** +The gotcha above is about the *input* buffers of exported functions. The harder +case is module-level scratch on a secret path: `ge_scalarmult_base` expands its +scalar into 64 signed radix-16 digits in `ge_scalarmult_base_e`, and +`ge_cmov8_base` copies the selected base-table entry into `minust`. Neither was +cleared, so `derive` left the clamped secret scalar behind and `sign` left the +nonce `r` — enough, with the signature, to recover the key and forge. Both are +scrubbed now; the rule is that **any** static a secret passes through gets +cleared before the function returns, not just the ones the ABI exposes. + +The reason it survived so long is worth more than the fix: the residue scan +searched for the 32-byte little-endian encodings and truthfully reported "0 +copies" the whole time. A scan finds a *representation*, not a secret. When you +put a secret into a new buffer, enumerate the form it actually takes there — +digit expansion, limb array, precomp entry, negated copy — and add that form to +the scan in the same commit. + **Establish fail-closed defaults before the first early exit.** `verify_blocks` -runs `OUTPUT_VERIFY.fill(255)` *before* validating the public key, so a batch +runs `OUTPUT_VERIFY.fill(0)` *before* validating the public key, so a batch that bails out reports every block as `false` rather than leaving stale bytes. Setting the default in the failure branch instead is the bug this shape avoids: a result buffer is only fail-closed if the default is written before anything