Repository navigation
crypto: a 5x51 field with typed bounds and erasure at the boundary (parity items 1 and 5) - #992
Conversation
Review of #992, round 1 (head 55005b1, top commit only)I read the commit, the full diff against f5fbc03, each changed file at the head, every caller of Architecture
Arithmetic, checked by hand
Erasure contract against the codeEvery error and early-return path drops its owners. I checked BLOCKER
SUGGESTION
Tests and evidence
Rosetta and ADX
Verdict: SEND BACK. Items 1–7 are required. Item 3 may instead go to the owner as an explicit contract relaxation. Once they are resolved, the erasure contract is in a reviewable state for the owner's landing condition. |
The 2^255 - 19 field multiplied each of its 25 products by a runtime 1 or
19 as u128 (three multiplications each), squared by multiplying, carried
in three full passes and erased every element and its u128 workspace with
volatile stores and a fence. Inversion was a square and a multiply per
exponent bit.
The field now has two element types for its limb bounds, as Erbsen et al.
describe ("Simple High-Level Code For Cryptographic Arithmetic", IEEE S&P
2019): `Tight`, what a multiplication or carry leaves, and `Loose`, the
sum or difference of two tight elements, which multiplication accepts and
addition refuses at compile time. Multiplication premultiplies four of b's
limbs by 19, squaring takes 15 products, `mul_small` is one row, and the
carry is one pass with the top carry folded by 19 and one short carry into
limb 1. Every carry and product uses wrapping operations whose bounds the
types establish, so the shipped profile adds no overflow check. Inversion
and the square root's power use the curve25519 addition chain (Bernstein
2006, section 5): 254 squarings and 11 multiplications.
X25519's ladder swaps with one XOR mask per coordinate pair instead of six
selects, and multiplies by 121665 with `mul_small`. Ed25519 doubles with
RFC 8032's dedicated doubling in its fixed-schedule multiplication and its
small-order check.
Erasure follows the owner's conditional approval of item 26: field
elements, NIST residues and points are `Copy` intermediates and are no
longer erased one by one. Secret owners still erase on drop, and a secret
operation's explicit state is a `secret::Workspace`, erased when the
operation returns on any path: the X25519 ladder's coordinates and the
Ed25519 and NIST scalar multiplications' accumulators. `secret`'s header
states the contract, including what it does not erase: registers, spills,
compiler copies, moved-from locations and finished frames. This lands only
with M1's measurement and the contract's review.
The integer-oracle tests cover squaring, mul_small, negation, inversion and
the square root's power, loose operands into every multiplication, and the
largest limbs each bound admits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Key cleanup is kept: each agreement erases its shared value before it becomes bytes. P-256/P-384's product point is a Workspace and its affine coordinates are wiped; X25519's x2 / z2 is a Workspace. - The Ed25519 and NIST scalar multiplications' accumulator Workspaces are gone: each held only the result the function returned. - secret.rs's contract states what is erased (owners and the hash, Keccak, SHA-1 and Poly1305 states; key-agreement results; workspaces, with what the ladder's actually covers), which types still erase per value (Ed25519 scalars, ML-KEM polynomials) and what is not erased. - Zeroable is an unsafe trait; each impl states why zero is valid. - square_times documents and debug-asserts k >= 1; the extreme-limb test is named for the reachable limbs it exercises. - The track's weakness record points to the contract and states the weakening and the owner's condition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
55005b1 to
41e89b9
Compare
Review of #992, round 2 (head 41e89b9)I read the round-1 commit 41e89b9 against 4c3c94a, every changed file at the head, I did not rebuild: this role runs no builds. The spot-check used the implementer's own I did not read the analysis notes or any ring or BoringSSL source. Round-1 items
M1M1 now holds on both benches. The coordinator relayed the AMD boot of probe fb93764 (judge exit 0): x25519 0.795, Ed25519 verify 0.794, sign 0.808, P-256 ECDH 0.882 ( FindingsBLOCKER A.
B.
SUGGESTION C.
D. Statement for the owner's condition
Verdict: SEND BACK (A, B; each a short wording change). |
- Each agreement writes its output into a Secret it owns: X25519's multiply writes into the caller's buffer, and NIST's affine writes into the caller's arrays, which agree holds in a Workspace. The returned coordinates' copies in agree's frame are gone. - The contract states the order (the values are erased once the output is written) and what remains: the copies of the output bytes that moving them into SharedSecret leaves in the finished frame, and the computation's arithmetic intermediates. - The workspace bullet keeps only what holds whatever the code generation: a workspace erases its own storage, not the copies the computation works on. - Zeroable's safety wording; the track record adds the AMD bench's M1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #992, round 3 (head d633558, base main)I read the round-2 commit d633558 against 41e89b9, Round-2 itemsA. Key-agreement bullet: fixed, and the code fix is correct.
B. Codegen sentence: fixed. The workspace bullet now says only what holds regardless of code generation. Suggestions C and D and the Statement for the owner's conditionThe erasure contract in
For the orchestrator
No findings. Verdict: LAND. |
Builds on #990 (merged). Its commits: round 0, 4c3c94a (rebased unchanged from 55005b1); round 1, 41e89b9; round 2, d633558.
Problem
The 2^255 - 19 field behind X25519 and Ed25519 cost about 91–100 ns per multiplication on the T14 against ring's 13–17 ns (spec section 1.2, derived from #932). It multiplied each of its 25 products by a runtime 1 or 19 as u128, squared by multiplying, carried in three full passes, inverted with a square and a multiply per exponent bit, and erased every element and workspace with volatile stores and a fence, which kept the arithmetic in memory.
What changes
Tightis what a multiplication or carry leaves (limbs 0, 2, 3, 4 below 2^51, limb 1 below 2^51 + 2^19);Looseis the sum or difference of two tight elements (every limb below 2^53). Multiplication takes either, addition and subtraction take onlyTight, so a missing carry does not compile. Multiplication premultiplies b's four wrapping limbs by 19, squaring takes 15 products,mul_smallis one row, the carry is one pass with the top carry folded by 19 plus one short carry. Every product and carry uses wrapping operations whose bounds the types establish, so the shipped profile's overflow checks add nothing. Inversion and the square root's power use the curve25519 addition chain: 254 squarings and 11 multiplications.mul_small.Copyintermediates. Secret owners still erase on drop. Each key agreement erases its shared value before it becomes bytes: X25519's x2 / z2, and P-256/P-384's product point and affine coordinates (one erase per agreement, restored as the orchestrator decided under the owner's rule that key cleanup is kept). The X25519 ladder's (x2, z2, x3, z3) is asecret::Workspace.The erasure contract
The contract lives in
lib/crypto/src/secret.rs's header, andlib.rspoints to it:Secret, and every type that owns a key, seed, expanded key, key schedule, scalar or shared secret, erases its own storage when it drops. So do the hash, HMAC, Keccak, SHA-1 and Poly1305 states. This is unchanged.Secretit owns, then erases the values they came from: X25519's x2 / z2, and P-256/P-384's product point and affine coordinates, each held in aWorkspace. The outputSecretis moved into the returnedSharedSecret, an owner. Not erased: the copies of the output bytes that this move leaves in the agreement's finished frame, and the computation's arithmetic intermediates.Workspace, dropped when the operation returns, on every path out of it, early returns and?included. The X25519 ladder's (x2, z2, x3, z3) is one. A workspace erases its own storage; the copies the computation works on, in registers and other slots, are not erased.scalar25519::S, until its reduction is rewritten) and ML-KEM polynomials (mlkem::Poly) still erase every temporary on drop.Copyvalues. Also not erased: registers; spill slots and every other copy the compiler makes, by-value moves and returns included; a moved value's earlier location; finished stack frames; the caller's buffers and public outputs. These remain until something overwrites them. The contract does not claim that any stack frame is wiped.panic = "abort", nothing runs after the panic.Zeroable, whicheraserelies on to write zero bytes over a value, is now anunsafe trait. Each impl states why zero is a valid value.What the generated code shows. This inspection replaces round 0's claim, which was wrong.
[profile.toyos]with the ToyOS toolchain (rustc 1.99.0-dev), I emitted the crate forx86_64-unknown-toyosandaarch64-unknown-toyos. A script walked each secret path's call graph and listed every conditional branch. The paths are P-256 and P-384agreeandpublic_key, Ed25519signandfrom_seed, and X25519agreeandpublic_key.affine's infinity check, and X25519's disclosed all-zero refusal). No branch in the point operations, the residue arithmetic or the 2^255 - 19 field operations depends on anything but a loop counter. Masks come from the register barrier.[profile.toyos], both targets. NIST'saffinenow writes into the caller's coordinateWorkspace, and X25519'smultiplyinto the caller'sSecret, so the returned-coordinate copies round 1 left are gone. On the success path, what remains is the copies of the output bytes that the move intoSharedSecretleaves:Secretlocal at −160..−129(%rbp), copied into the return slot. The product (−256..−161), the coordinates (−128..−65), theto_bestaging word (−48) and the consumed scalar are zeroed, each group followed by the fence.Secretdrops.affine's returned x[1..3] and y in agree's frame. That is now removed.Checks
Round 2, at d633558. Host CI ran on #997's head cd7960d, which contains this commit unchanged (result in #997).
cargo clippyandcargo test -p toyos-crypto --releasein the three CI forms: exit 0 each. The shipped agreements' code extracts: https://github.com/ToyOSOrg/evidence/tree/f3a54df6253a32c4ff3e3236ebe81b7a01664e1e/pr-997/r2/agree-codeRound 1, at 41e89b9. Logs: https://github.com/ToyOSOrg/evidence/tree/7c1d744ca3ee34eeda72bc1d301ba3451783227b/pr-992/r2
cargo run -- --ci host: exit 0, 87 steps green.cargo clippy -p toyos-crypto --all-targetswith the adopted lints and-D warnings, andcargo test -p toyos-crypto --release, in the three CI forms: exit 0 each.x86_64-unknown-toyosandaarch64-unknown-toyosin[profile.toyos]: see "What the generated code shows" above, andinspection/in the logs. Correction (review round 2): thesubq; cmovb; testq; jebranches inTight::bytesandto_bearecopy_nonoverlapping's debug overlap check on two stack addresses, which is on in[profile.toyos], not length checks. The addresses are public, so the conclusion stands.rosetta-adx-sigill.log. CPUID through Rosetta reports BMI2 and ADX.mulxruns, whileadcxandadoxexit 132 (SIGILL). No code here dispatches on ADX.Zeroable's safety and one debug assertion; none of that can change which test a field or ladder mutation reds.Round 0, at 55005b1 unless stated. Logs: https://github.com/ToyOSOrg/evidence/tree/e52b6c4658ed56e2719da2f8defd24d396d370d6/pr-992/r1
cargo run -- --ci host: exit 0, 87 steps green.cargo clippy -p toyos-crypto --all-targetswith the adopted lints and-D warnings, andcargo test -p toyos-crypto --release, in the three CI forms: exit 0 each.cargo test --test toyos-build -- crypto_instrument_prints_its_table(QEMU, x86_64) on the probe head fb93764, which carries this PR's crate unchanged: exit 0, 88 rows in each build.field25519_carries_and_noncanonical_inputs_match_integer_oracle(rewritten for the typed field: squaring,mul_small, negation, inversion, the (p-5)/8 power, loose operands into every multiplication) andfield25519_limb_bounds_hold_at_their_extremes(the largest limbs each type admits through every operation, and the tight bound of a product).--features sign --release --no-fail-fast, and reverted in the same run:SecretKey::from_bytesclamps bit 0 to zero, so that swap's mask is always zero. It stays as RFC 7748's ladder states it.Measurements
M1 on the T14 (probe fb93764 = round 0 of this PR plus additive rows; judge exit 0). Boundary over per-temporary erasure, same arithmetic:
M1 on the AMD bench (the same probe, judge exit 0): x25519/ecdh 0.795, ed25519/verify 0.794, ed25519/sign 0.808, p-256/ecdh 0.882.
Rounds 1 and 2 add one erase per agreement and remove two accumulator workspaces; I expect no visible change against rows of 83 to 517 us, but that is an inference, not a measurement.
This PR's parity rows on the T14 (same boot): x25519/ecdh 83.2 us against ring's 42.2 (1.97x; The ring comparator: toyos-crypto beside ring in a guest, on metal rows (crypto stage 2) #932 measured 13.3x). ed25519/sign 95.0 us against 12.9 (7.34x; was 71.1x). ed25519/verify 179.7 us against 53.1 (3.38x; was 34.5x). The NIST and RSA rows beneath it come from crypto: RSA R^2 by squarings, NIST curves as compile-time types, Ed25519 constants, a register barrier (parity items 1-4) #990: p-256/ecdh 8.56x (was 21.2x), ecdsa-p-256 verify 9.33x, p-384/ecdh 2.49x, rsa-2048-pkcs1 2.15x (was 38.1x). None passes yet.
Exploratory runs on the development Mac are superseded by the T14 numbers.
Risks
Workspaceand the owners being drop guards. Underpanic = "abort"nothing is erased after a panic.scalar25519::S) are owners and keep erasing on drop; crypto: Ed25519 verify by one joint multiplication, scalars by Barrett (parity items 6, E3, E4) #997 rewrites their reduction and settles that wording.🤖 Generated with Claude Code