From a12870d45acc45a4ee6c66c7dd9d19eade4a84c7 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Fri, 28 Aug 2026 11:45:12 -0700 Subject: [PATCH] Update agent file. --- AGENTS.md | 73 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index cbda5c7..e9a34f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -100,7 +100,7 @@ every pointer and byte length once at module load and re-exports them. | Buffer | Purpose | | --- | --- | -| `INPUT_MSG` | 32 KiB. A message for `sign`/`verify`, or packed 96-byte records for `verify_blocks` | +| `INPUT_MSG` | 64 KiB. A message for `sign`/`verify`, or packed 96-byte records for `verify_blocks` | | `INPUT_PRV`, `INPUT_PUB` | 32 bytes each | | `INPUT_SIG` | 64 B, one signature for single `verify` | | `OUTPUT_DERIVE` | 32 B, a public key | @@ -116,12 +116,8 @@ byte per block, *and* at least 64 bytes for a signature. Only the first was ever written down, so cutting the batch cap to 32 made `sign` overrun it. If you ever merge them again, both constraints have to be stated together. -`INPUT_MSG` still has this problem. It holds **either** a message for -`sign`/`verify` (up to `MAX_MESSAGE_BYTELENGTH`) **or** a packed batch -(`MAX_VERIFY_BLOCKS * SIGNEDBLOCK_BYTELENGTH` = 3,072 B). Only the first is -named. `1 << 15` satisfies both with room to spare, but it was briefly `1 << 10` -and a full batch overran it. **Before shrinking `MAX_MESSAGE_BYTELENGTH` or -raising `MAX_VERIFY_BLOCKS`, check both roles still fit.** +`INPUT_MSG` holds **either** a message for `sign`/`verify` **or** a packed +batch, and both roles are now in one expression, so the check is automatic. Records in `INPUT_MSG` are **`signature` then `hash`** (64 B + 32 B), libsodium combined-mode order. Nothing validates this; both sides just have to agree. @@ -129,25 +125,35 @@ combined-mode order. Nothing validates this; both sides just have to agree. The batch cap is `MAX_VERIFY_BLOCKS` (32); the wasm guard, the host guard and `OUTPUT_VERIFY` all read it, so they cannot drift. -⚠️ **`MAX_MESSAGE_BYTELENGTH` is currently derived from the wrong constant:** +`MAX_MESSAGE_BYTELENGTH` is a **deliberately chosen policy limit** — a cap on +how large a message this package will sign at all, so oversized inputs stay out +of scope. It sizes `INPUT_MSG`; the buffer does not constrain it. Its floor is +one full page, raised to whatever a full batch needs if that is ever larger: ```ts -export const MAX_VERIFY_BLOCKS: i32 = 32 -export const MAX_MESSAGE_BYTELENGTH: i32 = MAX_VERIFY_BLOCKS << 10 // 32,768 +const PAGE_BYTELENGTH: i32 = 1 << 16 +export const MAX_VERIFY_BLOCKS_BYTELENGTH: i32 = MAX_VERIFY_BLOCKS * SIGNEDBLOCK_BYTELENGTH +export const MAX_MESSAGE_BYTELENGTH: i32 = + MAX_VERIFY_BLOCKS_BYTELENGTH > PAGE_BYTELENGTH ? MAX_VERIFY_BLOCKS_BYTELENGTH : PAGE_BYTELENGTH ``` -This is right only because both happen to be 32. A batch needs -`MAX_VERIFY_BLOCKS * SIGNEDBLOCK_BYTELENGTH` = 3,072 B, not 32 KiB. Change the -cap and the **maximum signable message length changes with it**: at 16 it halves -to 16 KiB (a silent API regression), at 64 it doubles and pushes the module from -2 pages to 4. If you touch `MAX_VERIFY_BLOCKS`, check what happened to -`MAX_MESSAGE_BYTELENGTH` and to `memory.size()`. +Raising the *contract* rather than sizing the buffer is what makes this safe: +the host already validates message length against `MAX_MESSAGE_BYTELENGTH`, so +that one guard now covers batch packing too. **If you change `MAX_VERIFY_BLOCKS`, +check `memory.size()`** — the message limit follows it once a batch exceeds a +page, and pages are the expensive unit here. -Exported byte-length globals (`KEY_BYTELENGTH`, `MESSAGE_BUFFER_BYTELENGTH`, …) -are the single source of truth for these sizes — read them rather than -hardcoding, on both sides of the boundary. `test/index.html` currently -hardcodes the cap as `32` in two places and is the one file known to violate -this — it tracked the last cap change by hand. +These sizes are a **public API**: `nano25519.constants` carries +`BLOCKHASH_BYTELENGTH`, `KEY_BYTELENGTH`, `SIGNATURE_BYTELENGTH`, +`MAX_VERIFY_BLOCKS`, `MAX_VERIFY_BLOCKS_BYTELENGTH` and +`MAX_MESSAGE_BYTELENGTH`. Read them rather than hardcoding, on both sides of the +boundary and in consumers. Buffer **pointers** are grouped as `pointers` inside +`src/lib` and deliberately not exported from the package — the buffer-scrub +guarantee is a property of the module, not a promise about callers. + +`test/index.html` still hardcodes the cap as `32` in two places and is the one +file known to violate this; `constants.MAX_VERIFY_BLOCKS` is now available to +it. ## Build constraints that break normal assumptions @@ -169,11 +175,12 @@ ordinary AssemblyScript: arithmetic over other constants is fine and still const-folds. - **`enable: ["simd"]`** — field elements are 12 `i32` limbs, not 10. The two trailing limbs are padding; respect the stride. -- **`initialMemory: 2`** — pins the module at 2 pages (131,072 B); heap at rest - is 103,456 B. Without the setting the stub allocator doubles on growth - (1→2→4→8), so page counts are always powers of two. Re-derive this whenever a - buffer is resized rather than leaving it stale; it has been wrong in both - directions already. +- **`initialMemory: 4`** — pins the module at 4 pages (262,144 B); heap at rest + is 135,824 B. Only the stub allocator's *growth* doubles (1→2→4→8); the + setting itself takes any integer, so it need not be a power of two — **3 pages + currently fit**, with 60,784 B spare. Re-derive this whenever a buffer is + resized rather than leaving it stale or rounding it up; it has been wrong in + both directions twice. Memory growth *during* a call would detach every `Uint8Array` view the host holds, so keeping call paths allocation-free is a correctness requirement, not @@ -285,10 +292,22 @@ show up on the clock; only the point arithmetic does. ## Testing -`node ./test/node.mjs` after a build. Current state is **6178 passing, 0 +`node ./test/node.mjs` after a build. Current state is **6177 passing, 0 failing**. There is no longer an expected failure — if anything fails, it is a regression. +`equalbytes` additionally self-tests in the module's **start function** (top-level +code in `utils.ts`), flipping all 32 byte positions. A regression there makes the +module fail to instantiate, so the package becomes unimportable rather than +silently weakening signature verification. Keep that pattern for anything whose +failure mode is "quietly accepts too much". + +⚠️ **An abort during instantiation loses its message.** The `env.abort` handler +reads `exports.memory`, but `exports` is still in its temporal dead zone while +the start function runs, so a fired self-test reports `Cannot access 'memory' +before initialization` rather than its own text. If the module refuses to load +with that error, suspect an init-time assertion, not a binding problem. + `verify_blocks` now has vector coverage — a single block, a full 32-block batch, both range errors, and the three block-shape `TypeError`s. Coverage still runs thin in the middle: its bugs have twice been invisible below a boundary -- 2.52.0