]> git.codecow.com Git - nano25519.git/commitdiff
Update agent file.
authorChris Duncan <chris@zoso.dev>
Sat, 19 Sep 2026 06:27:29 +0000 (23:27 -0700)
committerChris Duncan <chris@zoso.dev>
Sat, 19 Sep 2026 06:27:29 +0000 (23:27 -0700)
AGENTS.md

index 50e8a75a6637e3896fa23ce7ee5c885f16676018..0a8beb059034667c8d9b19853211365076924978 100644 (file)
--- 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