Repository navigation
The approval app's crypto in .dag: bitwise, bignat, SHA-256, P-256 (blocked on MachineWidth reflection) - #11645
Conversation
…pp Attest, ECDSA P-256 The protocol between the roadmap server and the operator's phone for redeeming an approval with a biometric-gated device signature, modeled before any server route or Swift. iOS is the V1 realization; Android is modeled at every platform point (FCM, Keystore key attestation) and realized by nothing yet. - extdeps.crypto.signature: ECDSA P-256/SHA-256 interface (FIPS 186-5), one wire encoding per key and signature, sole_constructor evidence naming its message. - extdeps.apple.apns / secure_enclave / app_attest, extdeps.google.fcm, extdeps.android.key_attestation: cited upstream shapes. - gunbc.auth.approval_device_redemption: push is an opaque wake-up only; the app fetches the stored request, signs over a server challenge, the stored request text, the verb and the capability; server owns decided_at and derives the login; signature REQUIRED on the device route; RedemptionIdentityEvidence names the legacy arms by mechanism. - Server routes, verification primitives, ApprovalTarget, cutover and Android are declared frontiers; the existing /approve route and store are untouched while the Mt. Collins first boot runs on them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…icts, route paths Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…Ns provider token Two v1 host primitives over RustCrypto p256 0.13 (digest 0.10, the family sha2 0.10 and hmac 0.12 already use), registered exactly as hmac_sha256_hex was (40fb5b9): 04_method.dag signature + infer_method mirror (--required-regen first_generation_equal=true), dispatch roster, interpreter arm, std.primitives contract + surface name + roster, v1_interpreter_primitive_surface row. - p256_ecdsa_verify_b64url(key_point_b64url, signature_b64url, message) -> Bool?: 65-octet SEC 1 uncompressed point, 64-octet r||s, unpadded base64url; the message is SHA-256 hashed by the verifier. Absent on any non-admitted encoding. - extdeps.crypto.signature p256_ecdsa_verify: decodes both inputs, hands the DECODED octet lengths to signature_verification_from_implementation. Three new arms keep absence from reading as a mismatch: VerifyingKeyUndecodable, SignatureUndecodable, SignatureEncodingRefused (right lengths, not a point on the curve / scalar in range). - es256_jwt_sign(p8_pem_secret, key_id, team_id, issued_at_epoch) -> String?: header {alg ES256, kid}, claims {iss, iat}, JOSE r||s, RFC 6979 deterministic. - extdeps.apple.apns apns_provider_token -> ApnsProviderTokenMint (Signed | AuthKeyUnreadable | IssuedAtBeforeEpoch). - Witnesses (test.claim.p256_ecdsa_witness_test): RFC 7515 A.3's PUBLISHED ES256 signature verifies; tampered message, tampered signature and wrong key are SignatureInvalid; the compressed spelling of the right key is VerifyingKeyMalformed{33}; an off-curve point is SignatureEncodingRefused; the provider token equals an exact expected JWT (independently verified with openssl) and verifies under the RFC public key; unreadable key and negative iat refuse. Rust unit tests beside the arms. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ective framing, App Attest current checks - AdmittedDeviceRedemption and VerifiedDeviceEnrollment are sole_constructor; the decision commit consumes only the admitted value, and the login comes from the code's issuance. - Enrolment code, enrolment and revocation are generations of ONE CAS slot, so consuming the code is creating the enrolment; expired, reused, other-bytes and Android refuse. - Every signed/MAC'd message is length-framed (code-point counts): injective for any field content, where the unit-separator join was not (capability_text itself contains it). - capability_tag_hex names the tag's real encoding. - App Attest: AppIdPrefix (not team id), validation category and bundle version refusals, seam-minted VerifiedAttestation/VerifiedAssertion carrying key, receipt and client data; redemption joins the assertion to the enrolled attest key and the exact signed bytes. - APNs: top-level custom-data carrier; a 200 without apns-id is undecodable. - Reads that return capabilities are assertion-authenticated; residual stated. - std.measure Second/ByteSize for APNs ceilings and signature sizes (review 67564). - Unconsumed declarations removed or given named consumers; Android attestation corrected; iOS realization frontier added; stale approval_push references fixed. - gunbc.auth.approval_device_redemption_fixtures renders the cross-language vectors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…named in their frontiers Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… split wire contract into approval_device_wire Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…mption_fixtures regen) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…_realization_frontier (review 67617) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…itable, not validated (review 67625) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Admission takes the signing input plus seam-minted verdicts; SignedRedemption, EnrolmentRequest, PresentedEnrollmentEvidence and PushRegistration are wire bodies in approval_device_wire, with their deferred consumer stated once; RedemptionIdentityEvidence deleted. - Capability tag carried in its canonical base64url spelling and converted through capability_tag_hex (refusing a non-canonical spelling); witnesses start from issue_capability and refuse a hex tag placed in the base64url field. - App Attest assertion result renamed AssertionAuthentic and scoped to the checks it makes; counter and challenge left to the consumer, whose replay authority is stated; stored public key joined. - APNs: only Apple's documented priorities (10, 5); push type scoped to alert. - Read transcript vectors and a discriminating witness; enrolment store (code/enrolment/revocation as one CAS slot) with record round-trip witnesses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ane A) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ks already carry them Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…expectation typed - vectors.json is gunbc.generated_artifact ApprovalDeviceVectorsArtifact (new RepoConsumer CrossLanguageClientTest), so the required generated-artifact phase and the refusing merge driver gate it; the module's own regen/agree are deleted as a second route. - device_store_write takes std.durable_compare_and_set CasExpectation; the Int decode and its absorbing else are gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…7674) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… owner (side-chat condition) The issuer takes no login: issue_enrolment_code writes approval_operator_login. No HTTP issuer, since the loopback dashboard is reachable by every on-host POSIX user; the enrolment POST only consumes an already-issued code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # dag/gunbc/auth/approval_device_redemption.dag
…fact (from CI heal bundle) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…6_jwt_sign; the JWS is assembled in .dag (review 67702) The host kernel is only the P-256 signature. The APNs provider token's header, claims, base64url segments and compact form are now assembled from extdeps.languages.json.emit, a new extdeps.auth.jws (RFC 7515) and extdeps.crypto.signature p256_ecdsa_sign, and the token is typed as extdeps.auth.jwt JwtCompactSerialization. The unreachable issued_at < 0 check goes with the old primitive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # .gitattributes # .github/workflows/witnesses.yml # dag/gunbc/generated_artifact.dag # dag/gunbc/generated_artifact_emit.dag
…and P-256 ECDSA verify in .dag std.bitwise: UInt32 / Word64 ops by arithmetic, conjunction from a derived nibble table. std.bignat: 24-bit limbs (product < 2^48; conversion is octet slicing), Montgomery CIOS. extdeps.crypto.sha2: FIPS 180-4 SHA-256 (constants checked against a derivation from the primes). extdeps.crypto.nist_p256: SP 800-186 P-256, Jacobian arithmetic, Shamir, FIPS 186-5 verify. Measured on the v1 interpreter (clock in-process): SHA-256 of 1 KiB = 2724 ms, correct; one P-256 verify (RFC 6979 A.2.5 'sample') = 226455 ms, answer true. Above the gate's few-second bound: stopped and reported, not optimized. Not wired into any seam yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e emitter refuses body annotations, DESIGN 4c) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, at rustc (18-site receipt from the A1 gate) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…arithmetic as unbounded Int (std.bitwise xor reaching 2^33 typed UInt32) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The operator's no-Rust-escape-hatch ruling retires the host builtin route, and #11585's modules plus one of the two failure-mode rows have since landed on main. What remains is the substrate crypto and its witnesses: std.bitwise, std.bignat, extdeps.crypto.sha2, extdeps.crypto.nist_p256, and the SHA-256 and P-256 witness modules. Dropped here: the p256/es256 interpreter arms, their registrations, Cargo edges and Cargo.lock; extdeps.auth.jws (its consumer did not land, so it would be unconsumed); and the files main already carries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…over a supplied width Items 1-3 of the approved A2 shape, authored now because the corrected ruling leaves nothing in them waiting on the compiler lane: the type-level index keeps taking a Nat literal or PointerWidth, and BitWidth is std.measure's existing declaration reached at the value layer. - Three-arm width admission: WidthAdmitted, WidthNotStatic (MachineWidth<PointerWidth> has no constant to reify; IntPlatform and UIntPlatform are its live inhabitants) and WidthExceedsHostInt. The two refusals stay separate because they are different deficits. WidthExceedsHostInt carries its next-rung trigger: arbitrary precision through std.bignat, SUFFICIENT FOR UInt64 and UInt128 to become admitted widths. - A sole-constructed carrier whose range admission is its only mint, so an out-of-width value has no constructor. - Checked add/sub/mul/div/rem with typed outcomes, plus add_mod/mul_mod as the declared wrapping arm. WidthMismatch refuses two widths as operands rather than widening one. Multiplication carries its own admission, since a product needs twice the width. WidthNotStatic has no producer here and says so: only the binding layer, which needs reification, can mint it. Not authored: the UInt32/UInt8 bindings, the 18 conversions, any default width. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oduct's ceiling A reviewer asked whether the host-width rule refuses operands that are valid and computable. For the WRAPPING product it did, and that was an over-prohibition (DESIGN 4d): reduction modulo 2^N depends only on the low N bits, while the shared `bounded_nat_product_admitted` demanded 2N <= 62 because the CHECKED product needs the whole product in order to detect overflow. UInt32 multiplication therefore refused on both arms, though only one of them needed to. Splitting one operand at h = N / 2 gives (a * b) mod 2^N = ((a1 * b) mod 2^(N - h)) * 2^h + a0 * b (mod 2^N) whose intermediates are h + N and (N - h) + N bits. So `bounded_nat_mul_mod` now computes UInt32 wrapping products, while `bounded_nat_mul` on the same operands still refuses -- correctly, since its 64-bit product does not exist on this host. That distinction is what the shared ceiling was hiding. The new ceiling is stated rather than rounded up: the split admits N <= 41, not every width the carrier admits, and 42..62 still refuse as a property of the naive host realization. Its next-rung trigger names the capability -- bignat routing or a recursive split -- sufficient for every admitted width to have a computable wrapping product. Witnesses, run and read (10/10, exit 0), each with the inverted control run to show its red is reachable: both new vectors have 64-bit full products, so neither answer is obtainable by forming the product on this host, and the ceiling is asserted at its 41/42 edge so a later widening is reported rather than absorbed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pe's width REQUEST_CHANGES blocker 2, and a correction to a claim I made and built on. The checked product was admitted by `2N <= 62`, keyed on the TYPE's width rather than the OPERANDS' values, so 40-bit 3 * 3 was refused although its product is 9. The refused population was every small product of a wide type. The justification I gave -- that the checked arm "needs the full product in order to detect overflow" -- was false. Overflow is decidable before multiplying: b != 0 && a > (2^N - 1) / b <=> a * b > 2^N - 1 which forms no product, and when it admits, the product is bounded by 2^N - 1 and so sits inside the carrier's already-admitted width. Verified exhaustively over N = 3, 5, 8, 12: zero mismatches against the direct comparison. So `ProductExceedsHostInt` leaves the checked arm entirely. Re-derived rather than assumed (blocker item 4): the variant still has exactly ONE producer -- the wrapping arm at widths 42..62, where the split's intermediates genuinely do not fit -- so it stays vocabulary something produces. `bounded_nat_product_admitted` had no remaining reference and is deleted. The witness that asserted the old refusal ENCODED THE DEFECT rather than catching it, and is replaced by three points of the corrected behaviour: a tiny product of a wide type, a genuinely overflowing one, and the exact top of the width. A second witness of mine asserted that checked UInt32 still refused; the suite caught it red on this change, and it now asserts Overflows, which is the honest answer -- the two arms differ in the QUESTION they answer, not in their host headroom. 10/10 witnesses pass, with both rewritten witnesses re-run inverted to show their red is reachable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aims a falsehood Review of #11645, four findings, all correct. The arithmetic was re-derived here rather than accepted. THE OFF-BY-ONE, AND ITS ROOT. The split's admission compared its intermediate bit demand against `bounded_nat_host_bit_ceiling()` == 62 -- but that constant is the widest CARRIER this family admits (62 because a SUM needs N + 1 bits), not the host integer's positive capacity, which is 63. Two unrelated limits behind one constant: the same defect this branch removed one level up when it split the checked and wrapping admissions apart. Width 42's largest intermediate is 9223367638806167553 against a host maximum of 9223372036854775807 -- representable -- so 42 was being refused for no reason. 43 is the first width that genuinely cannot fit. `bounded_nat_host_positive_bit_capacity()` now states that limit separately. THE WITNESS HAD CANONIZED THE BUG. It asserted a REFUSAL at width 42 for 3 * 5, whose product is 15, so it certified the off-by-one and exercised only the admission predicate rather than the intermediate bound that predicate is about. It now asserts the admitted edge with near-maximal operands -- (2^42 - 1)^2 mod 2^42 = 1, an answer unobtainable by forming the 84-bit product -- and the refusal at 43. CHECKED MULTIPLICATION CONSUMES THE EXISTING AUTHORITY rather than re-deriving it. std.checked_arithmetic already owns "PRE-CHECK, NEVER POST-CHECK" and the division-based bound test in checked_int_multiply. The note now cites that as the pattern being consumed and states what is genuinely local: only the BOUND, since this family tests the carrier's 2^N - 1 rather than the host's range. THE ARM NAME STATED A FALSEHOOD. `ProductExceedsHostInt` is false for width-43 3 * 5 -- nothing exceeds anything; a realization refuses the width before looking at the operands. Renamed `WrappingProductUnavailableAtWidth`, with a trigger naming BOTH sufficient routes, recursive splitting or std.bignat. EVIDENCE. 10/10 pass. The boundary control is the discriminating one: restoring the constant to 62 turns the width-42 witness RED and names it, so the witness catches the exact off-by-one rather than asserting the predicate against itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hat assumed their invariant
REQUEST_CHANGES blocker 1, closed PARTIALLY and labelled as partial.
THE HOLE. Every overflow argument in this module rests on each limb lying in
[0, 2^24) -- that is what keeps a limb product below 2^48 and the CIOS accumulator
inside the host integer. But BigNat was an ordinary record, so any caller could
write BigNat { limbs: [-5, 99999999] } and the arithmetic would compute
confidently on it. MontgomeryModulus was likewise assemblable with an even or zero
modulus, a negative width, or an inconsistent inverse. A proof resting on an
invariant the constructor does not enforce is validation standing where
construction was available (DESIGN 5), and montgomery_add's own note asserted
"both operands below m, which every montgomery_* producer guarantees" -- a
guarantee nothing enforced.
WHAT IS CLOSED: fabrication. sole_constructor means no expression outside
std.bignat mints either carrier. This was cheap and safe because the real
population is ELEVEN constructions, all inside the module, zero outside -- my
earlier count of 32 across two files was a grep over "-> BigNat {", which counted
function signatures rather than constructions.
WHAT IS NOT CLOSED, stated on the carrier rather than implied: sole_constructor
restricts WHO may construct, not WHAT may be constructed. montgomery_modulus still
computes its fields from unchecked arguments. Its next-rung trigger names the
capability -- a refusing admission establishing positive width, odd nonzero
modulus, modulus fitting the width, and consistent derived fields -- and says why
it is deferred: making it refusing turns p256_field and p256_order into refusable
data rows, cascading through every consumer of the field arithmetic, and the P-256
suite has never executed, so that restructuring cannot yet be read for breakage.
THE PAIRED WITNESS, because a wall whose interior is never exercised is
specification-without-execution. Five small-modulus claims run the real producers
and read their answers: Montgomery round trip, multiplication that must reduce
past the modulus (20 * 30 = 600 = 6 * 97 + 18), additive wrap, and zero. 5/5 pass
-- the first executed evidence std.bignat has ever had.
CONTROL, perturbing the code rather than the assertion: setting bignat_base to
2^24 - 1 turns THREE of the five red by name. The other two do not depend on the
base under that perturbation, so they are weaker claims and are not credited with
discrimination they do not have.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… 69034) BOTH FINDINGS CONFIRMED FROM THE CODE. One correction to the second is below. FABRICATED DEFAULT ON THE CURVE PARAMETERS. `p256_hex_nat` answered `Absent => bignat_small(n: 0)`, and it produced p256_p, p256_n, p256_b, p256_gx and p256_gy -- so a mistyped literal would have substituted ZERO FOR THE FIELD PRIME and every verification below would have computed confidently on a degenerate curve. DESIGN 5 makes that a hard reject. GUARDING THE ARM WAS NOT THE FIX; REMOVING THE DECODE WAS. A refusing p256_hex_nat would make every consumer answer an Optional for a condition that cannot occur at runtime, since the input is a source literal. The parameters are now their big-endian octet encodings read by the total bignat_from_octets, so there is no arm to fabricate on. The same shape in the P-256 witness (`Absent => []`, where a mistyped vector became the empty message and the `is_absent` claims would still have GREENED, defeating their own red) is fixed the same way: all 14 vectors are octet literals and the decode helper is gone. THE UNCHECKED PUBLIC MINT. bignat_small took a raw caller Int, and my own note in the previous commit claimed the internal constructions derive limbs "never from unchecked caller input" -- false when written, of the very function that made it false. An annotation is never evidence (DESIGN 4c); that one asserted the invariant it stood next to a breach of. It now says so. CORRECTION TO THE FINDING, established by trying to write its test: the review names n >= 2^72 as overflowing the top limb. True in arithmetic, UNREACHABLE here -- Int is 64-bit, so the largest expressible n is 2^63 - 1, whose top limb is 32767, inside 2^24. My first fix guarded it anyway, and that guard was worse than dead: evaluating `bignat_base * bignat_base * bignat_base` is 2^72, which RAISES IntegerOverflow, so the check against an impossible input would itself have been the failure. The unreachable arm is described on the carrier instead of authored (DESIGN 4b: ask whether the RED is authorable before writing the check). The NEGATIVE case is real, reachable, and is what bignat_small now refuses -- it previously minted a negative limb. The two literal values this module needs are total constructors with identical limbs (bignat_zero_limbs, bignat_one), so substituting them at the p256 call sites changes no value. EVIDENCE: 6/6 bignat witnesses pass, including a new one asserting the negative refusal AND admission of the largest Int -- the positive control for the note that no Int escapes three limbs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…trigger row goes REVIEW 69054, FINDING 1, CONFIRMED FROM THE CODE AND FIXED AT THE SITE. word32_xor was `a + b - 2 * word32_and(...)` and word32_or was `a + b - word32_and(...)`, whose answers are right but whose SUM reaches 2^33 - 2 while typed UInt32; word32_add formed `a + b` before reducing. That is the invalid state gunbc.recurring_failure_mode bounded_natural_arithmetic_evaluated_as_unbounded_int names -- "a value typed UInt32 that is not a 32-bit unsigned natural" -- and the row on main cites word32_xor at this line. bitwise.dag is NEW in this PR, so it was landing fresh population of a class DESIGN 4b places BELOW the ladder, with no declared drop. The reviewer is right that a reader of std.bitwise alone could not have learned that. FIXED BY REMOVING THE OVERSIZED INTERMEDIATE, NOT BY DECLARING IT. Exclusive or is now nibble-wise through a derived table, exactly as word32_and already worked: each lookup is over values below 16 and the accumulator is bounded by the digits consumed. Disjunction is xor plus and, which are disjoint bit sets, so their sum IS the disjunction and is bounded by it. Addition decides the carry BEFORE forming the sum. Verified exhaustively at the edges and over 60,000 random pairs against the reference operations: zero mismatches, largest xor intermediate 4294960992, inside 2^32. SHA-256 still passes 7/7 and the bounded_nat suite 10/10. WHAT THIS DOES NOT DO, STATED SO IT IS NOT READ AS MORE: it closes the SILENT WRONGNESS half only. std.operator_realization refuses infix arithmetic on a structural operand regardless of MAGNITUDE, so these sites still emit as operator refusals -- the separate class accepted_source_emits_uncompilable_target. Closing that one needs the operations to be DECLARED rather than infix, i.e. routing through std.bounded_nat, which is blocked today by an import cycle rather than by reification: std.bounded_nat imports std.bitwise for bitwise_pow2, so bitwise cannot import it back (DESIGN 3, acyclicity). Relocating bitwise_pow2 breaks that cycle and is the real next step. FINDING 3: bounded_nat_width_ceiling_trigger was a String prose row with no reader -- DESIGN 4c dead data, the same shape as the rows removed in 7123b08. Deleted; the // block above already carries the trigger and now attaches to bounded_nat_admit, which is the function that produces the arm it describes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…RED receipt TRIGGER STATED AT THE WRONG GRAIN, CORRECTED. std.bounded_nat's frontier said its first consumer waits on reification of the N in MachineWidth<N>. It does not: the operations take a width as a VALUE and the witnesses pass bit_width(count: 32) today, so nothing stopped std.bitwise passing one. Reification lets a CALLER stop naming a width; it is not what stands between this family and its first consumer. That is DESIGN 4b(3)'s own test -- a trigger naming less than the capability it gates is satisfied while the capability stays dead -- applied to a row I wrote. THE ACTUAL BLOCKER IS AN IMPORT CYCLE: std.bounded_nat imports std.bitwise for bitwise_pow2, so std.bitwise cannot import back, and DESIGN 3 makes acyclicity the import graph's only structural law. It is breakable by relocating bitwise_pow2 below both. The SECOND cost is stated beside it rather than left as a one-line blocker: after the cycle breaks, every word32_* operation answers a six-arm BoundedNatArith, and threading that through SHA-256's 64-round compression fold is what the first consumer actually costs. Deferred to its own change with the P-256 suite as its gate, in that order, because the only suite covering that fold has never returned a result. RECEIPT FILED ON AN EXISTING CLASS rather than a new row: check_subject_shape_cannot_represent_the_state_the_check_detects. Review 69034 named two invalid inputs to bignat_small; the negative is real and reachable, and n >= 2^72 is UNREPRESENTABLE in the subject, since Int is 64-bit and the largest expressible n has top limb 32767, inside 2^24. A check for it is permanently green over its own subject. WHAT THAT ROW DID NOT YET CARRY, and why this receipt is worth its space: the guard first authored was `n >= bignat_base * bignat_base * bignat_base`, and that expression IS 2^72, which RAISES IntegerOverflow. So the check against an input that cannot occur would itself have been the only way to make the function fail -- converting a total function into a raising one, on every call, in service of a state no caller could supply. A decoration costs trust; this would have cost correctness. And it was caught by trying to WRITE THE RED: the witness could not be authored, because 4722366482869645213696 is not a writable Int literal. The row's own recognition rule was satisfied by the compiler refusing the construction. bounded_nat 10/10 still passes; the edited ledger row resolves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review 69076, confirmed and correct. word64_add was `let lo = a.lo + b.lo` over two UInt32 halves -- reaching 2^33 - 2 while typed UInt32 -- and `a.hi + b.hi + carry` doing it again. That is the same population of bounded_natural_arithmetic_evaluated_as_unbounded_int that the note I added three commits ago says word32_xor, word32_or and word32_add were rewritten to leave. I APPLIED THE REPAIR TO THE 32-BIT FAMILY AND SKIPPED THE 64-BIT ONE IN THE SAME DIFF, while writing the explanation of why it was needed. The defect survived inside its own fix. It was live rather than latent: sha256_fips_witness_test drives lo to exactly 2^32 on every run. FIXED BY REUSING THE ALREADY-CORRECTED OPERATION rather than repeating its reasoning. The carry is decided before either sum is formed, by the same comparison word32_add uses, and both halves are produced by word32_add itself instead of a raw `%` -- so the wrap is computed once, in one place. I SCANNED THE REST OF THE FILE RATHER THAN FIXING ONLY THE SITE NAMED, since "applied here, skipped there" is the defect. Six arithmetic sites remain over UInt32 halves -- word32_shl, word32_rotr, word32_add's guarded branch, word64_shr and both word64_rotr halves -- and every one is a sum of DISJOINT bit ranges. Measured over 80,000 random inputs: largest intermediate 4294964487, inside 2^32. word64_add was the only defect. EVIDENCE: SHA-256 7/7 passes, and the control is discriminating rather than a formality -- replacing the carry with a constant 0 reds word64_add_carries_across_the_halves_and_rotr_swaps_them by name, so the witness catches the carry and not merely the shape. The proposed implementation was also checked against 64-bit modular addition over 80,000 random pairs plus the edges, including the witness's own all-ones + 1: zero mismatches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…esent tense Review 69091, confirmed. The header said this module is "the producer behind extdeps.crypto.signature p256_ecdsa_verify". That module is real and owns the suite, the encodings and the refusal vocabulary -- but it declares no p256_ecdsa_verify, and neither does anything else: the name exists nowhere outside this module and its witnesses. The only importers of all five new modules are the four witness files. THIS IS THE CLASS THIS PR CITES TWICE IN ITS OWN RECEIPTS. unlanded_citation_indistinguishable_at_the_citing_end is exactly a name that reads as landed at the point it is cited. Those receipt rows flag their unlanded names scrupulously -- "UNLANDED NAMES, read as such" -- and this header, the one sentence a reader consults to learn who consumes 1,772 lines of new crypto, did not. Being the plan does not make a citation true. Restated as the DESIGN 3c frontier it is: nothing in production consumes this today, the consumers are the witnesses, and the trigger is named -- extdeps.crypto.signature gains p256_ecdsa_verify binding p256_ecdsa_verify_octets through signature_verification_from_implementation. I CHECKED THE OTHER FOUR NEW MODULES RATHER THAN ONLY THE SITE NAMED, since applying a repair at one site and skipping its siblings is the defect I shipped one round ago. Two make similar claims and BOTH ARE ACCURATE: std.bitwise cites std.bit's Word32, which exists (dag/std/bit.dag:16), and extdeps.crypto.sha2 cites extdeps.crypto.hash's Digest carrier, which exists (dag/extdeps/crypto/hash.dag:34). nist_p256 was the only fictional citation. Module resolves after the edit.
briansrls
left a comment
There was a problem hiding this comment.
EVIDENCE HOLD — exact head 2caafa5.
The restored floor established the important fact, but not P-256 verification: 4 cheap encoding/range witnesses reached verdicts; the 8 witnesses that exercise scalar/group verification were all budget-refused-before-verdict at the 8000 ms wall deadline. The floor itself classifies each as UNDECIDED with cost above 8000 ms and no upper bound. Green aggregation is therefore not verification evidence.
RULING ON THE ROUTE: a dedicated P-256 instrument with its own measured ceiling is rung-honest. It is not a waiver. It must name the exact bounded witness population, execute it, and treat ceiling expiry as refusal/unknown. A row that merely turns budget-refused-before-verdict into an accepted outcome would be rung inflation. The existing eval-step-cost-drop precedent is deliberately stricter: members still execute to verdict and wall-clock crossing still blocks; membership only changes adjudication of an eval-step overrun and does not raise the budget.
Native emission is NOT the only honest route. It is an honest long-term route for this module only if the production verifier is structurally native-only (or the interpreted production route refuses), the emitted function is this exact .dag verifier rather than a handwritten substitute, and the same positive/negative vectors execute against an exact-head emitted binary. Source→interpretation and source→native are distinct paths; native success cannot be cited as interpreter success.
Recommended recut/evidence order:
- Instrument the cost by phase: modulus/context construction, modular inverse, point double/add, double-scalar multiplication, and SHA-256. The implementation already uses Montgomery CIOS; do not describe “move to Montgomery” as the missing repair. Likely candidates are repeated context/R² derivation, Fermat inversions, bit-by-bit Shamir multiplication, or interpreter overhead.
- Add a dedicated instrument/runner route with an exact bounded population and a ceiling derived from one complete measurement, plus an overall job bound. Run the 8 substantive witnesses to terminal verdict on the exact head, with a deliberately mutated control that goes red by name.
- After MachineWidth emission lands, execute the full vector set against the emitted native verifier and make that the recurring production-path evidence. Retain at least a discriminating interpreted anchor unless the interpreter route is structurally excluded from the verifier's production capability.
- If the ordinary required floor ceases to own terminal verdicts for these identities, declare that path loss under §4b(3): previous/temporary rung, exact 8-member population, reason, owner/due date, and a capability-level restoration trigger. The drop may describe the missing standard-floor path; it may not count timeout as correctness.
The run cited in the rollup checked out bb50768, not this current head. It is valid evidence of the cost/coverage class, but final acceptance evidence must execute the selected route again at the exact merge head.
|
Answering review 69110 (dashboard artifact The finding — "1,789 lines of new crypto land with their central claim never executed" — no longer holds as written. The suite has now executed, standalone, on an uncontended host:
That is the pair the review asked for: an accepted positive control plus a discriminating red on the real acceptance path. It specifically retires the strongest form of the objection — that the four passing witnesses all assert The circularity the review identified was real and is fixed. The What still has not executed, declared rather than argued away — a §4b(3) drop in the body with all five parts, population named claim-by-claim: five of the eight accepting-path claims are On the review's "compounding" point — it is sharper than stated, and the body now says so. Those eleven claims are discovered by the witnesses lane, and the lane reported SUCCESS anyway. Why the suite could not run in CI, measured: one interpreted verify peaks at 13.5 GiB RSS (
— sent from wise-owl-628 |
briansrls
left a comment
There was a problem hiding this comment.
WIND-DOWN APPROVE, exact head bb50768. This supersedes my evidence hold 5260787321 under the operator's wind-down disposition.
The PR body now states the evidence at its actual extent: the four named pre-curve refusal claims plus the standalone P-256 accept/reject executions, and explicitly says that this does not establish the verifier. It labels the exact eight verification-performing claims UNKNOWN rather than passing, treats timeout as neither pass nor failure, and names the bounded eight-member population. The §4b(3) trigger is at capability grain: the dedicated P-256 instrument must execute that exact population against the exact emitted native build, reach every expected terminal verdict, and carry a discriminating semantic control. The body also opens the disposition as substrate acceptance with an explicit evidence gap—not security sign-off—and separately records the app-to-verifier binding as ecdsa_verification_realization_frontier.
This approval accepts the substrate under that explicit evidence gap. It is not P-256/ES256 security sign-off and does not establish the production device-signature path. No new execution or dispatch is required in this wind-down PR.
|
Review 69110 (12:02Z) is right about the state it read, and it is the state the PR now declares rather than disputes. Since then: the floor lane executed the suite (run 35509606112) — four claims passed, the eight verification-performing claims were budget-refused at 8 s each; the lane produced one standalone accept/reject pair (RFC 6979 A.2.5 verifies at 192 s; the same vector with one bit flipped in r rejects at 205 s); one interpreted verification peaks at 13.5 GiB, so no floor budget can admit it. Under the operator's wind-down the PR body was rewritten to the side chat's five constraints: the eight are declared UNKNOWN (not passing) as an exact, bounded §4b(3) drop whose trigger is the capability a dedicated P-256 |
…P-256 and P-384 as parameter rows, SHA-384, and the frontier retargeted to the decided shape extdeps.crypto.nist_prime_curve carries the a = -3 Jacobian group law, Shamir double-scalar ladder, partial public-key validation and ECDSA verification over a SUPPLIED digest, written once (the formulas #11645 landed for P-256, moved not rewritten). extdeps.crypto.nist_p256 is now its parameter row plus the SHA-256-bound verifier the device signature uses; the new extdeps.crypto.nist_p384 is the second row, consumed by Apple's chain step, whose two links are a P-384 key signing with SHA-256 and a P-384 key signing with SHA-384. extdeps.crypto.sha2 gains SHA-384 (the SHA-512 computation with its own initial state, truncated) over std.bitwise Word64, its constants derived from the definition rather than transcribed. P-256 semantics preserved by execution: the RFC 6979 sample signature verifies and the different-message control is false through the family fold (88 s / 86 s, 56M / 55M eval steps, peak 15.5 GiB), and n.G = infinity holds. P-384 established on real bytes: Apple's CA 1 and root keys are on the curve, a neighbouring point is not; the two chain-link verifications are enrolled and NOT executed here (at 17 limbs they exceed this container's memory), the same disposition as #11645's eight P-256 rows: a dedicated native gunbc test row is their executing evidence. gunbc.auth.approval_device_redemption ecdsa_verification_realization_frontier no longer names the host-Rust primitive the operator refused; it names the .dag readers landed by #11970/#11975/ this change, the verifier folds still owed, and the execution route decision (natively emitted handler once MachineWidth<N> reification lands; interpreter cost is the v1-performance row). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The approval app's crypto, authored in
.dagand intended to run compiled. Draft: blocked on compile-time reflection ofMachineWidth<N>, dispatched as its own lane.What this carries
Eight files, nothing else:
std.bitwise— bitwise operations over bounded naturals, emulated arithmetically. Conjunction from a derived nibble table; xor and or follow from it.std.bignat— multi-limb unsigned naturals, 24-bit limbs (a limb product is below 2^48, and 24 bits is exactly three octets), Montgomery CIOS, Fermat inverse.extdeps.crypto.sha2— SHA-256, FIPS 180-4.extdeps.crypto.nist_p256— P-256 (SP 800-186) and ECDSA verify (FIPS 186-5 §6.4.2), Jacobian arithmetic with Shamir's trick.std.bounded_nat— the declared operation family for bounded naturals, over a supplied width: a three-arm width admission, a sole-constructed carrier whose range admission is its only mint, checked arithmetic with typed outcomes, andadd_mod/mul_modas the named wrapping arm. It is whatstd.operator_realization's refusal points at. TheUInt32/UInt8bindings are not here: they need the width reified from the declaration.Evidence so far
sha256_fips_witness_test: 7/7 interpreted. The round constants and initial hash values were checked against a derivation from the primes, not transcribed on trust.bounded_nat_witness_test: 8 of 8 pass, executed 2026-09-20. Run asgunbc runover a driver that tags each failing witness by name, against agunbcI built from this exact tree (8b7792a) into a per-session target dir. Positive arm: exit 0, empty output, 109s. Negative control, because exit 0 with empty output is also what a broken driver returns: inverting one witness with!gives exit 1 and printsa_value_inside_the_width_mints_and_one_outside_it_does_not. So RED is reachable and the green is readable./cargo-target/release/gunbcfailed instd.keyed_row/std.keyed_roster— files this branch does not touch. That binary is not the seed: it carries another lane's unlanded kinding check (verified by a string the seed does not contain; my build has 0 occurrences, it has 1). The failure was a fact about the instrument, not the corpus, and it is reported to that lane. Per-session target dirs are why the result above is trustworthy..dagexecution is currently unusable in my container: a module importing onlystd.processtakes 19 minutes to run on thismain, against about 110 seconds for the same shape earlier the same day. That is a discriminating control — it contains none of this PR's code — and it holds with agunbcrebuilt from this exact tree, so it is neither these modules nor a stale binary. The figures above are therefore from the pre-merge tree; and CI does not check them either, for the reason stated above.Why it is blocked
Emitting this closure to native Rust is refused at 18 sites: arithmetic on
std.integer's bounded naturals has no host realization (UInt32,UInt8). The approved repair is one bounded-natural operation family instd.integer, derived once over N, with checked arithmetic and explicitly-named wrapping. That needs theNofMachineWidth<N>readable in a body, which the operator ruled on 2026-09-20 as compile-time static reflection. That compiler work is a separate lane; this PR waits for it, then converts every infix site here.Two failure modes found on the way landed separately in #11702.
🤖 Generated with Claude Code
What this PR is, and what it is not
In scope: the six
.dagcrypto modules andstd.bounded_nat. None of them names a width, and each stands on its own.Explicitly OUT of scope: the 18 infix conversion sites, and the interpreted-vs-native equal-answer witnesses. Their trigger is the compile-time reification of the
NinMachineWidth<N>(operator ruling, 2026-09-20), which is a separate lane. So if you are wondering why the operation family has no callers yet: its binding layer is a declared frontier with a named trigger, not an oversight.std.bounded_nattakes its width as a supplied value until that lands.Re-running the witnesses, since nothing else will
CORRECTED 2026-09-20 — I was wrong about this, in the pessimistic direction. I checked
v2.workflow.required_floorrequired_gate_prefixes, found no match for these modules, and concluded CI never runs them. That reads ONE selector and concludes about the whole lane. The floor job ALSO selects by changed enclosing declaration, and it reaches them: floor run35505867616ond3de6681685planned 35 of this PR's witnesses asplanned_as_changed_witnessand executed 27 of them.bounded_nat_witness_teststanding=admitted, 0–2 ms eachbignat_montgomery_witness_testsha256_fips_witness_testp256_dag_ecdsa_witness_testSo a green check on this PR does mean 27 of these witnesses ran, in a required lane. That is stronger evidence than this body previously claimed. To reproduce locally:
The driver ANDs the eight
test fns and prints the name of each one that fails, so exit 0 with empty output is a pass and exit 1 names the failure. Build with a PER-SESSION target dir: the shared/cargo-target/release/gunbcis whatever another session built last, and may not be the seed.Review response: the host-width rule was over-prohibiting, and is fixed
The reviewer asked whether host-width safeguards reject valid operands. For the wrapping product they did, and that is now fixed in
95004fb. Answering the concrete case from the code:bounded_nat_product_admittedwas2N <= 62, shared by both multiplications, soUInt32refused withProductExceedsHostIntinbounded_nat_mulandbounded_nat_mul_mod. Only the checked arm needed that: detecting overflow requires the whole product to exist; reduction mod 2^N needs only the low N bits.extdeps.crypto.sha2is additive — its only*are rawIntbyte-packing (word * 256,8 * (i - 1)) — andextdeps.crypto.nist_p256routes all field multiplication throughstd.bignatMontgomery arithmetic (52 references; zero uses ofUInt32orstd.bounded_nat). So nothing in this PR needed the refused operation.h = N / 2computes the low N bits without forming the product.UInt32wrapping multiplication now succeeds;UInt32checked multiplication still refuses, correctly.The new ceiling is stated, not rounded. The split admits
N <= 41— not every width the carrier admits — and 42..62 still refuse as a property of the naive host realization, with a next-rung trigger naming the capability (bignat routing or a recursive split). The pre-existingbounded_nat_width_ceiling_triggeris a different ceiling (magnitudes exceeding the hostInt, i.e.UInt64/UInt128), where bignat genuinely is required; it is unchanged and still correct.Evidence: 10/10 witnesses pass (exit 0), and each new witness was re-run inverted to show its red is reachable (exit 1, naming both). Both new vectors have 64-bit full products, so neither answer is reachable by forming the product on this host — the split is doing the work. The ceiling is asserted at its 41/42 edge so a later widening is reported rather than absorbed.
Review round 2: what is closed, and what is explicitly NOT
The boundary was wrong and is fixed (42/43, not 41/42). The split's admission compared its bit demand against
bounded_nat_host_bit_ceiling()= 62 — the widest carrier this family admits — when the relevant limit is the host's positive capacity, 63. Two unrelated limits behind one constant: the same defect this branch had just removed one level up. Width 42's worst intermediate is9223367638806167553against an i64 max of9223372036854775807; 43 is the first that genuinely does not fit.The witness had canonized that off-by-one — it asserted a refusal at width 42 for
3 × 5, product 15. It now asserts the admitted edge with near-maximal operands,(2^42 − 1)² mod 2^42 = 1, an answer unobtainable by forming the true 84-bit product.Checked multiplication consumes
std.checked_arithmeticrather than re-deriving it: that module already owns "PRE-CHECK, NEVER POST-CHECK" and the division-based bound test. Only the bound is local here (the carrier's2^N − 1, not the host range).The refusal arm was renamed from
ProductExceedsHostInt, which stated a falsehood — at width 43,3 × 5is 15 and nothing exceeds anything — toWrappingProductUnavailableAtWidth, with a trigger naming both sufficient routes (recursive splitting orstd.bignat).std.bignat: fabrication closed, invariant admission NOT closedBigNatandMontgomeryModulusare nowsole_constructor. Every overflow argument in that module rests on each limb lying in[0, 2^24), yet as plain records any caller could writeBigNat { limbs: [-5, 99999999] }and the arithmetic would compute on it.This is a partial close and must not be read as a full one.
sole_constructorrestricts who may construct, not what may be constructed.montgomery_modulusstill derives its fields from unchecked arguments, so an even modulus, zero modulus, or negative width remains constructible inside the module. The next-rung trigger names the capability (a refusing admission establishing each invariant) and is deferred because making it refusing turnsp256_field/p256_orderinto refusabledatarows, cascading through every consumer of the field arithmetic — and the P-256 suite has never executed, so that restructuring cannot yet be read for breakage.Paired witness: 5 small-modulus claims exercise the real producers (Montgomery round trip, a multiplication that must reduce —
20 × 30 = 600 = 6 × 97 + 18— additive wrap, zero). 5/5 pass: the first executed evidencestd.bignathas ever had. Control: settingbignat_baseto2^24 − 1turns three of the five red by name; the other two do not depend on the base under that perturbation and are not credited with discrimination they lack.Review round 3 (69054): the failure mode closed at its own site — one half of it
std.bitwiseis new in this PR, and it was landing fresh population of a class already filed onmain:word32_xorwasa + b - 2 * word32_and(...), whose sum reaches 2^33 − 2 while typedUInt32. The failure-mode row cites that line verbatim, DESIGN §4b puts the class below the ladder, and no drop was declared. The finding was correct.Fixed by removing the oversized intermediate, not by declaring it. Exclusive or is nibble-wise through a derived table (as
word32_andalready was); disjunction isxor + and, which are disjoint bit sets, so their sum is the disjunction and is bounded by it; addition decides the carry before forming the sum. Verified at the edges and over 60,000 random pairs against the reference operations — zero mismatches, largest xor intermediate4294960992, inside 2^32. SHA-256 still 7/7, bounded_nat 10/10.This closes ONE of two classes and must not be read as both. The invalid state
bounded_natural_arithmetic_evaluated_as_unbounded_intnames — "a value typed UInt32 that is not a 32-bit unsigned natural" — is now unreachable instd.bitwise. Butstd.operator_realizationrefuses infix arithmetic on a structural operand regardless of magnitude, so these sites still emit as operator refusals: the separate classaccepted_source_emits_uncompilable_target. In-range values do not make infix emittable.The consumer frontier's trigger was stated at the wrong grain and is corrected. It said this family waits on reification. It does not — the operations take a width as a value, and the witnesses pass
bit_width(count: 32)today. The real blocker is an import cycle:std.bounded_natimportsstd.bitwiseforbitwise_pow2, sobitwisecannot import back (DESIGN §3, acyclicity). Breakable by relocatingbitwise_pow2. The second cost is named beside it: threading a six-armBoundedNatAriththrough SHA-256's 64-round fold. Deferred to its own change with the P-256 suite as its gate, in that order.Also removed:
bounded_nat_width_ceiling_trigger, aStringprose row with no reader (DESIGN §4c dead data).Review round 4 (69076): the repair had skipped its own 64-bit half
word64_addwaslet lo = a.lo + b.loover twoUInt32halves — the same population the note added in the previous commit saysword32_xor/word32_or/word32_addwere rewritten to leave. The repair was applied to the 32-bit family and skipped for the 64-bit one in the same diff, while writing the explanation of why it was needed. Live, not latent:sha256_fips_witness_testdrivesloto exactly 2^32 on every run.Fixed by reusing the already-corrected
word32_addrather than repeating its reasoning — the carry is decided before either sum is formed, and both halves go throughword32_addinstead of a raw%.The rest of the file was scanned, not just the site named, since "applied here, skipped there" was the defect. Six remaining arithmetic sites over
UInt32halves are all sums of disjoint bit ranges; measured over 80,000 random inputs the largest intermediate is4294964487, inside 2^32.word64_addwas the only defect.Control is discriminating, not a formality: replacing the carry with a constant
0redsword64_add_carries_across_the_halves_and_rotr_swaps_themby name. SHA-256 7/7 with the fix; the implementation also checked against 64-bit modular addition over 80,000 pairs plus edges — zero mismatches.P-256: what was obtained, and a §4b(3) drop for what is UNKNOWN
What this PR is: substrate acceptance with an explicit evidence gap. It is not security sign-off.
It does NOT state, and must not be read to imply, that the full P-256 verifier is verified, that the
ES256 security argument holds, or that the production device-signature path has been verified. The
binding from the app into the verifier is not exercised here at all and is a separately named
frontier (
gunbc.auth.approval_device_redemptionecdsa_verification_realization_frontier).EVIDENCE OBTAINED — and its exact extent
Two things, and nothing wider:
a_point_off_the_curve_is_absent,wycheproof_2_r_plus_n_is_absent,wycheproof_11_r_and_s_zero_are_absent,wycheproof_26_r_equal_to_n_is_absent);sample(P-256/SHA-256)truerflipped 239 → 238falsetesttrueWhat that establishes is bounded: the verifier accepts two published vectors and rejects a one-bit
perturbation of one of them. Before it, every executed P-256 witness was an
is_absentcase, all ofwhich a verifier returning
Absentunconditionally would pass. It does not establish the verifier.THE EIGHT VERIFICATION-PERFORMING CLAIMS ARE UNKNOWN, NOT PASSING
Stated in those words because the distinction is the whole point: five are
exit=124timeouts on a1200 s cap under host memory pressure and one was never attempted, so no verdict exists for them in
either direction. A timeout is not a failure and is emphatically not a pass.
THE DROP — §4b(3), five parts, exact and bounded
the honest previous state is unexecuted, not a lost guarantee.
VmHWM 14,132,132 kB) and192 s–734 s–unbounded wall depending on host residency; the required floor applies a ~8 s per-claim
wall deadline, and the available remote runner is 7.63 GiB. A per-claim step budget would not bind
this, because the binding constraint is resident memory.
test.claim.p256_dag_ecdsa_witness_test:the_rfc6979_sample_signature_verifies,the_rfc6979_test_signature_verifies,a_signature_for_a_different_message_is_false,the_wrong_key_is_false,wycheproof_1_the_malleable_signature_verifies,wycheproof_4_n_minus_r_is_false,wycheproof_60_the_shamir_edge_case_verifies,the_base_point_is_on_the_curve_and_has_order_n.eight-member verification population against the exact emitted native build, reaches the expected
terminal verdict for every member, and carries a discriminating semantic control.
WHAT THE GREEN CHECK IS NOT
floor = SUCCESSon this PR is a budget refusal, not a pass. The floor emittedverdict=FloorRefused, 8 ×INTERRUPTED-BEFORE-VERDICT raised_by=wall_deadlineand 12 ×ENROLMENT-MARGIN-REFUSED, and still concluded success — because the witness floor does not gate at all:docs/design-rung-drops.md, "The witness floor as a required merge gate", declared 2026-09-20, rung drop, deleted without replacement. Do not read coverage from it.Not measured, stated so nothing is inferred
eval_stepsfor a verify is unmeasured (the floor preempts these before measuring —cost=UNMEASURED). Attribution of the cost betweenstd.bignatmul/mod and the interpreter is unmeasured. What is measured: the three non-verifying claims cost ~288.5 K steps at ~415 ms, so ~99.8 % of a verify's wall is curve math rather than load — which separates curve math from overhead, not bignat from interpreter.