Repository navigation
toyos-crypto stage 4, the test side: vectors, integer oracle, differential, fuzz, controls, Miri and the constant-time harness - #993
Conversation
Review r1 of #993 at ec5a0cdArchitectureThis PR is test-side only. No production line of The one harness change is The vector, integer-oracle, differential and fuzz layers are well built:
NOTICE checks out. I recomputed the sha256 of all 56 vector and CAVP files, and every one matches NOTICE. The byte sizes match too. I spot-checked three files against upstream at the pinned commits: The problems are in the constant-time harness and its registration, in a few accounting and cost claims, and in the fiat record. Net: about 2,900 lines of test Rust, 312 lines of licence rows, about 24 MB of vectors. No production growth. FindingsBLOCKER 1. The metal rows contradict the recorded design and enter every full metal pass.
Two fixes are acceptable:
BLOCKER 2. The harness confounds class with input preparation next to the clock.
Fix:
Without the null control, a LEAKS verdict on a bench cannot be told apart from a harness artefact. The leaky control only shows that the clock works. BLOCKER 3. The metal bounds are not derived for the benches.
BLOCKER 4. The NIST "declared count" check is a tautology, and the PR claims more than it does.
BLOCKER 5. Four committed vector files test nothing, or nothing a cheaper test lacks.
BLOCKER 6. The arithmetic-only differential and fuzz run three times over identical code.
BLOCKER 7. The fiat-crypto record is inaccurate. Issue
BLOCKER 8. The Miri status will be stale when it lands. Issue
SUGGESTION A. Size the leaky control to the leaks the rows exist to catch.
SUGGESTION B. Cite AMD as well. SUGGESTION C. CAVP GCMVS decrypt costs much for little.
SUGGESTION D. Small readability fixes.
Answers to the questions asked
SEND BACK |
… apart Review r1 of #993: - The 8192-bit RSA files' every case was the one size refusal `fuzz::rsa_components` already holds, and the semi-expanded ML-KEM file made no call at all: both are other interfaces, as the module says of the files never taken. GCMVS's decrypt files gave 750 judged cases in 15,750, refusals Wycheproof and the differential already hold; the encrypt files stay. - Each CAVP file's records are held to a count taken from its text apart from the parse (`Count = `, `Len = `, `Msg = `, `Qx = ` and the Monte Carlo `MD = ` lines), and each ACVP file's cases to its `"tcId"`s, so a parser that merged two records reds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r1 of #993: `portable` changes only the instruction paths, so the curves, scalar25519, RSA, ML-KEM, X25519, Ed25519 and WPA2's HMAC-SHA1 are the same code in every form, and their differential, fuzz and vector tests ran three times for one result. They now run in the `portable` form and are ignored, by name and reason, in the other two. AES-GCM, GHASH, ChaCha20, Poly1305 and SHA-2 stay in every form. Under Miri the heavy unit tests take a spread of their inputs, as `poly1305` and `test_arithmetic` already did: GCM's lengths and GHASH's bits, one ML-KEM key generation, and fewer arithmetic edge pairs. The readability points of the review are taken too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ized counter Review r1 of #993, and the first bench boots: every routine read LEAKS on both benches, the leaky control at |t| ~7,000, which fits a confound of the harness rather than of the crate. - Inputs are made a block of 1,000 at a time before any of them is timed, and each maker draws the same random bytes for either class, so what lies between two counter reads is the routine alone. The class draw is still per measurement. - A null control, both classes one distribution, runs at the start and the end of each program and must stay below |t| 4.5; the leaky control, now a 16-byte early-exit tag comparison after a ChaCha20-Poly1305 seal, must exceed 10 at both. A run whose controls fail exits nonzero, its verdicts all printed. - The counter is read behind `CPUID` at both ends, which serializes on Intel and AMD alike; `LFENCE` orders execution on AMD only under DE_CFG bit 1, which this kernel does not set, and QEMU's default CPU has no `RDTSCP`. - The two metal rows are named-only: a pass takes them by their whole name or their boot, never unfiltered and never by a part of the name, as stage 4 keeps the result out of the suite (`metal::select`). - The bounds come from the benches: the fast job took 968 s on the T14 and 823 s on the AMD bench; the curves job is derived from the T14's X25519 time. Under TCG at 20,000 measurements both programs end cleanly, the leaky control at |t| 10 to 22 and the null control below 2.1, and no routine reads LEAKS; TCG's timings judge nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The record now states what holds and what it cost: 43 Wycheproof files and NIST's held to independent counts, the arithmetic-only tests in one form with main's cost beside this one's, the complete Miri run, the timing rows named-only, and the first bench boots read as the harness confound they were. The fiat-crypto record names every lock entry and how it is reached: the dalek 4.1.3 and 5.0.0-pre.6 entries sit behind a `cfg` nothing sets, but russh's `p384` 0.14.0-rc.8 compiles fiat code into sshserver today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Response to review r1. The head is 7a36687; the code is at 9022dcc. Evidence: https://github.com/ToyOSOrg/evidence/tree/187abe7793116cbbbfb4dec63098032b1b8d61de/pr-993/r2
Suggestions:
Controls: all 15 are red at 9022dcc against their exact tests. |
Review r2 of #993: code at 9022dcc, head 7a36687This round checks the r1 required items and reviews only the code changed since r1. I read the full diff ec5a0cd..7a36687 and the r2 evidence ( r1 required items
Host-test costThe added cost is acceptable for routine host CI, and I would not trim further in this PR.
New findingsN1. BLOCKER: the named-only list can go silently inert.
N2. REQUIRED BEFORE LANDING: the curves bound must be measured. Suggestion (optional). The null control draws the same 16 random bytes for both classes, so it never exercises the fixed-class data path through SEND BACK, for N1 alone. N2 is a value from the run already in progress. Once N1 is fixed and N2 is recorded, I expect LAND without another full round. |
…ecorded Review r1 of #1005. `--ci host` compiled sshserver, and so fiat-crypto, for the host test and checked it for Linux and Windows: a build for a triple with no `[target]` table took no flags from the config. `[build] rustflags` is what cargo applies to exactly those builds, and the ToyOS tables, whose flags replace it, carry the cfg as well. The SSH track records the compromise: until stage 2, sshserver's P-384 (client `ecdsa-sha2-nistp384` verification before authentication, and an operator's P-384 host key) runs on the backend upstream calls experimental. The config comment no longer cites the crypto issue, whose record of the ruling lands with #993. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
7a36687 to
0ce19e2
Compare
… apart Review r1 of #993: - The 8192-bit RSA files' every case was the one size refusal `fuzz::rsa_components` already holds, and the semi-expanded ML-KEM file made no call at all: both are other interfaces, as the module says of the files never taken. GCMVS's decrypt files gave 750 judged cases in 15,750, refusals Wycheproof and the differential already hold; the encrypt files stay. - Each CAVP file's records are held to a count taken from its text apart from the parse (`Count = `, `Len = `, `Msg = `, `Qx = ` and the Monte Carlo `MD = ` lines), and each ACVP file's cases to its `"tcId"`s, so a parser that merged two records reds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r1 of #993: `portable` changes only the instruction paths, so the curves, scalar25519, RSA, ML-KEM, X25519, Ed25519 and WPA2's HMAC-SHA1 are the same code in every form, and their differential, fuzz and vector tests ran three times for one result. They now run in the `portable` form and are ignored, by name and reason, in the other two. AES-GCM, GHASH, ChaCha20, Poly1305 and SHA-2 stay in every form. Under Miri the heavy unit tests take a spread of their inputs, as `poly1305` and `test_arithmetic` already did: GCM's lengths and GHASH's bits, one ML-KEM key generation, and fewer arithmetic edge pairs. The readability points of the review are taken too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ized counter Review r1 of #993, and the first bench boots: every routine read LEAKS on both benches, the leaky control at |t| ~7,000, which fits a confound of the harness rather than of the crate. - Inputs are made a block of 1,000 at a time before any of them is timed, and each maker draws the same random bytes for either class, so what lies between two counter reads is the routine alone. The class draw is still per measurement. - A null control, both classes one distribution, runs at the start and the end of each program and must stay below |t| 4.5; the leaky control, now a 16-byte early-exit tag comparison after a ChaCha20-Poly1305 seal, must exceed 10 at both. A run whose controls fail exits nonzero, its verdicts all printed. - The counter is read behind `CPUID` at both ends, which serializes on Intel and AMD alike; `LFENCE` orders execution on AMD only under DE_CFG bit 1, which this kernel does not set, and QEMU's default CPU has no `RDTSCP`. - The two metal rows are named-only: a pass takes them by their whole name or their boot, never unfiltered and never by a part of the name, as stage 4 keeps the result out of the suite (`metal::select`). - The bounds come from the benches: the fast job took 968 s on the T14 and 823 s on the AMD bench; the curves job is derived from the T14's X25519 time. Under TCG at 20,000 measurements both programs end cleanly, the leaky control at |t| 10 to 22 and the null control below 2.1, and no routine reads LEAKS; TCG's timings judge nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The record now states what holds and what it cost: 43 Wycheproof files and NIST's held to independent counts, the arithmetic-only tests in one form with main's cost beside this one's, the complete Miri run, the timing rows named-only, and the first bench boots read as the harness confound they were. The fiat-crypto record names every lock entry and how it is reached: the dalek 4.1.3 and 5.0.0-pre.6 entries sit behind a `cfg` nothing sets, but russh's `p384` 0.14.0-rc.8 compiles fiat code into sshserver today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ured Review r2 of #993, N1: `METAL_NAMED_ONLY` was a list of names apart from the rows, so a renamed or mistyped row would quietly become an ordinary one and every unfiltered pass would take its hours-long boot. The arm now says it (`Arm::named_only`): an arm already carries what its jobs cost the boot (`adds_ms`), and every arm is built from `once`, so no other row changes. A row is named-only when one of its arms is. N2: the bounds are the slower bench's measured job time plus half, from the boots at 7a36687: the fast job 970 s (T14) and 822 s (AMD), the curves job 7,036 s (T14) and 6,234 s (AMD). The issue records those boots' verdicts: everything passes on AMD; on the T14 both P-256 routines leak, at #993's base before #990. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ctors Stage 4's independent vectors. Wycheproof's `testvectors_v1/` at the commit stage 1 pinned (7a325932) gains every file for an interface the crate offers: GMAC, AES key wrap, PBKDF2- and HMAC-SHA1 (WPA2), ECDH over SEC 1 points, ECDSA with each SHA-2 the provider pairs with a curve, RSA PKCS #1 at 2048, 3072, 4096 and 8192 bits, PSS at each salt and MGF1 hash the files hold, and ML-KEM-768 by seed. The ASN.1, PEM, JWK and P1363 files, other algorithms, curves and hashes, and RSA signature generation are other interfaces and stay out. Every case is judged against the file's result or counted outside the interface by a named reason, and the count is held against the file's `numberOfTests`. Outside means refused by an error where the interface can be handed the input (a 15-byte tag, an 8192-bit modulus, a PSS signature valid under another salt), or by a type that cannot carry it (a 64-bit IV, AES-192). A PSS signature invalid under the file's salt but made with TLS's ("s_len changed to 32") is counted, not judged. NIST's: CAVP's SHAVS files for SHA-384 beside SHA-256's and SHA-512's in `tests/cavp`, now read by this crate too; GCMVS's AES-128 and AES-256 files; FIPS 186-4's ECDSA SigVer and PKV; and ACVP's HMAC-SHA-1/256/384/512 at the ML-KEM files' commit. ML-KEM's ACVP test moves to `tests/nist.rs`. Each file is byte for byte, pinned by sha256 in NOTICE and a COMMITTED_FILES row, which the `tests/cavp` files now all have. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…crypto The owner rejected fiat-crypto (2026-10-21): ToyOS keeps its own crypto stack, and no fiat code is a dependency of any kind. Poly1305's oracle, its one use, becomes `num-bigint`, already the crate's test-only integer oracle, and the dev-dependency goes. `test_arithmetic` becomes a harness over one trait, `Field`, whose implementations alone name the crate's types: field25519 and the four NIST Montgomery moduli today, and a new representation of the same field by implementing it. Each is held against integers on: - every pair of boundary values: 0, 1, 2, p - 1, p - 2, p's halves, and each 51- and 64-bit limb's 2^k - 1, 2^k, p - 2^k and p - 2^k + 1, with inputs from p up where the decoder takes them; - seeded values against seeded partners; - inversion as a^(p-2), zero included; - a chain that feeds every result back as the operation left it, so an output's limb bounds are an input's; - distributivity and (x - y) + y = x. The Ed25519 group order is held the same way through `S`: reduction of any width, S < L, and the signing operations. ML-KEM's reduction modulo 3329 is checked over every pair of coefficients, and Compress and Decompress over every input at the widths ML-KEM-768 uses, against FIPS 203 §4.2.1's rounding. Iteration counts drop under Miri, which reads the same tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 4's differential, against ring and RustCrypto as dev-dependencies only. Each primitive takes 10,000 cases, each drawn from a generator seeded by its number, so a red names a case that reproduces whichever thread took it; the cases spread over every CPU. - SHA-2, HMAC and HKDF against ring, whole and streamed in random pieces, a changed tag refused. - AES-128/256-GCM and ChaCha20-Poly1305 against ring and RustCrypto, and one change to ciphertext, tag or AAD that both refuse, leaving the ciphertext as it was. - OpenSSH's construction against RustCrypto's ChaCha20 and Poly1305. - X25519 against dalek on random u-coordinates, values from p up and the high bit included. - Ed25519 against ring and dalek's `verify_strict`: ring's signatures verified, a bit flipped, S raised by the order, the message changed; signing against ring's; and random encodings decoded where dalek's are. - P-256 and P-384 key derivation and ECDH against RustCrypto, and verification of its signatures, changed or the other S, against ring. - RSA PKCS #1 v1.5 and PSS, 2048 and 4096 bits, against ring's verifier, over a pool of ring's signatures changed six ways. - ML-KEM-768 against RustCrypto, implicit rejection included. - WPA2's HMAC-SHA1 and 802.11 PRF against ring; the PMK, 8,192 HMAC computations each, at 200 cases. The small differential tests these supersede go; the length sweeps stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 4's fuzz, 4,000 cases per decoder as `build/ssh/tests/fuzz.rs` does: a valid encoding bent along its grammar, or random bytes, and an independent decoder's verdict. - SEC 1 points: the other y, coordinates at, past and below p, x + p, compressed and other tags; RustCrypto's decoder, uncompressed only. - DER signatures: tags, lengths, long-form lengths, integer padding and sign, r or s at 0, n - 1, n and n + 1, the other S, trailing bytes; ring's verifier. - RSA components: held to the interface's contract, and an accepted key verifies only where it is the signer's. - Ed25519 encodings near 0 and p, each sign: dalek's decoder, canonical and not weak. - ML-KEM-768 keys with coefficients past q, and ciphertexts changed or cut: RustCrypto's modulus check and decapsulation. - The rustls provider's key shares for every group, against the shares' own decoders and the zero X25519 result; its `RSAPublicKey` DER against ring's; and its records cut, changed or opened at another sequence number. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What Miri reads of the crate's `unsafe`: `erase` over sizes around its 16-byte stores, alignments below theirs, nested arrays and none at all; `wipe`, `wipe64` and `wipe32` over slices; a dropped `Secret`'s storage; and `ct`'s masks, selection, `opaque` and `equal` over their ranges and a difference at every byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 4's negative controls mutate each check a forgery would pass. Two mutations left every test green, so two tests that would have seen them: - SEC 1 decoding refuses a coordinate at or past p. A residue can be encoded above p only where it is small, below 2^256 - p, which a random point never is. The fuzz now takes the point with the least x, on P-256 and P-384, and encodes x or x + p. - RSA verification refuses a signature that is not the modulus's length (RFC 8017 §8.2.2 step 1). A signature whose first byte is zero, without it or with another zero before it, is the same integer: refused, as ring refuses it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…crets Stage 4's constant-time harness, a guest program for the benches: for each secret routine a consumer calls, a million measurements with the class drawn per measurement, one class holding a fixed special secret and the other a fresh uniform one, the public inputs the same in both. Welch's t over the raw set and dudect's 100 percentile crops of a calibration run; a routine passes below |t| = 4.5. Each input is made before the clock starts. A deliberately leaky control runs first in each program: a byte-at-a-time comparison that stops at the first difference. It must exceed |t| = 10, or the program fails, since that run could see no leak. Every other verdict is a printed line, evidence for the track issue rather than a suite verdict. Two programs, in two boots: `crypto_constant_time` (the AEADs' and OpenSSH's sealing and refusal, HMAC, HKDF, X25519, ML-KEM-768 encapsulation and decapsulation of valid against rejected ciphertexts) and `crypto_constant_time_curves` (P-256 and P-384 key derivation and agreement, Ed25519 signing), which takes a bench for hours. ML-KEM key generation is not timed: its matrix expansion depends on rho, a public output of the secret seed, which no two classes can share. A metal arm now says what its jobs add to its list's bound (`Arm::adds_ms`), as a shared boot's members do; each constant-time arm derives its bound from a smoke run's CPU time on the development Mac, scaled to the measurement count, four times over. The shared boot skips both programs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An AEAD case is one record rather than ten arguments, a committed RSA key a struct, and its DER exists only where the provider reads it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A QEMU smoke run of both programs, at 2,000 measurements, timed with `Instant` and its clock-page arithmetic, saw nothing of the leaky control: |t| 0.01 and 4.36, so both runs failed as they should. Each measurement now reads the time-stamp counter between `LFENCE`s (`arch/counter.rs`, with AArch64's virtual counter beside it), and at 20,000 measurements under TCG the control reads |t| 59 and 66 and both programs end cleanly. TCG's timings judge nothing. The metal rows are `crypto_secret_timing` and `crypto_secret_timing_curves`: a row and a shared member under one name are two verdicts under one name, which the suite refuses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The track's stage 4 replaces its fiat-crypto oracle with the integer one the owner chose (2026-10-21) and records what the test-side items hold: the vectors, the oracle, the differential, the fuzz, the negative controls and Miri, with their evidence; the constant-time harness built and waiting on the benches; ML-KEM key generation not timed, and why; and what stays open. `tests/cavp` gains its second reader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… apart Review r1 of #993: - The 8192-bit RSA files' every case was the one size refusal `fuzz::rsa_components` already holds, and the semi-expanded ML-KEM file made no call at all: both are other interfaces, as the module says of the files never taken. GCMVS's decrypt files gave 750 judged cases in 15,750, refusals Wycheproof and the differential already hold; the encrypt files stay. - Each CAVP file's records are held to a count taken from its text apart from the parse (`Count = `, `Len = `, `Msg = `, `Qx = ` and the Monte Carlo `MD = ` lines), and each ACVP file's cases to its `"tcId"`s, so a parser that merged two records reds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r1 of #993: `portable` changes only the instruction paths, so the curves, scalar25519, RSA, ML-KEM, X25519, Ed25519 and WPA2's HMAC-SHA1 are the same code in every form, and their differential, fuzz and vector tests ran three times for one result. They now run in the `portable` form and are ignored, by name and reason, in the other two. AES-GCM, GHASH, ChaCha20, Poly1305 and SHA-2 stay in every form. Under Miri the heavy unit tests take a spread of their inputs, as `poly1305` and `test_arithmetic` already did: GCM's lengths and GHASH's bits, one ML-KEM key generation, and fewer arithmetic edge pairs. The readability points of the review are taken too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ized counter Review r1 of #993, and the first bench boots: every routine read LEAKS on both benches, the leaky control at |t| ~7,000, which fits a confound of the harness rather than of the crate. - Inputs are made a block of 1,000 at a time before any of them is timed, and each maker draws the same random bytes for either class, so what lies between two counter reads is the routine alone. The class draw is still per measurement. - A null control, both classes one distribution, runs at the start and the end of each program and must stay below |t| 4.5; the leaky control, now a 16-byte early-exit tag comparison after a ChaCha20-Poly1305 seal, must exceed 10 at both. A run whose controls fail exits nonzero, its verdicts all printed. - The counter is read behind `CPUID` at both ends, which serializes on Intel and AMD alike; `LFENCE` orders execution on AMD only under DE_CFG bit 1, which this kernel does not set, and QEMU's default CPU has no `RDTSCP`. - The two metal rows are named-only: a pass takes them by their whole name or their boot, never unfiltered and never by a part of the name, as stage 4 keeps the result out of the suite (`metal::select`). - The bounds come from the benches: the fast job took 968 s on the T14 and 823 s on the AMD bench; the curves job is derived from the T14's X25519 time. Under TCG at 20,000 measurements both programs end cleanly, the leaky control at |t| 10 to 22 and the null control below 2.1, and no routine reads LEAKS; TCG's timings judge nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The record now states what holds and what it cost: 43 Wycheproof files and NIST's held to independent counts, the arithmetic-only tests in one form with main's cost beside this one's, the complete Miri run, the timing rows named-only, and the first bench boots read as the harness confound they were. The fiat-crypto record names every lock entry and how it is reached: the dalek 4.1.3 and 5.0.0-pre.6 entries sit behind a `cfg` nothing sets, but russh's `p384` 0.14.0-rc.8 compiles fiat code into sshserver today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ured Review r2 of #993, N1: `METAL_NAMED_ONLY` was a list of names apart from the rows, so a renamed or mistyped row would quietly become an ordinary one and every unfiltered pass would take its hours-long boot. The arm now says it (`Arm::named_only`): an arm already carries what its jobs cost the boot (`adds_ms`), and every arm is built from `once`, so no other row changes. A row is named-only when one of its arms is. N2: the bounds are the slower bench's measured job time plus half, from the boots at 7a36687: the fast job 970 s (T14) and 822 s (AMD), the curves job 7,036 s (T14) and 6,234 s (AMD). The issue records those boots' verdicts: everything passes on AMD; on the T14 both P-256 routines leak, at #993's base before #990. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…measure russh's `p384` is on its `bignum` backend under #1005, so the fiat record names the dependency as one no guest build selects. The T14's P-256 finding is re-measured on #1000's arithmetic, now on main, with a boot that sets DOITM beside the one that does not. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqpxbTfpvcbx1FJEy3Eny9
#1005 sets `p384_backend = "bignum"` for host and guest builds alike, so the record says that and what held before it, true in either landing order. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
0ce19e2 to
9217793
Compare
|
Response to review r2. The head is 9217793, rebased onto main d7f269d (#997, #999 and #1000 included).
The bench verdicts from 7a36687 are recorded in the issue: everything passes except P-256 on the T14 (old code). The re-measure on #1000's code, with a DOITM probe beside it, is staged. Miri is complete again on the new crypto code (44 passed). All 15 controls are red at d79698f. Evidence: https://github.com/ToyOSOrg/evidence/tree/a58ea8eb194c15f8274383cb620f7c426a0b726e/pr-993/r4 |
Review r3 of #993 at 9217793 (on main d7f269d)This is a focused round. I checked the r2 items, the rebase onto #997, #999 and #1000, the new issue record and the evidence ( r2 items
Rebase
Evidence
FindingsBLOCKER 1. The issue names an agent as an owner. BLOCKER 2. A failing verdict is called "marginal". Same file, line 425: "P-384 agreement reads 5.79, marginal". The pass bound is |t| < 4.5, so 5.79 is LEAKS by the harness's own rule, and the bench line says LEAKS. State it as a T14 finding next to P-256, and widen the exit to "a T14 pass of P-256 and P-384 agreement on the current NIST code". Suggestion. Both blockers are wording edits to the issue. They invalidate no evidence and need no rerun. A commit that makes only those edits can land without another review round. SEND BACK |
…s a durable owner Review r3 of #993. |t| 5.79 against the 4.5 bound is LEAKS by the harness's own rule, so P-384 agreement joins P-256 in the T14 finding, and the exit asks a T14 pass of both. The owner is this track's parity stage, the NIST arithmetic, not a coordination label. The drop test also holds `Workspace`, which carries the same erase on every return path: without its `erase` it reads all ones. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
|
Response to review r3, head e37f115.
|
What this is
Stage 4 ("Secure") of
issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md, the test-side items only. The arithmetic modules are untouched: typed domains and the arithmetic rewrite belong to the speed effort. These tests check whatever arithmetic sits underneath, so they guard that rewrite.What changes
Independent vectors. 43 Wycheproof files (
testvectors_v1at 7a325932, the stage 1 pin). That is every file for an interface the crate offers:The NIST vectors are CAVP SHAVS (SHA-384 added to
tests/cavp, which this crate now also reads), GCMVS AES-128/256 encryption, ECDSA SigVer and PKV, and ACVP HMAC beside the existing ML-KEM files. Each case is either judged against the file's verdict or counted outside the interface under a named reason. Wycheproof's totals are held tonumberOfTests; each NIST file's to a count taken from its text apart from the parse (Count =,Len =,Msg =,Qx =,"tcId"). Every file is byte for byte, pinned by sha256 inNOTICEwith aCOMMITTED_FILESrow; the oldtests/cavpfiles now have rows too.No fiat-crypto (owner, 2026-10-21). The dev-dependency is gone, and Poly1305's oracle is now
num-bigint.test_arithmeticis a harness over oneFieldtrait. field25519 and the four NIST Montgomery moduli are checked against integers on:scalar25519is checked the same way: reduction, S < L, add and mul. A new representation is checked by implementing the trait. ML-KEM's mod-q reduction is checked exhaustively, and Compress/Decompress over every input.Differential. A primitive with no instruction path (curves, X25519, Ed25519, RSA, ML-KEM, WPA2's HMAC-SHA1) is the same code in every form, so its differential, fuzz and vector tests run in the
portableform and are ignored by name in the other two. 10,000 seeded cases per primitive against ring and RustCrypto (norsa), spread over all CPUs. Each case is seeded from its own number, so a failure names a case that reproduces. The PMK alone runs 200 cases, because each one is 8,192 HMAC computations.Fuzz. 4,000 structured cases per decoder: SEC 1, DER, RSA components, Ed25519 encodings, ML-KEM keys and ciphertexts, and the rustls provider's key shares,
RSAPublicKeyDER and records. Each verdict comes from an independent decoder.Negative controls. 15 checked patches (tag compares ×5 including GCM's one-batch path, on-curve, coordinate range, scalar range, ML-KEM coefficient range, S < L, canonical Ed25519 y, and four length checks). At the measured head, each makes its named test fail. Two first survived the whole suite: SEC 1 coordinate range and RSA signature length. 8c785b4 adds the tests that catch them.
Miri. Every
portableunit test but the provider's runs under Miri, with new unit tests forerase, the wipes, a droppedSecretandct. The heavy tests take a spread of their inputs undercfg(miri). A control is included:eraseone byte past its array is reported as UB.Constant-time harness. Guest programs
crypto_constant_timeandcrypto_constant_time_curves(tests/guest/rust-tests/src/constant_time.rs):CPUIDat both ends (serializing on Intel and AMD;LFENCEorders on AMD only under DE_CFG bit 1, and QEMU's default CPU has noRDTSCP);Metal rows
crypto_secret_timingandcrypto_secret_timing_curves, the T14 and the AMD bench, each in its own boot. They are named-only (Arm::named_only, read bymetal::select): a pass takes them by their whole name or their boot, never unfiltered and never by part of a name, as stage 4 keeps the result out of the suite. Their judge only requires that the job ran with its controls held. Each bound is the slower bench's measured job time plus half: the fast job took 970 s (T14) and 822 s (AMD), the curves job 7,036 s (T14) and 6,234 s (AMD).Checks
The head is 9217793, on main d7f269d, which includes #997, #999 and #1000.
cargo run -- --ci hostat 9217793: exit 0, 93 steps green.lib/cryptoortests/guesthas changed since.named_onlyturnsmetal_words_take_rows_members_and_whole_bootsred.Evidence:
Costs
Measured in one session on the development Mac. The crate's three forms ran in 20 s wall and 15 s of CPU on main (5e01f8b); this branch takes 74 s wall and 396 s of CPU, almost all of it the
portableform's curve arithmetic, ours and RustCrypto's. That will shrink with the speed work. GitHub's runner is unmeasured, because CI skips drafts. The repository gains about 17 MB of vector files.Risks and open items
The T14 P-256 finding is open. It was measured on the old code and is re-measured on crypto: P-256 special reduction, Booth windows, joint ECDSA verify (parity item 8a) #1000's code by the staged boots.
ML-KEM key generation is not timed. Its matrix expansion depends on rho, which is public but derived from the secret seed, so the two classes cannot share it.
Not done here:
fiat-crypto is gone from
toyos-crypto.Cargo.lockstill names it:curve25519-dalekcfgs that nothing sets;p384, which sshserver: russh's p384 on its own bignum backend, host and guest, so no fiat-crypto compiles in #1005 moves to itsbignumbackend for every build.The issue's record holds in either landing order.
The GitHub runner's wall time for the host job is unmeasured, because CI skips drafts.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q