From 915fdb064ef0fc21300556b08a818ed39611b153 Mon Sep 17 00:00:00 2001 From: Elena Nadolinski Date: Tue, 2 Jun 2026 23:28:20 -0400 Subject: [PATCH 01/10] init --- bench/README.md | 84 ++ bench/agent_log/000-baseline.md | 90 ++ bench/agent_log/001-hinted-inversion-model.md | 62 + .../002-hinted-inversion-prototype.md | 73 ++ .../003-48-byte-hints-negative-tests.md | 100 ++ .../004-real-attestation-hot-path.md | 111 ++ .../005-real-attestation-per-cert-split.md | 100 ++ .../006-production-shaped-hinted-flow.md | 126 ++ .../agent_log/007-full-cold-warm-sequence.md | 151 +++ .../008-production-candidate-audit-docs.md | 159 +++ .../009-deployable-external-p384-verifier.md | 160 +++ .../010-offchain-witness-generator.md | 143 +++ bench/p384_hints.js | 624 ++++++++++ src/CertManagerHinted.sol | 11 + src/CertManagerHintedExternal.sol | 327 +++++ src/ECDSA384Hinted.sol | 1047 ++++++++++++++++ src/IHintedCertManager.sol | 16 + src/IP384Verifier.sol | 11 + src/NitroValidatorHinted.sol | 14 + src/NitroValidatorHintedExternal.sol | 217 ++++ src/P384Verifier.sol | 29 + test/bench/Bench.t.sol | 291 +++++ test/bench/ECDSA384Bench.sol | 1081 +++++++++++++++++ test/bench/HintedNitroBench.sol | 293 +++++ test/bench/RealAttestationBench.t.sol | 676 +++++++++++ 25 files changed, 5996 insertions(+) create mode 100644 bench/README.md create mode 100644 bench/agent_log/000-baseline.md create mode 100644 bench/agent_log/001-hinted-inversion-model.md create mode 100644 bench/agent_log/002-hinted-inversion-prototype.md create mode 100644 bench/agent_log/003-48-byte-hints-negative-tests.md create mode 100644 bench/agent_log/004-real-attestation-hot-path.md create mode 100644 bench/agent_log/005-real-attestation-per-cert-split.md create mode 100644 bench/agent_log/006-production-shaped-hinted-flow.md create mode 100644 bench/agent_log/007-full-cold-warm-sequence.md create mode 100644 bench/agent_log/008-production-candidate-audit-docs.md create mode 100644 bench/agent_log/009-deployable-external-p384-verifier.md create mode 100644 bench/agent_log/010-offchain-witness-generator.md create mode 100644 bench/p384_hints.js create mode 100644 src/CertManagerHinted.sol create mode 100644 src/CertManagerHintedExternal.sol create mode 100644 src/ECDSA384Hinted.sol create mode 100644 src/IHintedCertManager.sol create mode 100644 src/IP384Verifier.sol create mode 100644 src/NitroValidatorHinted.sol create mode 100644 src/NitroValidatorHintedExternal.sol create mode 100644 src/P384Verifier.sol create mode 100644 test/bench/Bench.t.sol create mode 100644 test/bench/ECDSA384Bench.sol create mode 100644 test/bench/HintedNitroBench.sol create mode 100644 test/bench/RealAttestationBench.t.sol diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 0000000..57ea19f --- /dev/null +++ b/bench/README.md @@ -0,0 +1,84 @@ +# P384 / Fusaka gas benchmarking loop + +Goal: make on-chain AWS Nitro TEE attestation verification (ECDSA over secp384r1) +land **fully within the EIP-7825 per-transaction gas cap (16,777,216) on Base**, +surviving the Fusaka MODEXP repricing (EIP-7883). Each cert verify should fit in a +single tx; a full cold chain may span several txs but must land within ~5s. + +## The loop + +``` +baseline gas -> experiment -> agent_log/NNN-*.md -> recommended next steps -> execute & repeat +``` + +Every iteration is an entry in `agent_log/`. Each entry records: hypothesis, what +was changed, the measured/modelled numbers, whether it fits the cap, soundness +notes, and the recommended next experiment. + +## Methodology / tooling + +- **Ground-truth current gas**: `forge test` with `gasleft()` around a single + uninstrumented `ECDSA384.verify` (`test/bench/Bench.t.sol`). +- **Exact MODEXP census**: `test/bench/ECDSA384Bench.sol` is an *instrumented copy* + of the library (`lib/solidity-lib/.../ECDSA384.sol`) with transient-storage + counters that tally field inversions vs. other modexp calls per verify. The + submodule itself is never modified. +- **Post-Fusaka projection**: only the MODEXP portion reprices. We compute exact + per-call gas under EIP-2565 (current) and EIP-7883 (post-Fusaka) from the EIP + formulas, applied to this library's operand profiles, and add the unchanged + non-MODEXP remainder. +- A debug-trace census (`vm.startDebugTraceRecording`) was tried and **abandoned**: + materializing the step array for a ~8M-gas verify costs >1B gas / OOMs. The + instrumented-copy approach is the supported method. + +### Per-call MODEXP gas (this library's profiles) + +| call | base/exp/mod (bytes) | EIP-2565 | EIP-7883 | factor | +|------|----------------------|----------|----------|--------| +| field inversion (`modinv`/`moddivAssign`) | 64 / 64 / 64 | 8,170 | 81,792 | ~10× | +| squaring / mulmul / reduce / mod | ≤96 / ≤32 / 64 | 200 (floor) | 500 (floor) | 2.5× | + +EIP-7883 vs EIP-2565 for the inversion compounds three changes: complexity +multiplier `words²→2·words²`, exponent per-word `8→16`, and removal of `÷3`. + +## Run + +```sh +git submodule update --init --recursive +forge test --match-path "test/bench/Bench.t.sol" -vv +``` + +## Off-chain hints + +Experiment 010 adds a dependency-free Node witness generator: + +```sh +node bench/p384_hints.js verify --hash <0xhash> --signature <0xr_s> --pubkey <0xxy> +node bench/p384_hints.js cert --cert <0xder|base64|@file> --pubkey <0xparent_xy> +node bench/p384_hints.js attestation --attestation <0xcose|base64|@file> --pubkey <0xleaf_xy> +``` + +To cross-check the generator against the Solidity collector for the real Nitro +fixture: + +```sh +NITRO_RUN_FFI=true forge test --ffi \ + --match-path test/bench/RealAttestationBench.t.sol \ + --match-test test_010_OffchainWitnessGeneratorMatchesSolidityCollector -vv +``` + +## Numbers to beat + +- Current single verify: **7.94M** gas. +- Projected single verify post-Fusaka (unoptimized): **50.6M** gas — **3× over the cap**. +- Projected hinted single verify post-Fusaka: **6.04M** gas including worst-case + calldata for 48-byte inverse witnesses. +- Real attestation cached hot path with hinted P384 projection: **13.68M** gas + post-Fusaka including worst-case witness calldata. +- Real attestation non-root cert split transactions with hinted P384 projection: + **6.80M-7.03M** gas post-Fusaka each. +- Real attestation full minimum cold sequence for the current fixture: + **5 transactions**. The max projected tx is **13.68M** gas post-Fusaka. +- Real attestation warm-cache sequence for the current fixture: + **1 transaction**, projected at **13.64M** gas post-Fusaka. +- Per-tx cap (EIP-7825): **16,777,216**. diff --git a/bench/agent_log/000-baseline.md b/bench/agent_log/000-baseline.md new file mode 100644 index 0000000..9ef750b --- /dev/null +++ b/bench/agent_log/000-baseline.md @@ -0,0 +1,90 @@ +# 000 — Baseline + +Date: 2026-06-02 +Status: ✅ complete + +## Question + +Given Fusaka's MODEXP repricing (EIP-7883) and the 16,777,216 per-tx gas cap +(EIP-7825), can the existing nitro-validator P384 path land on Base? Where does +the gas actually go? + +## Method + +- `forge test` + `gasleft()` for ground-truth current gas (uninstrumented). +- Instrumented copy `test/bench/ECDSA384Bench.sol` with transient counters for an + exact MODEXP census (inversions vs. other) — see `bench/README.md`. +- Post-Fusaka projection via exact EIP-2565 / EIP-7883 per-call formulas. + +## Results + +### Full attestation (cold, all certs, one tx) +- `validateAttestation`: **53.4M gas** (measured, `NitroValidator.t.sol`). + README quotes ~63M; current measured is 53.4M. + +### Single ECDSA-P384 verify (the hot unit) +| metric | value | +|--------|-------| +| measured gas (EIP-2565) | **7,938,921** | +| MODEXP calls | 3,048 | +| — field inversions | **570** | +| — other (sq/mul/reduce) | 2,478 | +| MODEXP gas now (EIP-2565) | 5,152,500 (64% of verify) | +| MODEXP gas post-Fusaka (EIP-7883) | 47,860,440 | +| **projected verify post-Fusaka** | **50,646,861 (6.37×)** | +| per-tx cap (EIP-7825) | 16,777,216 | +| fits in 1 tx post-Fusaka? | **NO — ~3× over** | + +Inversions are **97%** of the post-Fusaka MODEXP gas (570 × 81,792 = 46.6M). + +### Where the inversions live (phase breakdown) +| phase | inversions | other | post-Fusaka MODEXP gas | +|-------|-----------:|------:|-----------------------:| +| on-curve check | 0 | 3 | 1,500 | +| scalar divisions (2× moddiv) | 2 | 2 | 164,584 | +| precompute table | 61 | 187 | 5,082,812 | +| **double-scalar-mult (ladder)** | **507** | 2,285 | **42,611,044** | +| final mod | 0 | 1 | 500 | + +The scalar-mult ladder holds 89% of inversions / 84% of post-Fusaka MODEXP gas. +The 61 precompute-table inversions include the **fixed-base** (generator G) +multiples, which are constant and could be hoisted to compile-time constants +(no witness, no calldata) — a free secondary win. + +## Findings that correct the prior analysis + +1. **"Switch to Jacobian/projective is the dominant win" — FALSE for this repo.** + The library is already affine + Strauss-Shamir with 6-bit precompute, and its + own header states projective was tried and was *worse* (~9M vs ~8M) in pure EVM, + because EVM trades cheap MODEXP inversions for many more mulmuls. Going + projective is not free wins here; it may even regress. + +2. **"~3× repricing" — understated.** For the 64-byte operand profile this library + uses, a single inversion goes 8,170 → 81,792 ≈ **10×**, not 3×. + +3. **Per-cert splitting is already implemented and is NOT sufficient.** `CertManager` + already pins the AWS root as a constant, caches verified intermediates, supports + skip-if-verified, and exposes `verifyCACert(cert, parentHash)` for per-cert txs. + But a *single* ECDSA verify is atomic and is itself ~50.6M post-Fusaka — ~3× the + cap. **You cannot split one signature check across transactions** without making + the scalar-mult ladder resumable (storing point state in SSTORE between txs — + expensive and ugly). So splitting helps the chain, not the unit. + +4. **The architecture levers in the prior writeup are already done** (root pinning, + intermediate caching, 2-verify hot path). The remaining problem is purely the + per-verify cost. + +## Conclusion + +Optimization is **mandatory**, not optional: post-Fusaka, no single P384 verify +fits in a Base transaction. The target is unambiguous — **eliminate the 570 +on-chain field inversions** (97% of post-Fusaka MODEXP gas). + +## Recommended next step + +→ Experiment 001: model caller-supplied inverse *witnesses* verified by one modmul +each (eliminates on-chain `modinv`). See `001-hinted-inversion-model.md`. + +## Artifacts +- `test/bench/Bench.t.sol::test_Baseline` +- `test/bench/ECDSA384Bench.sol` (instrumented census copy) diff --git a/bench/agent_log/001-hinted-inversion-model.md b/bench/agent_log/001-hinted-inversion-model.md new file mode 100644 index 0000000..ccb91e1 --- /dev/null +++ b/bench/agent_log/001-hinted-inversion-model.md @@ -0,0 +1,62 @@ +# 001 — Hinted inversions (analytical model) + +Date: 2026-06-02 +Status: ✅ modelled (empirically parameterized) — followed by 002/003 prototypes + +## Hypothesis + +Field inversion via Fermat (`a^(p-2) mod p`, a 64-byte-operand MODEXP) is 97% of +the post-Fusaka MODEXP gas. Inversion is *expensive to compute* but *cheap to +verify*: if the caller supplies `a_inv` as calldata, the contract confirms it with +a single modular multiply `a · a_inv ≡ 1 (mod p)`. All values are public +(verification, not signing), so there is no secrecy concern. Each `moddiv` changes +from {1 inversion + 1 mulmul} to {2 mulmuls}. + +Soundness: the witness MUST be fully constrained on-chain (`a·a_inv == 1 mod p`). +An unconstrained supplied inverse is a signature-forgery vector. + +## Model (parameterized by the measured census: 570 inv, 2478 other) + +`test/bench/Bench.t.sol::test_HintedInversionModel` + +| metric | value | +|--------|-------| +| non-MODEXP verify gas (unchanged) | 2,786,421 | +| hinted verify gas, EIP-2565 | **3,396,021** | +| hinted verify gas, EIP-7883 | **4,310,421** | +| + witness calldata (570×48 B, worst-case 16/B) | 437,760 | +| **= total post-Fusaka** | **4,748,181** | +| fits 1 tx post-Fusaka? | **YES** (cap 16,777,216) | +| witness bytes / verify | 27,360 | + +## Outcome + +- Post-Fusaka single verify: **50.6M → ~4.75M** (~11× reduction), comfortably under + the cap with ~3.5M of headroom for parsing/SHA-384/overhead. +- Bonus: at *current* pricing the hinted verify (~3.4M) is also ~2.3× cheaper than + today's 7.94M. +- Full cold chain (~7 verifies) → ~33M, splits per-cert into 2–3 txs, each well + under cap; hot path (2 verifies) ~10M fits a single tx. +- This makes the pure-EVM path viable and immune to the EIP-7883 *inversion* blow-up + without ZK. + +## Caveats / open questions for the prototype + +1. **Witness plumbing**: ~570 inverses must thread from calldata into each `moddiv` + site. Need a calldata layout + cursor; the offchain prover replays the verify to + emit them in order. +2. **Remaining 0x5 dependence**: the 3,048 remaining mulmuls/squarings still call + MODEXP at the 500-gas floor (~1.5M total post-Fusaka). Optional follow-up: + replace modexp-based reduction with pure-Yul Montgomery/Barrett to be *fully* + immune to 0x5 repricing. Probably unnecessary given the headroom — measure first. +3. **Calldata on L2**: 27 KB/verify also incurs Base L1-data-availability cost + (separate from execution gas). Worth a real cost estimate; blob calldata helps. +4. **Audit**: this modifies audited cryptographic core (`ECDSA384.sol`). Re-audit + the witness constraint and the existing `r` vs `r+n` edge case. + +## Follow-up + +- **002 (prototype)** completed a benchmark-only hinted verifier and measured real + overhead. +- **003** packed witnesses to 48 bytes and added malformed-hint tests. +- **004** should measure the hinted verifier inside the certificate / Nitro path. diff --git a/bench/agent_log/002-hinted-inversion-prototype.md b/bench/agent_log/002-hinted-inversion-prototype.md new file mode 100644 index 0000000..415b505 --- /dev/null +++ b/bench/agent_log/002-hinted-inversion-prototype.md @@ -0,0 +1,73 @@ +# 002 — Hinted inversions (prototype) + +Date: 2026-06-02 +Status: ✅ complete + +## Hypothesis + +The analytical hinted-inversion model from 001 should survive real Solidity +control flow: replace every Fermat inversion MODEXP with a caller-supplied inverse +witness, then constrain the witness on-chain with `a * a_inv == 1 mod m`. + +## Implementation + +Benchmark-only changes: + +- Added `ECDSA384Bench.verifyWithHints(...)`. +- Added a transient-storage hint cursor. +- Added inverse-hint collection for test replay. +- Added `BenchHarness.collectInverseHints(...)`. +- Added `BenchHarness.countHintedVerify(...)`. +- Added `BenchTest.test_HintedInversionPrototype()`. + +The first passing prototype used 64-byte witnesses because that matches Solarity's +internal two-word `U384` representation. This was intentionally not production +encoding; experiment 003 packs witnesses to 48 bytes. + +## Debug finding + +The first attempt failed with `bad inverse hint`. + +Root cause: scalar inversions are modulo the curve order `n`, while field +inversions are modulo the field prime `p`. The initial hint check verified every +inverse modulo `p`, which correctly rejected the two scalar inverse witnesses. + +Fix: add a modulus-specific multiplication check for `modinv(..., m_)`. + +## Results + +Command: + +```sh +forge test --match-path test/bench/Bench.t.sol --gas-report -vv +``` + +Prototype output with 64-byte witnesses: + +| metric | value | +|--------|------:| +| hinted verify gas, EIP-2565 | 5,440,654 | +| MODEXP calls | 3,048 | +| — field inversions | 0 | +| — other / checks | 3,048 | +| projected verify gas, EIP-7883 | 6,355,054 | +| + witness calldata, worst-case | 583,680 | +| **= total post-Fusaka** | **6,938,734** | +| fits 1 tx post-Fusaka? | YES | +| witness bytes / verify | 36,480 | + +## Outcome + +The hinted-inversion approach is empirically viable in a benchmark copy. Even +with unoptimized 64-byte witnesses and real witness-check overhead, one P384 +verify projects to about **6.94M** gas post-Fusaka including pessimistic calldata +gas, far below the 16,777,216 cap. + +## Recommended next step + +→ Experiment 003: pack witnesses to 48 bytes and add malformed-hint tests. + +## Artifacts + +- `test/bench/ECDSA384Bench.sol::verifyWithHints` +- `test/bench/Bench.t.sol::test_HintedInversionPrototype` diff --git a/bench/agent_log/003-48-byte-hints-negative-tests.md b/bench/agent_log/003-48-byte-hints-negative-tests.md new file mode 100644 index 0000000..2bb1361 --- /dev/null +++ b/bench/agent_log/003-48-byte-hints-negative-tests.md @@ -0,0 +1,100 @@ +# 003 — 48-byte hints and negative tests + +Date: 2026-06-02 +Status: ✅ complete + +## Hypothesis + +The hinted verifier should accept compact 48-byte P384 inverse witnesses while +still rejecting malformed witness streams. + +## Implementation + +Benchmark-only changes: + +- `collectInverseHints(...)` now writes each inverse as 48 big-endian bytes. +- `_nextInverseHint()` reconstructs Solarity's internal two-word `U384` + representation from each packed hint. +- Added malformed-hint tests: + - `test_HintedInversionRejectsMutatedHint` + - `test_HintedInversionRejectsTruncatedHints` + - `test_HintedInversionRejectsSurplusHints` + +## Results + +Command: + +```sh +forge test --match-path test/bench/Bench.t.sol --gas-report -vv +``` + +Prototype output with 48-byte witnesses: + +| metric | value | +|--------|------:| +| hinted verify gas, EIP-2565 | 5,391,727 | +| MODEXP calls | 3,048 | +| — field inversions | 0 | +| — other / checks | 3,048 | +| projected verify gas, EIP-7883 | 6,306,127 | +| + witness calldata, worst-case | 437,760 | +| **= total post-Fusaka** | **6,743,887** | +| fits 1 tx post-Fusaka? | YES | +| witness bytes / verify | 27,360 | + +Focused test results: + +- `test_Baseline`: pass +- `test_HintedInversionModel`: pass +- `test_HintedInversionPrototype`: pass +- `test_HintedInversionRejectsMutatedHint`: pass +- `test_HintedInversionRejectsTruncatedHints`: pass +- `test_HintedInversionRejectsSurplusHints`: pass + +Full suite: + +```sh +forge test --gas-report +``` + +Result: 32 passed, 0 failed. + +## Outcome + +The compact hinted verifier stays well under the post-Fusaka per-transaction cap. +The current prototype is about **6.74M gas per P384 verify** including pessimistic +calldata gas. This leaves roughly **10.0M gas** of margin under EIP-7825 for +certificate parsing, SHA-384, storage writes, and production API overhead. + +## Caveats + +- The implementation is benchmark-only and uses transient storage helpers for + collection/counters. +- The off-chain witness generator still needs to be built; the test collector + simulates replay-derived witnesses. +- The verifier still uses MODEXP for floor-priced modular multiplication / + reduction. That is acceptable for the cap, but it is not fully repricing-proof. +- The production API should not silently alter the existing verifier; it should + expose an explicit hinted verifier ABI. + +## Recommended next step + +→ Experiment 004: measure the hinted verifier inside the cert / Nitro path. + +Concrete plan: + +1. Add a benchmark-only `CertManager` variant that calls a hinted P384 verifier. +2. Use collected hints for one existing cert signature from `CertManager.t.sol`. +3. Measure `verifyCACert` and `verifyClientCert` current gas and projected + EIP-7883 gas. +4. Repeat for the hot `validateAttestation` path. +5. Decide whether the full per-cert transaction has enough margin after parsing, + SHA-384, calldata, and storage overhead. + +## Artifacts + +- `test/bench/ECDSA384Bench.sol` +- `test/bench/Bench.t.sol::test_HintedInversionPrototype` +- `test/bench/Bench.t.sol::test_HintedInversionRejectsMutatedHint` +- `test/bench/Bench.t.sol::test_HintedInversionRejectsTruncatedHints` +- `test/bench/Bench.t.sol::test_HintedInversionRejectsSurplusHints` diff --git a/bench/agent_log/004-real-attestation-hot-path.md b/bench/agent_log/004-real-attestation-hot-path.md new file mode 100644 index 0000000..842e485 --- /dev/null +++ b/bench/agent_log/004-real-attestation-hot-path.md @@ -0,0 +1,111 @@ +# 004 — Real attestation fixture and hot-path projection + +Date: 2026-06-02 +Status: ✅ complete + +## Question + +Does the provided real Nitro attestation validate against the current contracts, +and does the hinted-P384 direction still fit once real Nitro parsing, SHA-384, +certificate cache checks, and attestation signature verification are included? + +## Fixture finding + +The pasted Base64 sample has a 3-byte corruption in the CBOR payload: + +- The COSE payload byte-string header declares length `0x1116`. +- The `public_key` CBOR key is missing bytes `0x69 0x63 0x5f` (`"ic_"`), so it + appears as `publkey`. +- Because those 3 bytes are missing, the payload break (`0xff`) and signature + header (`0x58 0x60`) appear 3 bytes earlier than the declared COSE payload end. + +The benchmark fixture restores those missing bytes before validation. After that +normalization, the attestation validates successfully. + +Timestamp decoded from the attestation: + +- `2026-01-03T20:41:07.402Z` +- Foundry warp used: `1767472867` + +## Method + +Added: + +- `test/bench/RealAttestationBench.t.sol` + +The test: + +1. Decodes the provided Base64 in Solidity test code. +2. Repairs the missing `"ic_"` bytes in `public_key`. +3. Measures `decodeAttestationTbs`. +4. Measures cold `validateAttestation`. +5. Measures cached `validateAttestation` by calling it a second time, after CA / + client certs are stored in `CertManager`. +6. Projects the cached hot path under post-Fusaka unoptimized P384 and hinted P384. + +Projection constants from experiments 000/003: + +- Current P384 verify: `7,938,921` +- Unoptimized post-Fusaka P384 verify: `50,646,861` +- Hinted post-Fusaka P384 verify including worst-case 48-byte witness calldata: + `6,743,887` +- EIP-7825 transaction cap: `16,777,216` + +## Results + +Command: + +```sh +forge test --match-path test/bench/RealAttestationBench.t.sol -vv +``` + +Output: + +| metric | value | +|--------|------:| +| repaired attestation bytes | 4,482 | +| attestationTbs bytes | 4,395 | +| signature bytes | 96 | +| `decodeAttestationTbs` gas | 102,550 | +| `validateAttestation` gas, cold | 53,582,064 | +| `validateAttestation` gas, cached | 16,169,057 | +| cached post-Fusaka, unoptimized | 58,876,997 | +| cached post-Fusaka, hinted + calldata | 14,974,023 | +| cached hinted fits `16,777,216` cap? | YES | + +## Outcome + +The hinted-P384 path still looks viable after including real Nitro parsing, +SHA-384, certificate-cache checks, and the final attestation signature verify. +The steady-state hot path projects to **14.97M gas post-Fusaka**, including +pessimistic witness calldata, leaving about **1.80M gas** of margin under the +EIP-7825 cap. + +This is tighter than the isolated P384 benchmark, but still feasible. + +## Implications + +- Current cached validation is already close to the cap at **16.17M**; post-Fusaka + without hinted P384 is impossible. +- With hinted P384, the hot path can remain a single transaction if certificates + are already cached. +- Cold validation still needs to be split across cert transactions. The next + measurement should confirm each hinted per-cert transaction stays comfortably + below the cap. + +## Recommended next step + +→ Experiment 005: benchmark a hinted `CertManager` path. + +Concrete plan: + +1. Add a benchmark-only `CertManager` copy that accepts P384 inverse hints. +2. Measure `verifyCACertWithHints` and `verifyClientCertWithHints` on the certs + extracted from this real attestation. +3. Project and/or directly measure per-cert gas with 48-byte witnesses. +4. Confirm the cold chain can be submitted as sequential under-cap transactions. +5. Then estimate Base L1 data cost for the witness payloads. + +## Artifacts + +- `test/bench/RealAttestationBench.t.sol` diff --git a/bench/agent_log/005-real-attestation-per-cert-split.md b/bench/agent_log/005-real-attestation-per-cert-split.md new file mode 100644 index 0000000..7833ff4 --- /dev/null +++ b/bench/agent_log/005-real-attestation-per-cert-split.md @@ -0,0 +1,100 @@ +# 005 — Real attestation per-cert split projection + +Date: 2026-06-02 +Status: ✅ complete + +## Question + +If the real attestation cold path is split across certificate transactions, does +each transaction fit under the EIP-7825 cap once P384 verification is moved to the +hinted-inversion path? + +## Method + +Extended: + +- `test/bench/RealAttestationBench.t.sol` + +The test: + +1. Decodes and repairs the real Base64 attestation fixture from experiment 004. +2. Uses a benchmark-only parse harness to expose `_parseAttestation`. +3. Extracts each `cabundle` certificate and the leaf/client certificate. +4. Measures current `CertManager.verifyCACert` / `verifyClientCert` gas for each + cert transaction. +5. Projects each non-root certificate by replacing one current P384 verify with + the hinted post-Fusaka P384 cost including pessimistic witness calldata. + +Projection formula for certs that perform one P384 signature check: + +```text +projected = currentGas - 7,938,921 + 6,743,887 +``` + +The root cert transaction is already cached/pinned and performs no P384 verify, so +it is left unchanged. + +## Results + +Command: + +```sh +forge test --match-path test/bench/RealAttestationBench.t.sol -vv +``` + +Output: + +| step | current gas | projected hinted post-Fusaka | fits cap? | +|------|------------:|------------------------------:|:---------:| +| cabundle[0] root | 24,670 | 24,670 | YES | +| cabundle[1] | 9,304,997 | 8,109,963 | YES | +| cabundle[2] | 9,527,634 | 8,332,600 | YES | +| cabundle[3] | 9,288,116 | 8,093,082 | YES | +| client cert | 9,312,028 | 8,116,994 | YES | + +## Outcome + +The cold chain can be split into under-cap transactions with substantial margin. +Each non-root certificate transaction projects to roughly **8.1M-8.3M gas +post-Fusaka**, including pessimistic P384 witness calldata. + +Combined with experiment 004: + +- Cold path: split cert transactions fit individually. +- Cached hot path: final attestation validation projects to **14.97M**, also + under cap. + +This supports a pure-EVM architecture: + +1. Submit/cert-cache the AWS cabundle in order. +2. Submit the leaf/client cert with hints. +3. Submit the attestation validation with hints. + +## Caveats + +- This is still a projection for hinted cert verification, not a production + `CertManager.verifyCACertWithHints` implementation. +- The projection assumes one P384 verification per non-root cert, matching the + current `CertManager` flow. +- L1 data cost for witness payloads on Base is not included in execution gas and + should be priced separately. + +## Recommended next step + +→ Experiment 006: build a production-shaped hinted API sketch. + +Concrete plan: + +1. Define calldata ABI for `verifyCACertWithHints`, `verifyClientCertWithHints`, + and `validateAttestationWithHints`. +2. Decide whether hints are passed as one packed `bytes` stream per P384 verify or + grouped by certificate. +3. Implement a minimal off-chain witness generator that replays verification and + emits 48-byte inverses. +4. Replace the benchmark-only transient-storage hint cursor with normal calldata + cursor logic. +5. Re-run 004/005 using the production-shaped API. + +## Artifacts + +- `test/bench/RealAttestationBench.t.sol::test_RealAttestationPerCertSplitProjection` diff --git a/bench/agent_log/006-production-shaped-hinted-flow.md b/bench/agent_log/006-production-shaped-hinted-flow.md new file mode 100644 index 0000000..168b87b --- /dev/null +++ b/bench/agent_log/006-production-shaped-hinted-flow.md @@ -0,0 +1,126 @@ +# 006 - Production-shaped hinted flow + +Date: 2026-06-02 +Status: complete + +## Question + +Can the hinted-inversion P384 path be represented as a production-shaped API for +the split Nitro flow, and do the real attestation certificate and cached +attestation transactions still fit under the 16,777,216 gas cap after EIP-7883? + +## Method + +Extended benchmark-only code: + +- `test/bench/ECDSA384Bench.sol` +- `test/bench/HintedNitroBench.sol` +- `test/bench/RealAttestationBench.t.sol` + +Changes: + +1. Replaced the verifier-side transient-storage hint cursor with an explicit + memory cursor stored in the `U384Bench` call context. +2. Kept transient storage only in the benchmark collector that generates witness + bytes for tests. +3. Added production-shaped benchmark APIs: + - `verifyCACertWithHints(cert, parentCertHash, signatureHints)` + - `verifyClientCertWithHints(cert, parentCertHash, signatureHints)` + - `validateAttestationWithHints(attestationTbs, signature, attestationSigHints)` +4. Added `P384HintCollectorBench`, which emits one packed `bytes` stream per P384 + verification. Each inverse witness is exactly 48 bytes, big-endian, and + consumed sequentially. +5. Projected post-Fusaka gas from measured hinted EIP-2565 gas by adding: + +```text +hinted MODEXP floor calls * (500 - 200) + inverseHintBytes * 16 +``` + +The calldata term is pessimistic execution gas for all-nonzero witness bytes. + +## Results + +Commands: + +```sh +forge test --match-path test/bench/Bench.t.sol -vv +forge test --match-path test/bench/RealAttestationBench.t.sol -vv +forge test +forge test --gas-report +``` + +All tests passed: `37 passed, 0 failed`. + +### Isolated P384 + +| metric | value | +|--------|------:| +| current unhinted verify | 7,938,921 | +| projected unhinted post-Fusaka | 50,646,861 | +| current hinted verify | 4,688,649 | +| hinted MODEXP floor calls | 3,048 | +| projected hinted post-Fusaka, excluding calldata | 5,603,049 | +| inverse witness bytes | 27,360 | +| projected hinted post-Fusaka, including calldata | 6,040,809 | + +### Real attestation, production-shaped per-cert split + +| step | current hinted gas | hint bytes | hinted MODEXP floor calls | projected post-Fusaka | fits cap? | +|------|-------------------:|-----------:|--------------------------:|----------------------:|:---------:| +| cabundle[0] root | 25,230 | 0 | 0 | 25,230 | YES | +| cabundle[1] | 6,074,268 | 27,456 | 3,056 | 7,430,364 | YES | +| cabundle[2] | 6,309,056 | 27,408 | 3,052 | 7,663,184 | YES | +| cabundle[3] | 6,075,746 | 27,408 | 3,052 | 7,429,874 | YES | +| client cert | 6,094,419 | 27,504 | 3,060 | 7,452,483 | YES | + +### Real attestation, cached hot path + +| metric | value | +|--------|------:| +| current hinted cached gas | 12,987,344 | +| inverse hint bytes | 27,312 | +| hinted MODEXP floor calls | 3,044 | +| projected post-Fusaka | 14,337,536 | +| fits cap? | YES | + +## Outcome + +The production-shaped hinted flow improves the prior replacement projection and +stays comfortably under the EIP-7825 per-transaction cap: + +- Cold split cert transactions: about 7.43M-7.66M gas post-Fusaka. +- Cached hot attestation transaction: about 14.34M gas post-Fusaka. + +This confirms that the near-term pure-EVM path is viable if the Nitro flow is +split into cert-cache transactions plus a cached attestation validation +transaction. + +## Soundness notes + +- Each supplied inverse is constrained on-chain by `denominator * inverse == 1` + modulo the relevant modulus. +- Truncated, mutated, and surplus hints are rejected in the isolated P384 tests. +- The production-shaped attestation path has a surplus-hint negative test. +- The benchmark witness collector still uses transient storage, but that is only + the test oracle. The verifier-under-measure carries hint state in memory. + +## Caveats + +- This is still benchmark-only code. Production `src/` remains untouched. +- The API currently takes `bytes memory`, matching existing contract style. A + deployment version should consider `bytes calldata` for external methods. +- The witness generator is not yet an off-chain CLI; it is a benchmark helper + that replays the verification and emits the exact packed inverse stream. +- Base L1 data fees for witness payloads are not included in execution gas. + +## Recommended next step + +Experiment 007 should turn this into an integration sketch: + +1. Add production interfaces for hinted cert caching and hinted attestation + validation. +2. Implement an off-chain witness generator CLI that emits one hint blob per + cert signature and one hint blob for the COSE attestation signature. +3. Price Base calldata/L1 data cost for the 27KB witness blobs. +4. Add a sequencing benchmark or script for the full cold flow: + root -> intermediate CAs -> client cert -> cached attestation. diff --git a/bench/agent_log/007-full-cold-warm-sequence.md b/bench/agent_log/007-full-cold-warm-sequence.md new file mode 100644 index 0000000..39fa371 --- /dev/null +++ b/bench/agent_log/007-full-cold-warm-sequence.md @@ -0,0 +1,151 @@ +# 007 - Full cold and warm hinted sequence + +Date: 2026-06-02 +Status: complete + +## Question + +Can we demonstrate the full Nitro attestation path as the transactions we would +submit on-chain, and distinguish the cold cert-cache setup from the warm-cache +reuse path? + +## Method + +Extended: + +- `test/bench/RealAttestationBench.t.sol` + +Added: + +- `test_007_FullColdAndWarmHintedSequence` + +The test starts from a fresh `HintedCertManagerBench`, whose constructor has only +the AWS Nitro root pinned. It then: + +1. Decodes the real attestation fixture into `attestationTbs` and `signature`. +2. Parses cabundle pointers from the attestation payload. +3. Confirms `cabundle[0]` is the pinned AWS Nitro root cert. +4. Submits the minimum cold sequence: + - non-root CA cert 1 + - non-root CA cert 2 + - non-root CA cert 3 + - client/leaf cert + - final attestation validation +5. Submits the warm-cache validation again as one transaction. + +Projection formula remains: + +```text +postFusakaGas = currentHintedGas + + hinted MODEXP floor calls * (500 - 200) + + inverseHintBytes * 16 +``` + +The final term is pessimistic execution gas for all-nonzero hint calldata. + +## Cold vs warm cache + +Cold cache means the non-root certificate chain and leaf/client cert have not +yet been verified into `CertManager.verified`. + +Warm cache means those cert hashes are already stored in `CertManager.verified` +and are still valid at `block.timestamp`. A later attestation can reuse the +cached certs if it uses the exact same DER cert bytes and the same cached +leaf/client certificate. The final attestation signature still needs its own +P384 verification and its own hint blob. + +The AWS Nitro root is not a cold-cache transaction in this repo because it is +pinned in `CertManager` at deployment. If we also submit a root no-op check for +operational symmetry, this fixture becomes 6 transactions instead of the minimum +5. + +## Results + +Command: + +```sh +forge test --match-path test/bench/RealAttestationBench.t.sol -vv +``` + +All focused tests passed: `6 passed, 0 failed`. + +Full suite: + +```sh +forge test +forge test --gas-report +``` + +Full suite passed: `38 passed, 0 failed`. + +### Cold sequence + +Minimum transaction count for this fixture: **5**. + +| tx | action | current hinted gas | hint bytes | hinted MODEXP floor calls | projected post-Fusaka | fits cap? | +|----|--------|-------------------:|-----------:|--------------------------:|----------------------:|:---------:| +| 1 | cache CA cert | 6,074,285 | 27,456 | 3,056 | 7,430,381 | YES | +| 2 | cache CA cert | 6,309,132 | 27,408 | 3,052 | 7,663,260 | YES | +| 3 | cache CA cert | 6,075,884 | 27,408 | 3,052 | 7,430,012 | YES | +| 4 | cache client cert | 6,094,655 | 27,504 | 3,060 | 7,452,719 | YES | +| 5 | validate attestation | 12,990,821 | 27,312 | 3,044 | 14,341,013 | YES | + +Cold sequence totals: + +| metric | value | +|--------|------:| +| current gas total | 37,544,777 | +| projected post-Fusaka total | 44,317,385 | +| max projected tx gas | 14,341,013 | +| per-tx cap | 16,777,216 | + +### Warm sequence + +Warm-cache transaction count: **1**. + +| tx | action | current hinted gas | hint bytes | hinted MODEXP floor calls | projected post-Fusaka | fits cap? | +|----|--------|-------------------:|-----------:|--------------------------:|----------------------:|:---------:| +| 1 | warm validate | 12,947,764 | 27,312 | 3,044 | 14,297,956 | YES | + +## Outcome + +The full cold path is under the EIP-7825 cap transaction-by-transaction after +EIP-7883, using the hinted P384 path: + +- Minimum cold flow for this fixture: **5 transactions**. +- Optional root no-op replay: **6 transactions**. +- Warm-cache flow: **1 transaction**. +- Largest projected transaction: **14.34M gas**, leaving about **2.44M gas** of + headroom under the 16.78M cap. + +The important architectural conclusion is that the full cold flow does not need +one giant transaction. The cert chain is converted into durable on-chain cache +state first, and the final validation transaction then reuses that cache. + +## Audit notes + +- The root cert is trusted only because its hash/pubkey are pinned by the + deployed `CertManager` constructor. +- Every non-root cert transaction verifies the parent is cached, unexpired, a CA, + and has remaining path length before writing the child cert to cache. +- The client cert is cached as non-CA and rejected if later loaded as a CA. +- The final validation still parses the attestation, validates required fields, + reloads the cert bundle through `verifyCertBundle`, and verifies the COSE + attestation signature. +- Cached cert reuse is constrained by exact `keccak256(cert)` identity and + `notAfter >= block.timestamp`. +- Hint blobs do not relax cryptographic checks; each inverse is constrained by + modular multiplication and surplus hints are rejected. + +## Recommended next step + +Experiment 008 should move from benchmark-only wrappers to production candidate +code and audit docs: + +1. Add production hinted interfaces while preserving the existing unhinted API. +2. Port the explicit-memory hint cursor into a production `ECDSA384` variant. +3. Add negative tests at the full-flow level: mutated cert hint, truncated cert + hint, wrong parent hash, expired cached cert, CA/client role mismatch, and + final attestation wrong-key rejection. +4. Start `docs/hinted-p384-nitro-attestation.md` with the threat model, ABI, + witness format, sequence diagrams, and invariants. diff --git a/bench/agent_log/008-production-candidate-audit-docs.md b/bench/agent_log/008-production-candidate-audit-docs.md new file mode 100644 index 0000000..af990a6 --- /dev/null +++ b/bench/agent_log/008-production-candidate-audit-docs.md @@ -0,0 +1,159 @@ +# 008 - Production candidate and audit docs + +Date: 2026-06-02 +Status: complete + +## Question + +Can the benchmark-only hinted flow be moved into additive production candidate +contracts, with audit-oriented documentation and full-flow negative tests? + +## Method + +Added production candidate files: + +- `src/ECDSA384Hinted.sol` +- `src/IHintedCertManager.sol` +- `src/CertManagerHinted.sol` +- `src/NitroValidatorHinted.sol` + +Added audit documentation: + +- `docs/hinted-p384-nitro-attestation.md` + +Updated benchmark tests: + +- `test/bench/RealAttestationBench.t.sol` + +The original unhinted contracts remain unchanged. The hinted path is additive. + +## Implementation Summary + +`ECDSA384Hinted` is a production candidate derived from the benchmark verifier: + +- verifier-side transient-storage hint cursor removed, +- benchmark counters removed, +- benchmark inverse collector removed, +- hint state carried in the P384 call context memory, +- every supplied inverse constrained with modular multiplication, +- truncated and surplus hint streams rejected. + +`CertManagerHinted` adds: + +- `verifyCACertWithHints` +- `verifyClientCertWithHints` +- `loadVerified` + +`NitroValidatorHinted` adds: + +- `validateAttestationWithHints` + +The hinted validator uses empty hint streams for cert bundle checks: + +```text +verifyCACertWithHints(cert, parentHash, "") +verifyClientCertWithHints(cert, parentHash, "") +``` + +Cached certs return before signature verification. Missing certs reach hinted +P384 verification with an empty stream and revert, which prevents accidental +fallback to the unhinted P384 path. + +## Results + +Focused command: + +```sh +forge test --match-path test/bench/RealAttestationBench.t.sol -vv +``` + +Focused result: `13 passed, 0 failed`. + +Full verification: + +```sh +forge test +forge test --gas-report +``` + +Full suite result: `45 passed, 0 failed`. + +### Production Candidate Full Sequence + +| tx | action | current hinted gas | hint bytes | hinted MODEXP floor calls | projected post-Fusaka | fits cap? | +|----|--------|-------------------:|-----------:|--------------------------:|----------------------:|:---------:| +| 1 | cache CA cert | 5,424,159 | 27,456 | 3,056 | 6,780,255 | YES | +| 2 | cache CA cert | 5,659,854 | 27,408 | 3,052 | 7,013,982 | YES | +| 3 | cache CA cert | 5,426,606 | 27,408 | 3,052 | 6,780,734 | YES | +| 4 | cache client cert | 5,443,681 | 27,504 | 3,060 | 6,801,745 | YES | +| 5 | validate attestation | 12,351,354 | 27,312 | 3,044 | 13,701,546 | YES | + +Cold sequence totals: + +| metric | value | +|--------|------:| +| current gas total | 34,305,654 | +| projected post-Fusaka total | 41,078,262 | +| max projected tx gas | 13,701,546 | +| per-tx cap | 16,777,216 | + +Warm validation: + +| tx | action | current hinted gas | hint bytes | hinted MODEXP floor calls | projected post-Fusaka | fits cap? | +|----|--------|-------------------:|-----------:|--------------------------:|----------------------:|:---------:| +| 1 | warm validate | 12,308,297 | 27,312 | 3,044 | 13,658,489 | YES | + +## Negative Tests Added + +The production candidate rejects: + +- mutated cert hint: `bad inverse hint`, +- truncated cert hint: `inverse hint underflow`, +- wrong parent hash: `parent cert unverified`, +- expired cached cert: `cert expired`, +- cached CA/client role mismatch: `cert is not a CA`, +- missing warm cache: `inverse hint underflow`, +- invalid final attestation signature, +- surplus final attestation hint: `unused inverse hints`. + +## Deployment Caveat + +The production candidate is audit-friendly but not yet deployment-size optimized. +Gas-report deployment sizes: + +| contract | deployed size | +|----------|--------------:| +| `CertManager` | 25,315 bytes | +| `CertManagerHinted` | 30,947 bytes | +| `NitroValidatorHinted` | 25,745 bytes | + +This needs a dedicated size-reduction pass before Base deployment. The most +likely direction is moving P384 verification behind a shared verifier contract or +linked-library boundary so it is not embedded into multiple large contracts. + +## Outcome + +The production candidate preserves the post-Fusaka result with better measured +gas than the benchmark wrapper: + +- minimum cold flow: 5 transactions, +- optional root no-op flow: 6 transactions, +- warm flow: 1 transaction, +- largest projected transaction: 13.70M gas. + +The audit documentation now has the initial threat model, ABI, witness format, +invariants, cold/warm cache semantics, failure modes, gas table, and deployment +caveat. + +## Recommended Next Step + +Experiment 009 should focus on deployability and audit hardening: + +1. Reduce deployed bytecode size below chain limits. +2. Decide whether P384 should be an external verifier contract, linked library, + or split CertManager/NitroValidator deployment. +3. Add equivalence tests between `ECDSA384.verify` and `ECDSA384Hinted.verify` + across multiple signatures. +4. Turn the Solidity witness collector into an off-chain CLI. +5. Continue expanding the audit doc with exact code references and sequence + diagrams. diff --git a/bench/agent_log/009-deployable-external-p384-verifier.md b/bench/agent_log/009-deployable-external-p384-verifier.md new file mode 100644 index 0000000..7c0ae43 --- /dev/null +++ b/bench/agent_log/009-deployable-external-p384-verifier.md @@ -0,0 +1,160 @@ +# 009 - Deployable external P384 verifier + +Date: 2026-06-02 +Status: complete + +## Question + +Can the hinted production candidate be made deployable under EIP-170 while +preserving the post-Fusaka split-flow gas result? + +## Method + +The 008 contracts embedded hinted P384 verification into both the cert manager +and the attestation validator. That kept the code easy to audit, but left the +hinted contracts over the runtime bytecode limit. + +This experiment split P384 into a shared external verifier and made the hinted +manager/validator hinted-only: + +- `src/IP384Verifier.sol` +- `src/P384Verifier.sol` +- `src/CertManagerHintedExternal.sol` +- `src/NitroValidatorHintedExternal.sol` + +The canonical hinted names now wrap the externalized variants: + +- `src/CertManagerHinted.sol` +- `src/NitroValidatorHinted.sol` + +The benchmark test now deploys: + +```text +P384Verifier +CertManagerHinted(P384Verifier) +NitroValidatorHinted(CertManagerHinted, P384Verifier) +``` + +`CertManagerHinted` still implements `ICertManager` through +`IHintedCertManager`, but its unhinted `verifyCACert` and `verifyClientCert` +entrypoints intentionally revert with `use hinted cert verification`. + +## Results + +Source-only size command: + +```sh +forge build src --sizes +``` + +Deployable runtime sizes: + +| contract | runtime size | EIP-170 margin | +|----------|-------------:|---------------:| +| `P384Verifier` | 7,805 bytes | 16,771 bytes | +| `CertManagerHinted` | 18,496 bytes | 6,080 bytes | +| `NitroValidatorHinted` | 13,101 bytes | 11,475 bytes | + +The `test/bench` instrumentation contracts remain oversized and are intentionally +excluded from the deployable source-size check. + +Focused sequence command: + +```sh +forge test --match-path test/bench/RealAttestationBench.t.sol --match-test test_007_FullColdAndWarmHintedSequence -vv +``` + +Focused result: `1 passed, 0 failed`. + +Full verification: + +```sh +forge test +``` + +Full suite result: `47 passed, 0 failed`. + +Gas-report command: + +```sh +forge test --gas-report --match-path test/bench/RealAttestationBench.t.sol +``` + +Gas-report result: `15 passed, 0 failed`. + +### Deployable Full Sequence + +| tx | action | current hinted gas | hint bytes | hinted MODEXP floor calls | projected post-Fusaka | fits cap? | +|----|--------|-------------------:|-----------:|--------------------------:|----------------------:|:---------:| +| 1 | cache CA cert | 5,441,446 | 27,456 | 3,056 | 6,797,542 | YES | +| 2 | cache CA cert | 5,674,610 | 27,408 | 3,052 | 7,028,738 | YES | +| 3 | cache CA cert | 5,441,378 | 27,408 | 3,052 | 6,795,506 | YES | +| 4 | cache client cert | 5,458,159 | 27,504 | 3,060 | 6,816,223 | YES | +| 5 | validate attestation | 12,330,471 | 27,312 | 3,044 | 13,680,663 | YES | + +Cold sequence totals: + +| metric | value | +|--------|------:| +| current gas total | 34,346,064 | +| projected post-Fusaka total | 41,118,672 | +| max projected tx gas | 13,680,663 | +| per-tx cap | 16,777,216 | + +Warm validation: + +| tx | action | current hinted gas | hint bytes | hinted MODEXP floor calls | projected post-Fusaka | fits cap? | +|----|--------|-------------------:|-----------:|--------------------------:|----------------------:|:---------:| +| 1 | warm validate | 12,287,414 | 27,312 | 3,044 | 13,637,606 | YES | + +## Tests Added + +- `test_009_DeployableHintedContractsFitEIP170` +- `test_009_DeployableCertManagerDisablesUnhintedEntrypoints` + +These supplement the 008 negative tests for: + +- mutated cert hints, +- truncated cert hints, +- wrong parent hashes, +- expired cached certs, +- cached CA/client role mismatch, +- missing warm cache, +- invalid final attestation signatures, +- surplus final attestation hints. + +## Soundness Notes + +- Externalizing P384 does not trust the verifier caller. Hints are still consumed + inside `ECDSA384Hinted` and each inverse is constrained before use. +- `CertManagerHinted` disables the inherited unhinted interface methods, so a + caller cannot accidentally route through the old MODEXP-heavy path on the + hinted manager. +- `NitroValidatorHinted` has no unhinted validation method. Its cached cert + bundle checks still pass empty hint streams, which means missing cache entries + fail rather than falling back to cert signature verification. +- The external verifier address is immutable in both `CertManagerHinted` and + `NitroValidatorHinted`. +- The external call adds a small gas cost, but the max projected transaction + remains 3.10M gas below the 16.78M cap. + +## Outcome + +The deployable hinted architecture satisfies both constraints measured so far: + +- every deployable hinted runtime is below EIP-170, +- the full real-attestation cold and warm flows remain below the EIP-7825 + per-transaction cap after the EIP-7883 projection. + +## Recommended Next Step + +Experiment 010 should focus on turning the Solidity witness oracle into an +off-chain witness generator and hardening equivalence tests: + +1. Add a CLI that parses a DER cert or COSE attestation signature and emits the + exact 48-byte inverse hint stream expected by `P384Verifier`. +2. Cross-check CLI output against `P384HintCollectorBench` for the real fixture. +3. Add direct equivalence tests for `P384Verifier` vs the unhinted verifier on + known-good and known-bad signatures. +4. Expand `docs/hinted-p384-nitro-attestation.md` with code-reference tables for + each invariant before audit handoff. diff --git a/bench/agent_log/010-offchain-witness-generator.md b/bench/agent_log/010-offchain-witness-generator.md new file mode 100644 index 0000000..2e2ea4a --- /dev/null +++ b/bench/agent_log/010-offchain-witness-generator.md @@ -0,0 +1,143 @@ +# 010 - Off-chain witness generator + +Date: 2026-06-02 +Status: complete + +## Question + +Can the Solidity-only witness oracle be replaced with an off-chain generator that +emits the exact 48-byte inverse hint stream expected by `P384Verifier`? + +## Method + +Added: + +- `bench/p384_hints.js` + +The generator is dependency-free Node.js: + +- uses built-in `BigInt` for P384 arithmetic, +- uses built-in `crypto` for SHA-384, +- mirrors the Solarity affine Strauss-Shamir P384 verification order, +- records every modular inverse denominator in the same order as + `ECDSA384Hinted`, +- emits each inverse as exactly 48-byte big-endian bytes. + +Supported commands: + +```sh +node bench/p384_hints.js verify --hash <0xhash> --signature <0xr_s> --pubkey <0xxy> +node bench/p384_hints.js cert --cert <0xder|base64|@file> --pubkey <0xparent_xy> +node bench/p384_hints.js attestation --attestation <0xcose|base64|@file> --pubkey <0xleaf_xy> +``` + +Added optional FFI cross-check: + +- `test_010_OffchainWitnessGeneratorMatchesSolidityCollector` + +The test is skipped in normal `forge test` runs. Enable it with: + +```sh +NITRO_RUN_FFI=true forge test --ffi \ + --match-path test/bench/RealAttestationBench.t.sol \ + --match-test test_010_OffchainWitnessGeneratorMatchesSolidityCollector -vv +``` + +## Results + +Syntax check: + +```sh +node --check bench/p384_hints.js +``` + +Result: pass. + +Default focused test: + +```sh +forge test --match-path test/bench/RealAttestationBench.t.sol --match-test test_010 -vv +``` + +Result: pass, with the FFI check skipped unless `NITRO_RUN_FFI=true`. + +Enabled FFI cross-check: + +```sh +NITRO_RUN_FFI=true forge test --ffi \ + --match-path test/bench/RealAttestationBench.t.sol \ + --match-test test_010_OffchainWitnessGeneratorMatchesSolidityCollector -vv +``` + +Result: `1 passed, 0 failed`. + +The enabled test checked 5 real-fixture signatures: + +| signature | source | +|-----------|--------| +| 1 | non-root CA cert | +| 2 | non-root CA cert | +| 3 | non-root CA cert | +| 4 | client/leaf cert | +| 5 | COSE attestation | + +For each signature, the Node generator output matched +`P384HintCollectorBench` byte-for-byte. The generated hints were then used to +cache the cert chain and validate the Nitro attestation through the deployable +hinted contracts. + +Final attestation hint stream size: `27,312` bytes. + +Full suite: + +```sh +forge test +``` + +Result: `48 passed, 0 failed`. + +Source-only deployability check: + +```sh +forge build src --sizes +``` + +Result: pass; hinted runtime sizes unchanged from 009. + +## Soundness Notes + +- The off-chain generator is not trusted by the contracts. A malicious or buggy + generator can only produce hints that are accepted if every inverse satisfies + the on-chain `denominator * inverse == 1 mod m` check. +- The generator must still match the verifier's deterministic hint order. The + FFI test proves this for the real fixture against the Solidity collector. +- The `cert` mode parses DER TBS and ECDSA `r,s`; the `attestation` mode + reconstructs Nitro's COSE `Sig_structure`. Both paths converge to the same + `verify` hint generator. +- The default test suite does not require FFI, so CI and normal local tests do + not depend on Node process execution. + +## Outcome + +The witness-generation loop is now practical: + +1. Generate hints off-chain with `bench/p384_hints.js`. +2. Submit cert-cache transactions with cert signature hints. +3. Submit the warm attestation validation transaction with attestation signature + hints. +4. Use the optional FFI test to prove local generator equivalence before audit or + deployment rehearsals. + +## Recommended Next Step + +Experiment 011 should harden audit evidence around equivalence and negative +behavior: + +1. Add direct `P384Verifier` equivalence tests against the original unhinted + verifier for known-good and known-bad signatures. +2. Add CLI negative tests outside Solidity for malformed DER, malformed COSE, + wrong pubkey, wrong signature, and truncated inputs. +3. Add code-reference tables to `docs/hinted-p384-nitro-attestation.md` mapping + every audit invariant to the exact Solidity and JS locations. +4. Consider a small deployment rehearsal script that prints the cold/warm + transaction sequence with generated hints and expected calldata sizes. diff --git a/bench/p384_hints.js b/bench/p384_hints.js new file mode 100644 index 0000000..4eb7b4b --- /dev/null +++ b/bench/p384_hints.js @@ -0,0 +1,624 @@ +#!/usr/bin/env node +"use strict"; + +const crypto = require("crypto"); +const fs = require("fs"); + +const P = hexToBigInt("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff"); +const N = hexToBigInt("ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973"); +const LOW_S_MAX = N - 1n; +const A = hexToBigInt("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc"); +const B = hexToBigInt("b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef"); +const GX = hexToBigInt("aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7"); +const GY = hexToBigInt("3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f"); +const MASK_256 = (1n << 256n) - 1n; + +main(); + +function main() { + try { + const { command, options } = parseArgs(process.argv.slice(2)); + let hints; + + if (command === "verify") { + hints = collectVerifyHints( + readBytes(options, "hash"), + readBytes(options, "signature"), + readBytes(options, "pubkey"), + ); + } else if (command === "cert") { + const cert = readBytes(options, "cert"); + const pubKey = readBytes(options, "pubkey"); + const { hash, signature } = parseCertSignature(cert); + hints = collectVerifyHints(hash, signature, pubKey); + } else if (command === "attestation") { + const attestation = readBytes(options, "attestation"); + const pubKey = readBytes(options, "pubkey"); + const { hash, signature } = parseAttestationSignature(attestation); + hints = collectVerifyHints(hash, signature, pubKey); + } else { + usage(); + process.exit(2); + } + + process.stdout.write(`0x${Buffer.concat(hints).toString("hex")}`); + } catch (err) { + process.stderr.write(`${err.stack || err.message}\n`); + process.exit(1); + } +} + +function usage() { + process.stderr.write(`Usage: + node bench/p384_hints.js verify --hash --signature --pubkey + node bench/p384_hints.js cert --cert --pubkey + node bench/p384_hints.js attestation --attestation --pubkey + +Inputs may be 0x-prefixed hex, base64, or @path. Output is a 0x-prefixed packed +stream of 48-byte big-endian inverse hints. +`); +} + +function parseArgs(args) { + const command = args[0] || ""; + const options = {}; + + for (let i = 1; i < args.length; i += 2) { + const key = args[i]; + const value = args[i + 1]; + if (!key || !key.startsWith("--") || value === undefined) { + usage(); + process.exit(2); + } + options[key.slice(2)] = value; + } + + return { command, options }; +} + +function readBytes(options, name) { + const value = options[name]; + if (value === undefined) { + throw new Error(`missing --${name}`); + } + return decodeBytes(value); +} + +function decodeBytes(value) { + let raw = value; + if (raw.startsWith("@")) { + raw = fs.readFileSync(raw.slice(1), "utf8").trim(); + } + + if (raw.startsWith("0x") || raw.startsWith("0X")) { + const hex = raw.slice(2); + if (hex.length % 2 !== 0 || /[^0-9a-f]/i.test(hex)) { + throw new Error("invalid hex input"); + } + return Buffer.from(hex, "hex"); + } + + if (/^[0-9a-f]+$/i.test(raw) && raw.length % 2 === 0) { + return Buffer.from(raw, "hex"); + } + + return Buffer.from(raw, "base64"); +} + +function collectVerifyHints(hashBytes, signatureBytes, pubKeyBytes) { + if (signatureBytes.length !== 96) { + throw new Error(`signature must be 96 bytes, got ${signatureBytes.length}`); + } + if (pubKeyBytes.length !== 96) { + throw new Error(`pubkey must be 96 bytes, got ${pubKeyBytes.length}`); + } + if (hashBytes.length > 48) { + throw new Error(`hash must be at most 48 bytes, got ${hashBytes.length}`); + } + + const ctx = { hints: [] }; + const r = bytesToBigInt(signatureBytes.subarray(0, 48)); + const s = bytesToBigInt(signatureBytes.subarray(48, 96)); + const pubX = bytesToBigInt(pubKeyBytes.subarray(0, 48)); + const pubY = bytesToBigInt(pubKeyBytes.subarray(48, 96)); + + if (r === 0n || r >= N || s === 0n || s > LOW_S_MAX) { + throw new Error("signature rejected by scalar bounds"); + } + if (!isOnCurve(pubX, pubY)) { + throw new Error("pubkey is not on P384"); + } + + const paddedHash = leftPad(hashBytes, 48); + const h = bytesToBigInt(paddedHash); + let scalar1 = modDiv(ctx, h, s, N); + const scalar2 = modDiv(ctx, r, s, N); + + const points = precomputePointsTable(ctx, pubX, pubY); + const result = doubleScalarMultiplication(ctx, points, scalar1, scalar2); + scalar1 = mod(result.x, N); + + if (scalar1 !== r) { + throw new Error("signature verification failed"); + } + + return ctx.hints; +} + +function isOnCurve(x, y) { + if (x === 0n || x === P || y === 0n || y === P) { + return false; + } + + let rhs = modPow(x, 3n, P); + if (A !== 0n) { + rhs = modAdd(rhs, modMul(x, A, P), P); + } + if (B !== 0n) { + rhs = modAdd(rhs, B, P); + } + + return modPow(y, 2n, P) === rhs; +} + +function precomputePointsTable(ctx, hx, hy) { + const points = Array.from({ length: 64 }, () => ({ x: 0n, y: 0n })); + points[0x01] = { x: hx, y: hy }; + points[0x08] = { x: GX, y: GY }; + + for (let i = 0; i < 8; ++i) { + for (let j = 0; j < 8; ++j) { + if (i + j < 2) { + continue; + } + + const maskTo = (i << 3) | j; + if (i !== 0) { + const maskFrom = ((i - 1) << 3) | j; + points[maskTo] = addAffine(ctx, points[maskFrom], { x: GX, y: GY }); + } else { + const maskFrom = (i << 3) | (j - 1); + points[maskTo] = addAffine(ctx, points[maskFrom], { x: hx, y: hy }); + } + } + } + + return points; +} + +function doubleScalarMultiplication(ctx, points, scalar1, scalar2) { + let x = 0n; + let y = 0n; + const scalar1High = scalar1 >> 256n; + const scalar2High = scalar2 >> 256n; + const scalar1Low = scalar1 & MASK_256; + const scalar2Low = scalar2 & MASK_256; + + ({ x, y } = twiceAffine(ctx, { x, y })); + + let mask = Number((scalar1High >> 183n) << 3n) | Number(scalar2High >> 183n); + if (mask !== 0) { + ({ x, y } = addAffine(ctx, points[mask], { x, y })); + } + + for (let word = 4; word <= 184; word += 3) { + ({ x, y } = twice3Affine(ctx, { x, y })); + + const shift = BigInt(184 - word); + mask = Number(((scalar1High >> shift) & 0x07n) << 3n) | Number((scalar2High >> shift) & 0x07n); + + if (mask !== 0) { + ({ x, y } = addAffine(ctx, points[mask], { x, y })); + } + } + + ({ x, y } = twiceAffine(ctx, { x, y })); + + mask = Number((scalar1Low >> 255n) << 3n) | Number(scalar2Low >> 255n); + if (mask !== 0) { + ({ x, y } = addAffine(ctx, points[mask], { x, y })); + } + + for (let word = 4; word <= 256; word += 3) { + ({ x, y } = twice3Affine(ctx, { x, y })); + + const shift = BigInt(256 - word); + mask = Number(((scalar1Low >> shift) & 0x07n) << 3n) | Number((scalar2Low >> shift) & 0x07n); + + if (mask !== 0) { + ({ x, y } = addAffine(ctx, points[mask], { x, y })); + } + } + + return { x, y }; +} + +function twiceAffine(ctx, point) { + const x1 = point.x; + const y1 = point.y; + if (x1 === 0n || y1 === 0n) { + return { x: 0n, y: 0n }; + } + + let m1 = modPow(x1, 2n, P); + m1 = modMul(m1, 3n, P); + m1 = modAdd(m1, A, P); + + const m2 = modShl1(y1, P); + m1 = modDiv(ctx, m1, m2, P); + + const x2 = modSub(modSub(modPow(m1, 2n, P), x1, P), x1, P); + const y2 = modSub(modMul(modSub(x1, x2, P), m1, P), y1, P); + + return { x: x2, y: y2 }; +} + +function twice3Affine(ctx, point) { + const x1Start = point.x; + const y1Start = point.y; + if (x1Start === 0n || y1Start === 0n) { + return { x: 0n, y: 0n }; + } + + let m1 = modPow(x1Start, 2n, P); + m1 = modAdd(modMul(m1, 3n, P), A, P); + + let m2 = modShl1(y1Start, P); + m1 = modDiv(ctx, m1, m2, P); + + let x2 = modSub(modSub(modPow(m1, 2n, P), x1Start, P), x1Start, P); + let y2 = modSub(modMul(modSub(x1Start, x2, P), m1, P), y1Start, P); + + if (y2 === 0n) { + return { x: 0n, y: 0n }; + } + + m1 = modPow(x2, 2n, P); + m1 = modAdd(modMul(m1, 3n, P), A, P); + + m2 = modShl1(y2, P); + m1 = modDiv(ctx, m1, m2, P); + + let x1 = modSub(modSub(modPow(m1, 2n, P), x2, P), x2, P); + let y1 = modSub(modMul(modSub(x2, x1, P), m1, P), y2, P); + + if (y1 === 0n) { + return { x: 0n, y: 0n }; + } + + m1 = modPow(x1, 2n, P); + m1 = modAdd(modMul(m1, 3n, P), A, P); + + m2 = modShl1(y1, P); + m1 = modDiv(ctx, m1, m2, P); + + x2 = modSub(modSub(modPow(m1, 2n, P), x1, P), x1, P); + y2 = modSub(modMul(modSub(x1, x2, P), m1, P), y1, P); + + return { x: x2, y: y2 }; +} + +function addAffine(ctx, point1, point2) { + const x1 = point1.x; + const y1 = point1.y; + const x2 = point2.x; + const y2 = point2.y; + + if (x1 === 0n || x2 === 0n) { + if (x1 === 0n && x2 === 0n) { + return { x: 0n, y: 0n }; + } + return x1 === 0n ? { x: x2, y: y2 } : { x: x1, y: y1 }; + } + + if (x1 === x2) { + if (y1 === y2) { + return twiceAffine(ctx, point1); + } + return { x: 0n, y: 0n }; + } + + let m1 = modSub(y1, y2, P); + const m2 = modSub(x1, x2, P); + m1 = modDiv(ctx, m1, m2, P); + + const x3 = modSub(modSub(modPow(m1, 2n, P), x1, P), x2, P); + const y3 = modSub(modMul(modSub(x1, x3, P), m1, P), y1, P); + + return { x: x3, y: y3 }; +} + +function modDiv(ctx, a, b, m) { + const inv = recordInverse(ctx, b, m); + return modMul(a, inv, m); +} + +function recordInverse(ctx, value, modulus) { + const normalized = mod(value, modulus); + if (normalized === 0n) { + throw new Error("cannot invert zero"); + } + + const inverse = modInv(normalized, modulus); + ctx.hints.push(bigIntToFixedBuffer(inverse, 48)); + return inverse; +} + +function mod(value, modulus) { + const result = value % modulus; + return result >= 0n ? result : result + modulus; +} + +function modAdd(a, b, m) { + const sum = a + b; + return sum >= m ? sum - m : sum; +} + +function modSub(a, b, m) { + return a >= b ? a - b : a + m - b; +} + +function modMul(a, b, m) { + return (a * b) % m; +} + +function modShl1(a, m) { + const shifted = a << 1n; + return shifted >= m ? shifted - m : shifted; +} + +function modPow(base, exponent, modulus) { + let result = 1n; + let b = mod(base, modulus); + let e = exponent; + while (e > 0n) { + if (e & 1n) { + result = (result * b) % modulus; + } + b = (b * b) % modulus; + e >>= 1n; + } + return result; +} + +function modInv(value, modulus) { + let low = mod(value, modulus); + let high = modulus; + let lm = 1n; + let hm = 0n; + + while (low > 1n) { + const ratio = high / low; + const nm = hm - lm * ratio; + const nw = high - low * ratio; + hm = lm; + high = low; + lm = nm; + low = nw; + } + + if (low !== 1n) { + throw new Error("inverse does not exist"); + } + + return mod(lm, modulus); +} + +function parseCertSignature(cert) { + const root = readAsn1(cert, 0); + requireTag(root, 0x30, "certificate"); + + const tbs = readAsn1(cert, root.contentStart); + const sigAlgo = readAsn1(cert, tbs.end); + const sig = readAsn1(cert, sigAlgo.end); + requireTag(sig, 0x03, "certificate signature bit string"); + if (cert[sig.contentStart] !== 0x00) { + throw new Error("unsupported nonzero signature unused-bits count"); + } + + const sigRoot = readAsn1(cert, sig.contentStart + 1); + requireTag(sigRoot, 0x30, "ECDSA signature"); + const rNode = readAsn1(cert, sigRoot.contentStart); + const sNode = readAsn1(cert, rNode.end); + + const r = parseAsn1Integer(cert.subarray(rNode.contentStart, rNode.contentEnd)); + const s = parseAsn1Integer(cert.subarray(sNode.contentStart, sNode.contentEnd)); + const hash = crypto.createHash("sha384").update(cert.subarray(tbs.start, tbs.end)).digest(); + + return { hash, signature: Buffer.concat([r, s]) }; +} + +function parseAsn1Integer(bytes) { + let value = bytes; + while (value.length > 0 && value[0] === 0x00) { + value = value.subarray(1); + } + if (value.length > 48) { + throw new Error(`ASN.1 integer exceeds 48 bytes: ${value.length}`); + } + return leftPad(value, 48); +} + +function readAsn1(bytes, start) { + if (start >= bytes.length) { + throw new Error("ASN.1 read out of bounds"); + } + + const tag = bytes[start]; + const { length, lengthBytes } = readDerLength(bytes, start + 1); + const contentStart = start + 1 + lengthBytes; + const contentEnd = contentStart + length; + if (contentEnd > bytes.length) { + throw new Error("ASN.1 length out of bounds"); + } + + return { tag, start, contentStart, contentEnd, end: contentEnd }; +} + +function readDerLength(bytes, offset) { + const first = bytes[offset]; + if (first === undefined) { + throw new Error("missing DER length"); + } + if (first < 0x80) { + return { length: first, lengthBytes: 1 }; + } + + const count = first & 0x7f; + if (count === 0 || count > 4) { + throw new Error("unsupported DER length"); + } + if (offset + count >= bytes.length) { + throw new Error("DER length out of bounds"); + } + + let length = 0; + for (let i = 0; i < count; ++i) { + length = (length << 8) | bytes[offset + 1 + i]; + } + + return { length, lengthBytes: 1 + count }; +} + +function requireTag(node, tag, label) { + if (node.tag !== tag) { + throw new Error(`${label} has unexpected ASN.1 tag 0x${node.tag.toString(16)}`); + } +} + +function parseAttestationSignature(attestation) { + let offset = 1; + if (attestation[0] === 0xd2) { + offset = 2; + } + + const protectedPtr = readCborItem(attestation, offset); + requireCborMajor(protectedPtr, 2, "protected header"); + const unprotectedPtr = readCborItem(attestation, protectedPtr.end); + requireCborMajor(unprotectedPtr, 5, "unprotected header"); + const payloadPtr = readCborItem(attestation, unprotectedPtr.end); + requireCborMajor(payloadPtr, 2, "payload"); + const signaturePtr = readCborItem(attestation, payloadPtr.end); + requireCborMajor(signaturePtr, 2, "signature"); + + const rawProtectedBytes = attestation.subarray(offset, protectedPtr.end); + const rawPayloadBytes = attestation.subarray(unprotectedPtr.end, payloadPtr.end); + const attestationTbs = Buffer.concat([ + Buffer.from([0x84, 0x6a]), + Buffer.from("Signature1", "ascii"), + rawProtectedBytes, + Buffer.from([0x40]), + rawPayloadBytes, + ]); + + return { + hash: crypto.createHash("sha384").update(attestationTbs).digest(), + signature: attestation.subarray(signaturePtr.contentStart, signaturePtr.end), + }; +} + +function readCborItem(bytes, start) { + const initial = bytes[start]; + if (initial === undefined) { + throw new Error("CBOR read out of bounds"); + } + const major = initial >> 5; + const ai = initial & 0x1f; + const { value, headerLength } = readCborValue(bytes, start + 1, ai); + const contentStart = start + headerLength; + let end; + + if (major === 2 || major === 3) { + end = contentStart + Number(value); + } else if (major === 4) { + end = contentStart; + for (let i = 0n; i < value; ++i) { + end = readCborItem(bytes, end).end; + } + } else if (major === 5) { + end = contentStart; + for (let i = 0n; i < value * 2n; ++i) { + end = readCborItem(bytes, end).end; + } + } else if (major === 0 || major === 1 || major === 6 || major === 7) { + end = contentStart; + } else { + throw new Error(`unsupported CBOR major type ${major}`); + } + + if (end > bytes.length) { + throw new Error("CBOR item length out of bounds"); + } + + return { major, ai, value, start, contentStart, end }; +} + +function readCborValue(bytes, offset, ai) { + if (ai < 24) { + return { value: BigInt(ai), headerLength: 1 }; + } + if (ai === 24) { + return { value: BigInt(bytes[offset]), headerLength: 2 }; + } + if (ai === 25) { + return { value: BigInt(readUintBE(bytes, offset, 2)), headerLength: 3 }; + } + if (ai === 26) { + return { value: BigInt(readUintBE(bytes, offset, 4)), headerLength: 5 }; + } + if (ai === 27) { + return { value: readBigUintBE(bytes, offset, 8), headerLength: 9 }; + } + throw new Error(`unsupported CBOR additional information ${ai}`); +} + +function requireCborMajor(node, major, label) { + if (node.major !== major) { + throw new Error(`${label} has unexpected CBOR major type ${node.major}`); + } +} + +function readUintBE(bytes, offset, length) { + let value = 0; + for (let i = 0; i < length; ++i) { + value = value * 256 + bytes[offset + i]; + } + return value; +} + +function readBigUintBE(bytes, offset, length) { + let value = 0n; + for (let i = 0; i < length; ++i) { + value = value * 256n + BigInt(bytes[offset + i]); + } + return value; +} + +function leftPad(buffer, length) { + if (buffer.length > length) { + throw new Error(`value length ${buffer.length} exceeds ${length}`); + } + if (buffer.length === length) { + return Buffer.from(buffer); + } + return Buffer.concat([Buffer.alloc(length - buffer.length), buffer]); +} + +function bytesToBigInt(bytes) { + if (bytes.length === 0) { + return 0n; + } + return BigInt(`0x${Buffer.from(bytes).toString("hex")}`); +} + +function bigIntToFixedBuffer(value, length) { + const hex = value.toString(16).padStart(length * 2, "0"); + if (hex.length > length * 2) { + throw new Error(`integer exceeds ${length} bytes`); + } + return Buffer.from(hex, "hex"); +} + +function hexToBigInt(hex) { + return BigInt(`0x${hex}`); +} diff --git a/src/CertManagerHinted.sol b/src/CertManagerHinted.sol new file mode 100644 index 0000000..1c7410a --- /dev/null +++ b/src/CertManagerHinted.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {CertManagerHintedExternal} from "./CertManagerHintedExternal.sol"; +import {IP384Verifier} from "./IP384Verifier.sol"; + +/// @notice Deployable hinted Nitro certificate manager. +/// @dev Kept as the canonical hinted name; P384 verification is delegated to `p384Verifier`. +contract CertManagerHinted is CertManagerHintedExternal { + constructor(IP384Verifier p384Verifier_) CertManagerHintedExternal(p384Verifier_) {} +} diff --git a/src/CertManagerHintedExternal.sol b/src/CertManagerHintedExternal.sol new file mode 100644 index 0000000..2e4324f --- /dev/null +++ b/src/CertManagerHintedExternal.sol @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {Sha2Ext} from "./Sha2Ext.sol"; +import {Asn1Decode, Asn1Ptr, LibAsn1Ptr} from "./Asn1Decode.sol"; +import {IHintedCertManager} from "./IHintedCertManager.sol"; +import {IP384Verifier} from "./IP384Verifier.sol"; +import {LibBytes} from "./LibBytes.sol"; + +/// @notice Hinted-only Nitro certificate cache that keeps P384 verification in an external verifier contract. +/// @dev The unhinted ICertManager methods are intentionally disabled to avoid accidental MODEXP-heavy fallbacks. +contract CertManagerHintedExternal is IHintedCertManager { + using Asn1Decode for bytes; + using LibAsn1Ptr for Asn1Ptr; + using LibBytes for bytes; + + event CertVerified(bytes32 indexed certHash); + + bytes32 internal constant ROOT_CA_CERT_HASH = 0x311d96fcd5c5e0ccf72ef548e2ea7d4c0cd53ad7c4cc49e67471aed41d61f185; + uint64 internal constant ROOT_CA_CERT_NOT_AFTER = 2519044085; + int64 internal constant ROOT_CA_CERT_MAX_PATH_LEN = -1; + bytes32 internal constant ROOT_CA_CERT_SUBJECT_HASH = + 0x3c3e2e5f1dd14dee5db88341ba71521e939afdb7881aa24c9f1e1c007a2fa8b6; + bytes internal constant ROOT_CA_CERT_PUB_KEY = + hex"fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4"; + + bytes32 internal constant CERT_ALGO_OID = 0x53ce037f0dfaa43ef13b095f04e68a6b5e3f1519a01a3203a1e6440ba915b87e; + bytes32 internal constant EC_PUB_KEY_OID = 0xb60fee1fd85f867dd7c8d16884a49a20287ebe4c0fb49294e9825988aa8e42b4; + bytes32 internal constant SECP_384_R1_OID = 0xbd74344bb507daeb9ed315bc535f24a236ccab72c5cd6945fb0efe5c037e2097; + bytes32 internal constant BASIC_CONSTRAINTS_OID = + 0x6351d72a43cb42fb9a2531a28608c278c89629f8f025b5f5dc705f3fe45e950a; + bytes32 internal constant KEY_USAGE_OID = 0x45529d8772b07ebd6d507a1680da791f4a2192882bf89d518801579f7a5167d2; + + IP384Verifier public immutable p384Verifier; + mapping(bytes32 => bytes) private verified; + + constructor(IP384Verifier p384Verifier_) { + require(address(p384Verifier_) != address(0), "missing P384 verifier"); + p384Verifier = p384Verifier_; + _saveVerified( + ROOT_CA_CERT_HASH, + VerifiedCert({ + ca: true, + notAfter: ROOT_CA_CERT_NOT_AFTER, + maxPathLen: ROOT_CA_CERT_MAX_PATH_LEN, + subjectHash: ROOT_CA_CERT_SUBJECT_HASH, + pubKey: ROOT_CA_CERT_PUB_KEY + }) + ); + } + + function verifyCACert(bytes memory, bytes32) external pure returns (bytes32) { + revert("use hinted cert verification"); + } + + function verifyClientCert(bytes memory, bytes32) external pure returns (VerifiedCert memory) { + revert("use hinted cert verification"); + } + + function verifyCACertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) + external + returns (bytes32) + { + bytes32 certHash = keccak256(cert); + _verifyCertWithHints(cert, certHash, true, _loadVerified(parentCertHash), signatureHints); + return certHash; + } + + function verifyClientCertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) + external + returns (VerifiedCert memory) + { + return _verifyCertWithHints(cert, keccak256(cert), false, _loadVerified(parentCertHash), signatureHints); + } + + function loadVerified(bytes32 certHash) external view returns (VerifiedCert memory) { + return _loadVerified(certHash); + } + + function _verifyCertWithHints( + bytes memory certificate, + bytes32 certHash, + bool ca, + VerifiedCert memory parent, + bytes memory signatureHints + ) internal returns (VerifiedCert memory cert) { + if (certHash != ROOT_CA_CERT_HASH) { + require(parent.pubKey.length > 0, "parent cert unverified"); + require(parent.notAfter >= block.timestamp, "parent cert expired"); + require(parent.ca, "parent cert is not a CA"); + require(!ca || parent.maxPathLen != 0, "maxPathLen exceeded"); + } + + cert = _loadVerified(certHash); + if (cert.pubKey.length != 0) { + require(cert.notAfter >= block.timestamp, "cert expired"); + require(cert.ca == ca, "cert is not a CA"); + return cert; + } + + Asn1Ptr root = certificate.root(); + Asn1Ptr tbsCertPtr = certificate.firstChildOf(root); + (uint64 notAfter, int64 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey) = + _parseTbs(certificate, tbsCertPtr, ca); + + require(parent.subjectHash == issuerHash, "issuer / subject mismatch"); + + if (parent.maxPathLen > 0 && (maxPathLen < 0 || maxPathLen >= parent.maxPathLen)) { + maxPathLen = parent.maxPathLen - 1; + } + + _verifyCertSignatureWithHints(certificate, tbsCertPtr, parent.pubKey, signatureHints); + + cert = VerifiedCert({ + ca: ca, notAfter: notAfter, maxPathLen: maxPathLen, subjectHash: subjectHash, pubKey: pubKey + }); + _saveVerified(certHash, cert); + + emit CertVerified(certHash); + } + + function _parseTbs(bytes memory certificate, Asn1Ptr ptr, bool ca) + internal + view + returns (uint64 notAfter, int64 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey) + { + Asn1Ptr versionPtr = certificate.firstChildOf(ptr); + Asn1Ptr vPtr = certificate.firstChildOf(versionPtr); + Asn1Ptr serialPtr = certificate.nextSiblingOf(versionPtr); + Asn1Ptr sigAlgoPtr = certificate.nextSiblingOf(serialPtr); + + require(certificate.keccak(sigAlgoPtr.content(), sigAlgoPtr.length()) == CERT_ALGO_OID, "invalid cert sig algo"); + require(certificate.uintAt(vPtr) == 2, "version should be 3"); + + (notAfter, maxPathLen, issuerHash, subjectHash, pubKey) = _parseTbsInner(certificate, sigAlgoPtr, ca); + } + + function _parseTbsInner(bytes memory certificate, Asn1Ptr sigAlgoPtr, bool ca) + internal + view + returns (uint64 notAfter, int64 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey) + { + Asn1Ptr issuerPtr = certificate.nextSiblingOf(sigAlgoPtr); + issuerHash = certificate.keccak(issuerPtr.content(), issuerPtr.length()); + Asn1Ptr validityPtr = certificate.nextSiblingOf(issuerPtr); + Asn1Ptr subjectPtr = certificate.nextSiblingOf(validityPtr); + subjectHash = certificate.keccak(subjectPtr.content(), subjectPtr.length()); + Asn1Ptr subjectPublicKeyInfoPtr = certificate.nextSiblingOf(subjectPtr); + Asn1Ptr extensionsPtr = certificate.nextSiblingOf(subjectPublicKeyInfoPtr); + + if (certificate[extensionsPtr.header()] == 0x81) { + extensionsPtr = certificate.nextSiblingOf(extensionsPtr); + } + if (certificate[extensionsPtr.header()] == 0x82) { + extensionsPtr = certificate.nextSiblingOf(extensionsPtr); + } + + notAfter = _verifyValidity(certificate, validityPtr); + maxPathLen = _verifyExtensions(certificate, extensionsPtr, ca); + pubKey = _parsePubKey(certificate, subjectPublicKeyInfoPtr); + } + + function _parsePubKey(bytes memory certificate, Asn1Ptr subjectPublicKeyInfoPtr) + internal + pure + returns (bytes memory subjectPubKey) + { + Asn1Ptr pubKeyAlgoPtr = certificate.firstChildOf(subjectPublicKeyInfoPtr); + Asn1Ptr pubKeyAlgoIdPtr = certificate.firstChildOf(pubKeyAlgoPtr); + Asn1Ptr algoParamsPtr = certificate.nextSiblingOf(pubKeyAlgoIdPtr); + Asn1Ptr subjectPublicKeyPtr = certificate.nextSiblingOf(pubKeyAlgoPtr); + Asn1Ptr subjectPubKeyPtr = certificate.bitstring(subjectPublicKeyPtr); + + require( + certificate.keccak(pubKeyAlgoIdPtr.content(), pubKeyAlgoIdPtr.length()) == EC_PUB_KEY_OID, + "invalid cert algo id" + ); + require( + certificate.keccak(algoParamsPtr.content(), algoParamsPtr.length()) == SECP_384_R1_OID, + "invalid cert algo param" + ); + + uint256 end = subjectPubKeyPtr.content() + subjectPubKeyPtr.length(); + subjectPubKey = certificate.slice(end - 96, 96); + } + + function _verifyValidity(bytes memory certificate, Asn1Ptr validityPtr) internal view returns (uint64 notAfter) { + Asn1Ptr notBeforePtr = certificate.firstChildOf(validityPtr); + Asn1Ptr notAfterPtr = certificate.nextSiblingOf(notBeforePtr); + + uint256 notBefore = certificate.timestampAt(notBeforePtr); + notAfter = uint64(certificate.timestampAt(notAfterPtr)); + + require(notBefore <= block.timestamp, "certificate not valid yet"); + require(notAfter >= block.timestamp, "certificate not valid anymore"); + } + + function _verifyExtensions(bytes memory certificate, Asn1Ptr extensionsPtr, bool ca) + internal + pure + returns (int64 maxPathLen) + { + require(certificate[extensionsPtr.header()] == 0xa3, "invalid extensions"); + extensionsPtr = certificate.firstChildOf(extensionsPtr); + Asn1Ptr extensionPtr = certificate.firstChildOf(extensionsPtr); + uint256 end = extensionsPtr.content() + extensionsPtr.length(); + bool basicConstraintsFound = false; + bool keyUsageFound = false; + maxPathLen = -1; + + while (true) { + Asn1Ptr oidPtr = certificate.firstChildOf(extensionPtr); + bytes32 oid = certificate.keccak(oidPtr.content(), oidPtr.length()); + + if (oid == BASIC_CONSTRAINTS_OID || oid == KEY_USAGE_OID) { + Asn1Ptr valuePtr = certificate.nextSiblingOf(oidPtr); + + if (certificate[valuePtr.header()] == 0x01) { + require(valuePtr.length() == 1, "invalid critical bool value"); + valuePtr = certificate.nextSiblingOf(valuePtr); + } + + valuePtr = certificate.octetString(valuePtr); + + if (oid == BASIC_CONSTRAINTS_OID) { + basicConstraintsFound = true; + maxPathLen = _verifyBasicConstraintsExtension(certificate, valuePtr, ca); + } else { + keyUsageFound = true; + _verifyKeyUsageExtension(certificate, valuePtr, ca); + } + } + + if (extensionPtr.content() + extensionPtr.length() == end) { + break; + } + extensionPtr = certificate.nextSiblingOf(extensionPtr); + } + + require(basicConstraintsFound, "basicConstraints not found"); + require(keyUsageFound, "keyUsage not found"); + require(ca || maxPathLen == -1, "maxPathLen must be undefined for client cert"); + } + + function _verifyBasicConstraintsExtension(bytes memory certificate, Asn1Ptr valuePtr, bool ca) + internal + pure + returns (int64 maxPathLen) + { + maxPathLen = -1; + Asn1Ptr basicConstraintsPtr = certificate.firstChildOf(valuePtr); + bool isCA; + if (certificate[basicConstraintsPtr.header()] == 0x01) { + require(basicConstraintsPtr.length() == 1, "invalid isCA bool value"); + isCA = certificate[basicConstraintsPtr.content()] == 0xff; + basicConstraintsPtr = certificate.nextSiblingOf(basicConstraintsPtr); + } + require(ca == isCA, "isCA must be true for CA certs"); + if (certificate[basicConstraintsPtr.header()] == 0x02) { + maxPathLen = int64(uint64(certificate.uintAt(basicConstraintsPtr))); + } + } + + function _verifyKeyUsageExtension(bytes memory certificate, Asn1Ptr valuePtr, bool ca) internal pure { + uint256 value = certificate.bitstringUintAt(valuePtr); + if (ca) { + require(value & 0x04 == 0x04, "CertSign must be present"); + } else { + require(value & 0x80 == 0x80, "DigitalSignature must be present"); + } + } + + function _verifyCertSignatureWithHints( + bytes memory certificate, + Asn1Ptr ptr, + bytes memory pubKey, + bytes memory signatureHints + ) internal { + Asn1Ptr sigAlgoPtr = certificate.nextSiblingOf(ptr); + require(certificate.keccak(sigAlgoPtr.content(), sigAlgoPtr.length()) == CERT_ALGO_OID, "invalid cert sig algo"); + + bytes memory hash = Sha2Ext.sha384(certificate, ptr.header(), ptr.totalLength()); + bytes memory sigPacked = _certSignature(certificate, sigAlgoPtr); + + require(p384Verifier.verifyP384SignatureWithHints(hash, sigPacked, pubKey, signatureHints), "invalid sig"); + } + + function _certSignature(bytes memory certificate, Asn1Ptr sigAlgoPtr) + internal + pure + returns (bytes memory sigPacked) + { + Asn1Ptr sigPtr = certificate.nextSiblingOf(sigAlgoPtr); + Asn1Ptr sigBPtr = certificate.bitstring(sigPtr); + Asn1Ptr sigRoot = certificate.rootOf(sigBPtr); + Asn1Ptr sigRPtr = certificate.firstChildOf(sigRoot); + Asn1Ptr sigSPtr = certificate.nextSiblingOf(sigRPtr); + (uint128 rhi, uint256 rlo) = certificate.uint384At(sigRPtr); + (uint128 shi, uint256 slo) = certificate.uint384At(sigSPtr); + sigPacked = abi.encodePacked(rhi, rlo, shi, slo); + } + + function _saveVerified(bytes32 certHash, VerifiedCert memory cert) internal { + verified[certHash] = abi.encodePacked(cert.ca, cert.notAfter, cert.maxPathLen, cert.subjectHash, cert.pubKey); + } + + function _loadVerified(bytes32 certHash) internal view returns (VerifiedCert memory) { + bytes memory packed = verified[certHash]; + if (packed.length == 0) { + return VerifiedCert({ca: false, notAfter: 0, maxPathLen: 0, subjectHash: 0, pubKey: ""}); + } + uint8 ca; + uint64 notAfter; + int64 maxPathLen; + bytes32 subjectHash; + assembly { + ca := mload(add(packed, 0x1)) + notAfter := mload(add(packed, 0x9)) + maxPathLen := mload(add(packed, 0x11)) + subjectHash := mload(add(packed, 0x31)) + } + bytes memory pubKey = packed.slice(0x31, packed.length - 0x31); + return VerifiedCert({ + ca: ca != 0, notAfter: notAfter, maxPathLen: maxPathLen, subjectHash: subjectHash, pubKey: pubKey + }); + } +} diff --git a/src/ECDSA384Hinted.sol b/src/ECDSA384Hinted.sol new file mode 100644 index 0000000..faf5680 --- /dev/null +++ b/src/ECDSA384Hinted.sol @@ -0,0 +1,1047 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {MemoryUtils} from "@solarity/libs/utils/MemoryUtils.sol"; + +/** + * @notice Cryptography module + * + * This library provides functionality for ECDSA verification over any 384-bit curve. Currently, + * this is the most efficient implementation out there, consuming ~8.025 million gas per call. + * + * The approach is Strauss-Shamir double scalar multiplication with 6 bits of precompute + affine coordinates. + * For reference, naive implementation uses ~400 billion gas, which is 50000 times more expensive. + * + * We also tried using projective coordinates, however, the gas consumption rose to ~9 million gas. + */ +library ECDSA384Hinted { + using MemoryUtils for *; + using U384Hinted for *; + + /** + * @notice 384-bit curve parameters. + */ + struct Parameters { + bytes a; + bytes b; + bytes gx; + bytes gy; + bytes p; + bytes n; + bytes lowSmax; + } + + struct _Parameters { + uint256 a; + uint256 b; + uint256 gx; + uint256 gy; + uint256 p; + uint256 n; + uint256 lowSmax; + } + + struct _Inputs { + uint256 r; + uint256 s; + uint256 x; + uint256 y; + } + + /** + * @notice The function to verify the ECDSA signature + * @param curveParams_ the 384-bit curve parameters. `lowSmax` is `n / 2`. + * @param hashedMessage_ the already hashed message to be verified. + * @param signature_ the ECDSA signature. Equals to `bytes(r) + bytes(s)`. + * @param pubKey_ the full public key of a signer. Equals to `bytes(x) + bytes(y)`. + * + * Note that signatures only from the lower part of the curve are accepted. + * If your `s > n / 2`, change it to `s = n - s`. + */ + function verify( + Parameters memory curveParams_, + bytes memory hashedMessage_, + bytes memory signature_, + bytes memory pubKey_ + ) internal returns (bool) { + (bool ok_,) = _verify(curveParams_, hashedMessage_, signature_, pubKey_, "", false); + return ok_; + } + + function verifyWithHints( + Parameters memory curveParams_, + bytes memory hashedMessage_, + bytes memory signature_, + bytes memory pubKey_, + bytes memory inverseHints_ + ) internal returns (bool ok_) { + uint256 consumed_; + (ok_, consumed_) = _verify(curveParams_, hashedMessage_, signature_, pubKey_, inverseHints_, true); + require(consumed_ == inverseHints_.length, "unused inverse hints"); + } + + function verifyWithHintsConsumed( + Parameters memory curveParams_, + bytes memory hashedMessage_, + bytes memory signature_, + bytes memory pubKey_, + bytes memory inverseHints_ + ) internal returns (bool ok_, uint256 consumed_) { + return _verify(curveParams_, hashedMessage_, signature_, pubKey_, inverseHints_, true); + } + + function _verify( + Parameters memory curveParams_, + bytes memory hashedMessage_, + bytes memory signature_, + bytes memory pubKey_, + bytes memory inverseHints_, + bool useHints_ + ) private returns (bool ok_, uint256 consumed_) { + unchecked { + _Inputs memory inputs_; + + (inputs_.r, inputs_.s) = U384Hinted.init2(signature_); + (inputs_.x, inputs_.y) = U384Hinted.init2(pubKey_); + + _Parameters memory params_ = _Parameters({ + a: curveParams_.a.init(), + b: curveParams_.b.init(), + gx: curveParams_.gx.init(), + gy: curveParams_.gy.init(), + p: curveParams_.p.init(), + n: curveParams_.n.init(), + lowSmax: curveParams_.lowSmax.init() + }); + + uint256 call = + useHints_ ? U384Hinted.initCallWithHints(params_.p, inverseHints_) : U384Hinted.initCall(params_.p); + + /// accept s only from the lower part of the curve + if ( + U384Hinted.eqInteger(inputs_.r, 0) || U384Hinted.cmp(inputs_.r, params_.n) >= 0 + || U384Hinted.eqInteger(inputs_.s, 0) || U384Hinted.cmp(inputs_.s, params_.lowSmax) > 0 + ) { + return (false, U384Hinted.hintCursor(call)); + } + + if (!_isOnCurve(call, params_.p, params_.a, params_.b, inputs_.x, inputs_.y)) { + return (false, U384Hinted.hintCursor(call)); + } + + /// allow compatibility with non-384-bit hash functions. + { + uint256 hashedMessageLength_ = hashedMessage_.length; + + if (hashedMessageLength_ < 48) { + bytes memory tmp_ = new bytes(48); + + MemoryUtils.unsafeCopy( + hashedMessage_.getDataPointer(), + tmp_.getDataPointer() + 48 - hashedMessageLength_, + hashedMessageLength_ + ); + + hashedMessage_ = tmp_; + } + } + + uint256 scalar1 = U384Hinted.moddiv(call, hashedMessage_.init(), inputs_.s, params_.n); + uint256 scalar2 = U384Hinted.moddiv(call, inputs_.r, inputs_.s, params_.n); + + { + uint256 three = U384Hinted.init(3); + + /// We use 6-bit masks where the first 3 bits refer to `scalar1` and the last 3 bits refer to `scalar2`. + uint256[2][64] memory points_ = _precomputePointsTable( + call, params_.p, three, params_.a, params_.gx, params_.gy, inputs_.x, inputs_.y + ); + + (scalar1,) = _doubleScalarMultiplication(call, params_.p, three, params_.a, points_, scalar1, scalar2); + } + + U384Hinted.modAssign(call, scalar1, params_.n); + + return (U384Hinted.eq(scalar1, inputs_.r), U384Hinted.hintCursor(call)); + } + } + + /** + * @dev Check if a point in affine coordinates is on the curve. + */ + function _isOnCurve(uint256 call, uint256 p, uint256 a, uint256 b, uint256 x, uint256 y) private returns (bool) { + unchecked { + if (U384Hinted.eqInteger(x, 0) || U384Hinted.eq(x, p) || U384Hinted.eqInteger(y, 0) || U384Hinted.eq(y, p)) + { + return false; + } + + uint256 LHS = U384Hinted.modexp(call, y, 2); + uint256 RHS = U384Hinted.modexp(call, x, 3); + + if (!U384Hinted.eqInteger(a, 0)) { + RHS = U384Hinted.modadd(RHS, U384Hinted.modmul(call, x, a), p); // x^3 + a*x + } + + if (!U384Hinted.eqInteger(b, 0)) { + RHS = U384Hinted.modadd(RHS, b, p); // x^3 + a*x + b + } + + return U384Hinted.eq(LHS, RHS); + } + } + + /** + * @dev Compute the Strauss-Shamir double scalar multiplication scalar1*G + scalar2*H. + */ + function _doubleScalarMultiplication( + uint256 call, + uint256 p, + uint256 three, + uint256 a, + uint256[2][64] memory points, + uint256 scalar1, + uint256 scalar2 + ) private returns (uint256 x, uint256 y) { + unchecked { + uint256 mask_; + uint256 scalar1Bits_; + uint256 scalar2Bits_; + + assembly { + scalar1Bits_ := mload(scalar1) + scalar2Bits_ := mload(scalar2) + } + + (x, y) = _twiceAffine(call, p, three, a, x, y); + + mask_ = ((scalar1Bits_ >> 183) << 3) | (scalar2Bits_ >> 183); + + if (mask_ != 0) { + (x, y) = _addAffine(call, p, three, a, points[mask_][0], points[mask_][1], x, y); + } + + for (uint256 word = 4; word <= 184; word += 3) { + (x, y) = _twice3Affine(call, p, three, a, x, y); + + mask_ = (((scalar1Bits_ >> (184 - word)) & 0x07) << 3) | ((scalar2Bits_ >> (184 - word)) & 0x07); + + if (mask_ != 0) { + (x, y) = _addAffine(call, p, three, a, points[mask_][0], points[mask_][1], x, y); + } + } + + assembly { + scalar1Bits_ := mload(add(scalar1, 0x20)) + scalar2Bits_ := mload(add(scalar2, 0x20)) + } + + (x, y) = _twiceAffine(call, p, three, a, x, y); + + mask_ = ((scalar1Bits_ >> 255) << 3) | (scalar2Bits_ >> 255); + + if (mask_ != 0) { + (x, y) = _addAffine(call, p, three, a, points[mask_][0], points[mask_][1], x, y); + } + + for (uint256 word = 4; word <= 256; word += 3) { + (x, y) = _twice3Affine(call, p, three, a, x, y); + + mask_ = (((scalar1Bits_ >> (256 - word)) & 0x07) << 3) | ((scalar2Bits_ >> (256 - word)) & 0x07); + + if (mask_ != 0) { + (x, y) = _addAffine(call, p, three, a, points[mask_][0], points[mask_][1], x, y); + } + } + } + } + + /** + * @dev Double an elliptic curve point in affine coordinates. + */ + function _twiceAffine(uint256 call, uint256 p, uint256 three, uint256 a, uint256 x1, uint256 y1) + private + returns (uint256 x2, uint256 y2) + { + unchecked { + if (x1 == 0) { + return (0, 0); + } + + if (U384Hinted.eqInteger(y1, 0)) { + return (0, 0); + } + + uint256 m1 = U384Hinted.modexp(call, x1, 2); + U384Hinted.modmulAssign(call, m1, three); + U384Hinted.modaddAssign(m1, a, p); + + uint256 m2 = U384Hinted.modshl1(y1, p); + U384Hinted.moddivAssign(call, m1, m2); + + x2 = U384Hinted.modexp(call, m1, 2); + U384Hinted.modsubAssign(x2, x1, p); + U384Hinted.modsubAssign(x2, x1, p); + + y2 = U384Hinted.modsub(x1, x2, p); + U384Hinted.modmulAssign(call, y2, m1); + U384Hinted.modsubAssign(y2, y1, p); + } + } + + /** + * @dev Doubles an elliptic curve point 3 times in affine coordinates. + */ + function _twice3Affine(uint256 call, uint256 p, uint256 three, uint256 a, uint256 x1, uint256 y1) + private + returns (uint256 x2, uint256 y2) + { + unchecked { + if (x1 == 0) { + return (0, 0); + } + + if (U384Hinted.eqInteger(y1, 0)) { + return (0, 0); + } + + uint256 m1 = U384Hinted.modexp(call, x1, 2); + U384Hinted.modmulAssign(call, m1, three); + U384Hinted.modaddAssign(m1, a, p); + + uint256 m2 = U384Hinted.modshl1(y1, p); + U384Hinted.moddivAssign(call, m1, m2); + + x2 = U384Hinted.modexp(call, m1, 2); + U384Hinted.modsubAssign(x2, x1, p); + U384Hinted.modsubAssign(x2, x1, p); + + y2 = U384Hinted.modsub(x1, x2, p); + U384Hinted.modmulAssign(call, y2, m1); + U384Hinted.modsubAssign(y2, y1, p); + + if (U384Hinted.eqInteger(y2, 0)) { + return (0, 0); + } + + U384Hinted.modexpAssignTo(call, m1, x2, 2); + U384Hinted.modmulAssign(call, m1, three); + U384Hinted.modaddAssign(m1, a, p); + + U384Hinted.modshl1AssignTo(m2, y2, p); + U384Hinted.moddivAssign(call, m1, m2); + + U384Hinted.modexpAssignTo(call, x1, m1, 2); + U384Hinted.modsubAssign(x1, x2, p); + U384Hinted.modsubAssign(x1, x2, p); + + U384Hinted.modsubAssignTo(y1, x2, x1, p); + U384Hinted.modmulAssign(call, y1, m1); + U384Hinted.modsubAssign(y1, y2, p); + + if (U384Hinted.eqInteger(y1, 0)) { + return (0, 0); + } + + U384Hinted.modexpAssignTo(call, m1, x1, 2); + U384Hinted.modmulAssign(call, m1, three); + U384Hinted.modaddAssign(m1, a, p); + + U384Hinted.modshl1AssignTo(m2, y1, p); + U384Hinted.moddivAssign(call, m1, m2); + + U384Hinted.modexpAssignTo(call, x2, m1, 2); + U384Hinted.modsubAssign(x2, x1, p); + U384Hinted.modsubAssign(x2, x1, p); + + U384Hinted.modsubAssignTo(y2, x1, x2, p); + U384Hinted.modmulAssign(call, y2, m1); + U384Hinted.modsubAssign(y2, y1, p); + } + } + + /** + * @dev Add two elliptic curve points in affine coordinates. + */ + function _addAffine( + uint256 call, + uint256 p, + uint256 three, + uint256 a, + uint256 x1, + uint256 y1, + uint256 x2, + uint256 y2 + ) private returns (uint256 x3, uint256 y3) { + unchecked { + if (x1 == 0 || x2 == 0) { + if (x1 == 0 && x2 == 0) { + return (0, 0); + } + + return x1 == 0 ? (x2.copy(), y2.copy()) : (x1.copy(), y1.copy()); + } + + if (U384Hinted.eq(x1, x2)) { + if (U384Hinted.eq(y1, y2)) { + return _twiceAffine(call, p, three, a, x1, y1); + } + + return (0, 0); + } + + uint256 m1 = U384Hinted.modsub(y1, y2, p); + uint256 m2 = U384Hinted.modsub(x1, x2, p); + + U384Hinted.moddivAssign(call, m1, m2); + + x3 = U384Hinted.modexp(call, m1, 2); + U384Hinted.modsubAssign(x3, x1, p); + U384Hinted.modsubAssign(x3, x2, p); + + y3 = U384Hinted.modsub(x1, x3, p); + U384Hinted.modmulAssign(call, y3, m1); + U384Hinted.modsubAssign(y3, y1, p); + } + } + + function _precomputePointsTable( + uint256 call, + uint256 p, + uint256 three, + uint256 a, + uint256 gx, + uint256 gy, + uint256 hx, + uint256 hy + ) private returns (uint256[2][64] memory points_) { + unchecked { + (points_[0x01][0], points_[0x01][1]) = (hx.copy(), hy.copy()); + (points_[0x08][0], points_[0x08][1]) = (gx.copy(), gy.copy()); + + for (uint256 i = 0; i < 8; ++i) { + for (uint256 j = 0; j < 8; ++j) { + if (i + j < 2) { + continue; + } + + uint256 maskTo = (i << 3) | j; + + if (i != 0) { + uint256 maskFrom = ((i - 1) << 3) | j; + + (points_[maskTo][0], points_[maskTo][1]) = + _addAffine(call, p, three, a, points_[maskFrom][0], points_[maskFrom][1], gx, gy); + } else { + uint256 maskFrom = (i << 3) | (j - 1); + + (points_[maskTo][0], points_[maskTo][1]) = + _addAffine(call, p, three, a, points_[maskFrom][0], points_[maskFrom][1], hx, hy); + } + } + } + } + } +} + +/** + * @notice Low-level utility library that implements unsigned 384-bit arithmetics. + * + * Should not be used outside of this file. + */ +library U384Hinted { + uint256 private constant SHORT_ALLOCATION = 64; + + uint256 private constant MUL_OFFSET = 288; + uint256 private constant EXP_OFFSET = 2 * 288; + uint256 private constant INV_OFFSET = 3 * 288; + uint256 private constant HINT_DATA_OFFSET = 0x480; + uint256 private constant HINT_LENGTH_OFFSET = 0x4A0; + uint256 private constant HINT_CURSOR_OFFSET = 0x4C0; + uint256 private constant HINT_ENABLED_OFFSET = 0x4E0; + uint256 private constant CALL_ALLOCATION = 0x500; + + function init(uint256 from_) internal returns (uint256 handler_) { + unchecked { + handler_ = _allocate(SHORT_ALLOCATION); + + assembly { + mstore(handler_, 0x00) + mstore(add(0x20, handler_), from_) + } + + return handler_; + } + } + + function init(bytes memory from_) internal returns (uint256 handler_) { + unchecked { + require(from_.length == 48, "U384: not 384"); + + handler_ = _allocate(SHORT_ALLOCATION); + + assembly { + mstore(handler_, 0x00) + mstore(add(handler_, 0x10), mload(add(from_, 0x20))) + mstore(add(handler_, 0x20), mload(add(from_, 0x30))) + } + + return handler_; + } + } + + function init2(bytes memory from2_) internal returns (uint256 handler1_, uint256 handler2_) { + unchecked { + require(from2_.length == 96, "U384: not 768"); + + handler1_ = _allocate(SHORT_ALLOCATION); + handler2_ = _allocate(SHORT_ALLOCATION); + + assembly { + mstore(handler1_, 0x00) + mstore(add(handler1_, 0x10), mload(add(from2_, 0x20))) + mstore(add(handler1_, 0x20), mload(add(from2_, 0x30))) + + mstore(handler2_, 0x00) + mstore(add(handler2_, 0x10), mload(add(from2_, 0x50))) + mstore(add(handler2_, 0x20), mload(add(from2_, 0x60))) + } + + return (handler1_, handler2_); + } + } + + function initCall(uint256 m_) internal returns (uint256 handler_) { + unchecked { + handler_ = _allocate(CALL_ALLOCATION); + + _sub(m_, init(2), handler_ + INV_OFFSET + 0xA0); + + assembly { + let call_ := add(handler_, MUL_OFFSET) + + mstore(call_, 0x60) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0xC0, call_), 0x01) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + call_ := add(handler_, EXP_OFFSET) + + mstore(call_, 0x40) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0xC0, call_), mload(m_)) + mstore(add(0xE0, call_), mload(add(m_, 0x20))) + + call_ := add(handler_, INV_OFFSET) + + mstore(call_, 0x40) + mstore(add(0x20, call_), 0x40) + mstore(add(0x40, call_), 0x40) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + mstore(add(handler_, HINT_DATA_OFFSET), 0) + mstore(add(handler_, HINT_LENGTH_OFFSET), 0) + mstore(add(handler_, HINT_CURSOR_OFFSET), 0) + mstore(add(handler_, HINT_ENABLED_OFFSET), 0) + } + } + } + + function initCallWithHints(uint256 m_, bytes memory inverseHints_) internal returns (uint256 handler_) { + handler_ = initCall(m_); + + assembly { + mstore(add(handler_, HINT_DATA_OFFSET), add(inverseHints_, 0x20)) + mstore(add(handler_, HINT_LENGTH_OFFSET), mload(inverseHints_)) + mstore(add(handler_, HINT_CURSOR_OFFSET), 0) + mstore(add(handler_, HINT_ENABLED_OFFSET), 1) + } + } + + function hintCursor(uint256 call_) internal returns (uint256 cursor_) { + assembly { + cursor_ := mload(add(call_, HINT_CURSOR_OFFSET)) + } + } + + function copy(uint256 handler_) internal returns (uint256 handlerCopy_) { + unchecked { + handlerCopy_ = _allocate(SHORT_ALLOCATION); + + assembly { + mstore(handlerCopy_, mload(handler_)) + mstore(add(handlerCopy_, 0x20), mload(add(handler_, 0x20))) + } + + return handlerCopy_; + } + } + + function eq(uint256 a_, uint256 b_) internal returns (bool eq_) { + assembly { + eq_ := and(eq(mload(a_), mload(b_)), eq(mload(add(a_, 0x20)), mload(add(b_, 0x20)))) + } + } + + function eqInteger(uint256 a_, uint256 bInteger_) internal returns (bool eq_) { + assembly { + eq_ := and(eq(mload(a_), 0), eq(mload(add(a_, 0x20)), bInteger_)) + } + } + + function cmp(uint256 a_, uint256 b_) internal returns (int256 cmp_) { + unchecked { + uint256 aWord_; + uint256 bWord_; + + assembly { + aWord_ := mload(a_) + bWord_ := mload(b_) + } + + if (aWord_ > bWord_) { + return 1; + } + + if (aWord_ < bWord_) { + return -1; + } + + assembly { + aWord_ := mload(add(a_, 0x20)) + bWord_ := mload(add(b_, 0x20)) + } + + if (aWord_ > bWord_) { + return 1; + } + + if (aWord_ < bWord_) { + return -1; + } + } + } + + function modAssign(uint256 call_, uint256 a_, uint256 m_) internal { + assembly { + mstore(call_, 0x40) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0x60, call_), mload(a_)) + mstore(add(0x80, call_), mload(add(a_, 0x20))) + mstore(add(0xA0, call_), 0x01) + mstore(add(0xC0, call_), mload(m_)) + mstore(add(0xE0, call_), mload(add(m_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0100, a_, 0x40)) + } + } + + function modexp(uint256 call_, uint256 b_, uint256 eInteger_) internal returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + assembly { + call_ := add(call_, EXP_OFFSET) + + mstore(add(0x60, call_), mload(b_)) + mstore(add(0x80, call_), mload(add(b_, 0x20))) + mstore(add(0xA0, call_), eInteger_) + + pop(staticcall(gas(), 0x5, call_, 0x0100, r_, 0x40)) + } + + return r_; + } + } + + function modexpAssignTo(uint256 call_, uint256 to_, uint256 b_, uint256 eInteger_) internal { + assembly { + call_ := add(call_, EXP_OFFSET) + + mstore(add(0x60, call_), mload(b_)) + mstore(add(0x80, call_), mload(add(b_, 0x20))) + mstore(add(0xA0, call_), eInteger_) + + pop(staticcall(gas(), 0x5, call_, 0x0100, to_, 0x40)) + } + } + + function modadd(uint256 a_, uint256 b_, uint256 m_) internal returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + _add(a_, b_, r_); + + if (cmp(r_, m_) >= 0) { + _subFrom(r_, m_); + } + + return r_; + } + } + + function modaddAssign(uint256 a_, uint256 b_, uint256 m_) internal { + unchecked { + _addTo(a_, b_); + + if (cmp(a_, m_) >= 0) { + return _subFrom(a_, m_); + } + } + } + + function modmul(uint256 call_, uint256 a_, uint256 b_) internal returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + _mul(a_, b_, call_ + MUL_OFFSET + 0x60); + + assembly { + call_ := add(call_, MUL_OFFSET) + + pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) + } + + return r_; + } + } + + function modmulAssign(uint256 call_, uint256 a_, uint256 b_) internal { + unchecked { + _mul(a_, b_, call_ + MUL_OFFSET + 0x60); + + assembly { + call_ := add(call_, MUL_OFFSET) + + pop(staticcall(gas(), 0x5, call_, 0x0120, a_, 0x40)) + } + } + } + + function modsub(uint256 a_, uint256 b_, uint256 m_) internal returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + if (cmp(a_, b_) >= 0) { + _sub(a_, b_, r_); + return r_; + } + + _add(a_, m_, r_); + _subFrom(r_, b_); + } + } + + function modsubAssign(uint256 a_, uint256 b_, uint256 m_) internal { + unchecked { + if (cmp(a_, b_) >= 0) { + _subFrom(a_, b_); + return; + } + + _addTo(a_, m_); + _subFrom(a_, b_); + } + } + + function modsubAssignTo(uint256 to_, uint256 a_, uint256 b_, uint256 m_) internal { + unchecked { + if (cmp(a_, b_) >= 0) { + _sub(a_, b_, to_); + return; + } + + _add(a_, m_, to_); + _subFrom(to_, b_); + } + } + + function modshl1(uint256 a_, uint256 m_) internal returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + _shl1(a_, r_); + + if (cmp(r_, m_) >= 0) { + _subFrom(r_, m_); + } + + return r_; + } + } + + function modshl1AssignTo(uint256 to_, uint256 a_, uint256 m_) internal { + unchecked { + _shl1(a_, to_); + + if (cmp(to_, m_) >= 0) { + _subFrom(to_, m_); + } + } + } + + /// @dev Stores modinv into `b_` and moddiv into `a_`. + function moddivAssign(uint256 call_, uint256 a_, uint256 b_) internal { + unchecked { + uint256 baseCall_ = call_; + if (_hintsEnabled(call_)) { + uint256 inv_ = _nextInverseHint(call_); + uint256 check_ = modmul(call_, b_, inv_); + require(eqInteger(check_, 1), "bad inverse hint"); + + assembly { + mstore(b_, mload(inv_)) + mstore(add(b_, 0x20), mload(add(inv_, 0x20))) + } + } else { + assembly { + call_ := add(call_, INV_OFFSET) + + mstore(add(0x60, call_), mload(b_)) + mstore(add(0x80, call_), mload(add(b_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0120, b_, 0x40)) + } + } + + modmulAssign(baseCall_, a_, b_); + } + } + + function moddiv(uint256 call_, uint256 a_, uint256 b_, uint256 m_) internal returns (uint256 r_) { + unchecked { + r_ = modinv(call_, b_, m_); + + _mul(a_, r_, call_ + 0x60); + + assembly { + mstore(call_, 0x60) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0xC0, call_), 0x01) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) + } + } + } + + function modinv(uint256 call_, uint256 b_, uint256 m_) internal returns (uint256 r_) { + unchecked { + if (_hintsEnabled(call_)) { + r_ = _nextInverseHint(call_); + uint256 check_ = _modmulWithMod(call_, b_, r_, m_); + require(eqInteger(check_, 1), "bad inverse hint"); + return r_; + } + + r_ = _allocate(SHORT_ALLOCATION); + + _sub(m_, init(2), call_ + 0xA0); + + assembly { + mstore(call_, 0x40) + mstore(add(0x20, call_), 0x40) + mstore(add(0x40, call_), 0x40) + mstore(add(0x60, call_), mload(b_)) + mstore(add(0x80, call_), mload(add(b_, 0x20))) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) + } + } + } + + function _hintsEnabled(uint256 call_) private returns (bool enabled_) { + assembly { + enabled_ := mload(add(call_, HINT_ENABLED_OFFSET)) + } + } + + function _nextInverseHint(uint256 call_) private returns (uint256 r_) { + uint256 cursor_; + uint256 length_; + assembly { + cursor_ := mload(add(call_, HINT_CURSOR_OFFSET)) + length_ := mload(add(call_, HINT_LENGTH_OFFSET)) + } + require(cursor_ + 48 <= length_, "inverse hint underflow"); + + r_ = _allocate(SHORT_ALLOCATION); + + assembly { + let src_ := add(mload(add(call_, HINT_DATA_OFFSET)), cursor_) + mstore(r_, shr(128, mload(src_))) + mstore(add(r_, 0x20), mload(add(src_, 0x10))) + mstore(add(call_, HINT_CURSOR_OFFSET), add(cursor_, 48)) + } + } + + function _modmulWithMod(uint256 call_, uint256 a_, uint256 b_, uint256 m_) private returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + _mul(a_, b_, call_ + 0x60); + + assembly { + mstore(call_, 0x60) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0xC0, call_), 0x01) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) + } + } + } + + function _shl1(uint256 a_, uint256 r_) internal { + assembly { + let a1_ := mload(add(a_, 0x20)) + + mstore(r_, or(shl(1, mload(a_)), shr(255, a1_))) + mstore(add(r_, 0x20), shl(1, a1_)) + } + } + + function _add(uint256 a_, uint256 b_, uint256 r_) private { + assembly { + let aWord_ := mload(add(a_, 0x20)) + let sum_ := add(aWord_, mload(add(b_, 0x20))) + + mstore(add(r_, 0x20), sum_) + + sum_ := gt(aWord_, sum_) + sum_ := add(sum_, add(mload(a_), mload(b_))) + + mstore(r_, sum_) + } + } + + function _sub(uint256 a_, uint256 b_, uint256 r_) private { + assembly { + let aWord_ := mload(add(a_, 0x20)) + let diff_ := sub(aWord_, mload(add(b_, 0x20))) + + mstore(add(r_, 0x20), diff_) + + diff_ := gt(diff_, aWord_) + diff_ := sub(sub(mload(a_), mload(b_)), diff_) + + mstore(r_, diff_) + } + } + + function _subFrom(uint256 a_, uint256 b_) private { + assembly { + let aWord_ := mload(add(a_, 0x20)) + let diff_ := sub(aWord_, mload(add(b_, 0x20))) + + mstore(add(a_, 0x20), diff_) + + diff_ := gt(diff_, aWord_) + diff_ := sub(sub(mload(a_), mload(b_)), diff_) + + mstore(a_, diff_) + } + } + + function _addTo(uint256 a_, uint256 b_) private { + assembly { + let aWord_ := mload(add(a_, 0x20)) + let sum_ := add(aWord_, mload(add(b_, 0x20))) + + mstore(add(a_, 0x20), sum_) + + sum_ := gt(aWord_, sum_) + sum_ := add(sum_, add(mload(a_), mload(b_))) + + mstore(a_, sum_) + } + } + + function _mul(uint256 a_, uint256 b_, uint256 r_) private { + assembly { + let a0_ := mload(a_) + let a1_ := shr(128, mload(add(a_, 0x20))) + let a2_ := and(mload(add(a_, 0x20)), 0xffffffffffffffffffffffffffffffff) + + let b0_ := mload(b_) + let b1_ := shr(128, mload(add(b_, 0x20))) + let b2_ := and(mload(add(b_, 0x20)), 0xffffffffffffffffffffffffffffffff) + + // r5 + let current_ := mul(a2_, b2_) + let r0_ := and(current_, 0xffffffffffffffffffffffffffffffff) + + // r4 + current_ := shr(128, current_) + + let temp_ := mul(a1_, b2_) + current_ := add(current_, temp_) + let curry_ := lt(current_, temp_) + + temp_ := mul(a2_, b1_) + current_ := add(current_, temp_) + curry_ := add(curry_, lt(current_, temp_)) + + mstore(add(r_, 0x40), add(shl(128, current_), r0_)) + + // r3 + current_ := add(shl(128, curry_), shr(128, current_)) + curry_ := 0 + + temp_ := mul(a0_, b2_) + current_ := add(current_, temp_) + curry_ := lt(current_, temp_) + + temp_ := mul(a1_, b1_) + current_ := add(current_, temp_) + curry_ := add(curry_, lt(current_, temp_)) + + temp_ := mul(a2_, b0_) + current_ := add(current_, temp_) + curry_ := add(curry_, lt(current_, temp_)) + + r0_ := and(current_, 0xffffffffffffffffffffffffffffffff) + + // r2 + current_ := add(shl(128, curry_), shr(128, current_)) + curry_ := 0 + + temp_ := mul(a0_, b1_) + current_ := add(current_, temp_) + curry_ := lt(current_, temp_) + + temp_ := mul(a1_, b0_) + current_ := add(current_, temp_) + curry_ := add(curry_, lt(current_, temp_)) + + mstore(add(r_, 0x20), add(shl(128, current_), r0_)) + + // r1 + current_ := add(shl(128, curry_), shr(128, current_)) + current_ := add(current_, mul(a0_, b0_)) + + mstore(r_, current_) + } + } + + function _allocate(uint256 bytes_) private returns (uint256 handler_) { + unchecked { + assembly { + handler_ := mload(0x40) + mstore(0x40, add(handler_, bytes_)) + } + + return handler_; + } + } +} diff --git a/src/IHintedCertManager.sol b/src/IHintedCertManager.sol new file mode 100644 index 0000000..3c56827 --- /dev/null +++ b/src/IHintedCertManager.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {ICertManager} from "./ICertManager.sol"; + +interface IHintedCertManager is ICertManager { + function verifyCACertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) + external + returns (bytes32); + + function verifyClientCertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) + external + returns (VerifiedCert memory); + + function loadVerified(bytes32 certHash) external view returns (VerifiedCert memory); +} diff --git a/src/IP384Verifier.sol b/src/IP384Verifier.sol new file mode 100644 index 0000000..068341a --- /dev/null +++ b/src/IP384Verifier.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +interface IP384Verifier { + function verifyP384SignatureWithHints( + bytes memory hash, + bytes memory signature, + bytes memory pubKey, + bytes memory inverseHints + ) external returns (bool); +} diff --git a/src/NitroValidatorHinted.sol b/src/NitroValidatorHinted.sol new file mode 100644 index 0000000..7d8405d --- /dev/null +++ b/src/NitroValidatorHinted.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {IHintedCertManager} from "./IHintedCertManager.sol"; +import {IP384Verifier} from "./IP384Verifier.sol"; +import {NitroValidatorHintedExternal} from "./NitroValidatorHintedExternal.sol"; + +/// @notice Deployable hinted Nitro attestation validator. +/// @dev Kept as the canonical hinted name; P384 verification is delegated to `p384Verifier`. +contract NitroValidatorHinted is NitroValidatorHintedExternal { + constructor(IHintedCertManager certManager_, IP384Verifier p384Verifier_) + NitroValidatorHintedExternal(certManager_, p384Verifier_) + {} +} diff --git a/src/NitroValidatorHintedExternal.sol b/src/NitroValidatorHintedExternal.sol new file mode 100644 index 0000000..cdd3664 --- /dev/null +++ b/src/NitroValidatorHintedExternal.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {CborDecode, CborElement, LibCborElement} from "./CborDecode.sol"; +import {ICertManager} from "./ICertManager.sol"; +import {IHintedCertManager} from "./IHintedCertManager.sol"; +import {IP384Verifier} from "./IP384Verifier.sol"; +import {LibBytes} from "./LibBytes.sol"; +import {Sha2Ext} from "./Sha2Ext.sol"; + +/// @notice Hinted-only Nitro attestation validator that requires the cert chain to be cached first. +/// @dev Certificate and attestation P384 signatures are verified through an external verifier contract. +contract NitroValidatorHintedExternal { + using LibBytes for bytes; + using CborDecode for bytes; + using LibCborElement for CborElement; + + bytes32 internal constant ATTESTATION_TBS_PREFIX = + 0x63ce814bd924c1ef12c43686e4cbf48ed1639a78387b0570c23ca921e8ce071c; + bytes32 internal constant ATTESTATION_DIGEST = 0x501a3a7a4e0cf54b03f2488098bdd59bc1c2e8d741a300d6b25926d531733fef; + + bytes32 internal constant CERTIFICATE_KEY = 0x925cec779426f44d8d555e01d2683a3a765ce2fa7562ca7352aeb09dfc57ea6a; + bytes32 internal constant PUBLIC_KEY_KEY = 0xc7b28019ccfdbd30ffc65951d94bb85c9e2b8434111a000b5afd533ce65f57a4; + bytes32 internal constant MODULE_ID_KEY = 0x8ce577cf664c36ba5130242bf5790c2675e9f4e6986a842b607821bee25372ee; + bytes32 internal constant TIMESTAMP_KEY = 0x4ebf727c48eac2c66272456b06a885c5cc03e54d140f63b63b6fd10c1227958e; + bytes32 internal constant USER_DATA_KEY = 0x5e4ea5393e4327b3014bc32f2264336b0d1ee84a4cfd197c8ad7e1e16829a16a; + bytes32 internal constant CABUNDLE_KEY = 0x8a8cb7aa1da17ada103546ae6b4e13ccc2fafa17adf5f93925e0a0a4e5681a6a; + bytes32 internal constant DIGEST_KEY = 0x682a7e258d80bd2421d3103cbe71e3e3b82138116756b97b8256f061dc2f11fb; + bytes32 internal constant NONCE_KEY = 0x7ab1577440dd7bedf920cb6de2f9fc6bf7ba98c78c85a3fa1f8311aac95e1759; + bytes32 internal constant PCRS_KEY = 0x61585f8bc67a4b6d5891a4639a074964ac66fc2241dc0b36c157dc101325367a; + + struct Ptrs { + CborElement moduleID; + uint64 timestamp; + CborElement digest; + CborElement[] pcrs; + CborElement cert; + CborElement[] cabundle; + CborElement publicKey; + CborElement userData; + CborElement nonce; + } + + IHintedCertManager public immutable hintedCertManager; + IP384Verifier public immutable p384Verifier; + + constructor(IHintedCertManager certManager_, IP384Verifier p384Verifier_) { + require(address(certManager_) != address(0), "missing cert manager"); + require(address(p384Verifier_) != address(0), "missing P384 verifier"); + hintedCertManager = certManager_; + p384Verifier = p384Verifier_; + } + + function decodeAttestationTbs(bytes memory attestation) + external + pure + returns (bytes memory attestationTbs, bytes memory signature) + { + uint256 offset = 1; + if (attestation[0] == 0xD2) { + offset = 2; + } + + CborElement protectedPtr = attestation.byteStringAt(offset); + CborElement unprotectedPtr = attestation.nextMap(protectedPtr); + CborElement payloadPtr = attestation.nextByteString(unprotectedPtr); + CborElement signaturePtr = attestation.nextByteString(payloadPtr); + + uint256 rawProtectedLength = protectedPtr.end() - offset; + uint256 rawPayloadLength = payloadPtr.end() - unprotectedPtr.end(); + bytes memory rawProtectedBytes = attestation.slice(offset, rawProtectedLength); + bytes memory rawPayloadBytes = attestation.slice(unprotectedPtr.end(), rawPayloadLength); + attestationTbs = + _constructAttestationTbs(rawProtectedBytes, rawProtectedLength, rawPayloadBytes, rawPayloadLength); + signature = attestation.slice(signaturePtr.start(), signaturePtr.length()); + } + + function validateAttestationWithHints( + bytes memory attestationTbs, + bytes memory signature, + bytes memory attestationSigHints + ) public returns (Ptrs memory) { + Ptrs memory ptrs = _parseAttestation(attestationTbs); + + require(ptrs.moduleID.length() > 0, "no module id"); + require(ptrs.timestamp > 0, "no timestamp"); + require(ptrs.cabundle.length > 0, "no cabundle"); + require(attestationTbs.keccak(ptrs.digest) == ATTESTATION_DIGEST, "invalid digest"); + require(1 <= ptrs.pcrs.length && ptrs.pcrs.length <= 32, "invalid pcrs"); + require( + ptrs.publicKey.isNull() || (1 <= ptrs.publicKey.length() && ptrs.publicKey.length() <= 1024), + "invalid pub key" + ); + require(ptrs.userData.isNull() || (ptrs.userData.length() <= 512), "invalid user data"); + require(ptrs.nonce.isNull() || (ptrs.nonce.length() <= 512), "invalid nonce"); + + for (uint256 i = 0; i < ptrs.pcrs.length; i++) { + require( + ptrs.pcrs[i].length() == 32 || ptrs.pcrs[i].length() == 48 || ptrs.pcrs[i].length() == 64, "invalid pcr" + ); + } + + bytes memory cert = attestationTbs.slice(ptrs.cert); + bytes[] memory cabundle = new bytes[](ptrs.cabundle.length); + for (uint256 i = 0; i < ptrs.cabundle.length; i++) { + require(1 <= ptrs.cabundle[i].length() && ptrs.cabundle[i].length() <= 1024, "invalid cabundle cert"); + cabundle[i] = attestationTbs.slice(ptrs.cabundle[i]); + } + + ICertManager.VerifiedCert memory parent = verifyCachedCertBundle(cert, cabundle); + bytes memory hash = Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length); + require( + p384Verifier.verifyP384SignatureWithHints(hash, signature, parent.pubKey, attestationSigHints), + "invalid sig" + ); + + return ptrs; + } + + function verifyCachedCertBundle(bytes memory certificate, bytes[] memory cabundle) + internal + returns (ICertManager.VerifiedCert memory) + { + bytes32 parentHash; + for (uint256 i = 0; i < cabundle.length; i++) { + parentHash = hintedCertManager.verifyCACertWithHints(cabundle[i], parentHash, ""); + } + return hintedCertManager.verifyClientCertWithHints(certificate, parentHash, ""); + } + + function _constructAttestationTbs( + bytes memory rawProtectedBytes, + uint256 rawProtectedLength, + bytes memory rawPayloadBytes, + uint256 rawPayloadLength + ) internal pure returns (bytes memory attestationTbs) { + attestationTbs = new bytes(13 + rawProtectedLength + rawPayloadLength); + attestationTbs[0] = bytes1(uint8(4 << 5 | 4)); + attestationTbs[1] = bytes1(uint8(3 << 5 | 10)); + attestationTbs[12 + rawProtectedLength] = bytes1(uint8(2 << 5)); + + string memory sig = "Signature1"; + uint256 dest; + uint256 sigSrc; + uint256 protectedSrc; + uint256 payloadSrc; + assembly { + dest := add(attestationTbs, 32) + sigSrc := add(sig, 32) + protectedSrc := add(rawProtectedBytes, 32) + payloadSrc := add(rawPayloadBytes, 32) + } + + LibBytes.memcpy(dest + 2, sigSrc, 10); + LibBytes.memcpy(dest + 12, protectedSrc, rawProtectedLength); + LibBytes.memcpy(dest + 13 + rawProtectedLength, payloadSrc, rawPayloadLength); + } + + function _parseAttestation(bytes memory attestationTbs) internal pure returns (Ptrs memory) { + require(attestationTbs.keccak(0, 18) == ATTESTATION_TBS_PREFIX, "invalid attestation prefix"); + + CborElement payload = attestationTbs.byteStringAt(18); + CborElement current = attestationTbs.mapAt(payload.start()); + + Ptrs memory ptrs; + uint256 end = payload.end(); + while (current.end() < end) { + if (uint8(attestationTbs[current.end()]) == 0xff) break; + current = attestationTbs.nextTextString(current); + bytes32 keyHash = attestationTbs.keccak(current); + if (keyHash == MODULE_ID_KEY) { + current = attestationTbs.nextTextString(current); + ptrs.moduleID = current; + } else if (keyHash == DIGEST_KEY) { + current = attestationTbs.nextTextString(current); + ptrs.digest = current; + } else if (keyHash == CERTIFICATE_KEY) { + current = attestationTbs.nextByteString(current); + ptrs.cert = current; + } else if (keyHash == PUBLIC_KEY_KEY) { + current = attestationTbs.nextByteStringOrNull(current); + ptrs.publicKey = current; + } else if (keyHash == USER_DATA_KEY) { + current = attestationTbs.nextByteStringOrNull(current); + ptrs.userData = current; + } else if (keyHash == NONCE_KEY) { + current = attestationTbs.nextByteStringOrNull(current); + ptrs.nonce = current; + } else if (keyHash == TIMESTAMP_KEY) { + current = attestationTbs.nextPositiveInt(current); + ptrs.timestamp = uint64(current.value()); + } else if (keyHash == CABUNDLE_KEY) { + current = attestationTbs.nextArray(current); + ptrs.cabundle = new CborElement[](current.value()); + for (uint256 i = 0; i < ptrs.cabundle.length; i++) { + current = attestationTbs.nextByteString(current); + ptrs.cabundle[i] = current; + } + } else if (keyHash == PCRS_KEY) { + current = attestationTbs.nextMap(current); + ptrs.pcrs = new CborElement[](current.value()); + for (uint256 i = 0; i < ptrs.pcrs.length; i++) { + current = attestationTbs.nextPositiveInt(current); + uint256 key = current.value(); + require(key < ptrs.pcrs.length, "invalid pcr key value"); + require(CborElement.unwrap(ptrs.pcrs[key]) == 0, "duplicate pcr key"); + current = attestationTbs.nextByteString(current); + ptrs.pcrs[key] = current; + } + } else { + revert("invalid attestation key"); + } + } + + return ptrs; + } +} diff --git a/src/P384Verifier.sol b/src/P384Verifier.sol new file mode 100644 index 0000000..3a27773 --- /dev/null +++ b/src/P384Verifier.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {ECDSA384Curve} from "./ECDSA384Curve.sol"; +import {ECDSA384Hinted} from "./ECDSA384Hinted.sol"; +import {IP384Verifier} from "./IP384Verifier.sol"; + +contract P384Verifier is IP384Verifier { + function verifyP384SignatureWithHints( + bytes memory hash, + bytes memory signature, + bytes memory pubKey, + bytes memory inverseHints + ) external returns (bool) { + return ECDSA384Hinted.verifyWithHints(_p384(), hash, signature, pubKey, inverseHints); + } + + function _p384() internal pure returns (ECDSA384Hinted.Parameters memory) { + return ECDSA384Hinted.Parameters({ + a: ECDSA384Curve.CURVE_A, + b: ECDSA384Curve.CURVE_B, + gx: ECDSA384Curve.CURVE_GX, + gy: ECDSA384Curve.CURVE_GY, + p: ECDSA384Curve.CURVE_P, + n: ECDSA384Curve.CURVE_N, + lowSmax: ECDSA384Curve.CURVE_LOW_S_MAX + }); + } +} diff --git a/test/bench/Bench.t.sol b/test/bench/Bench.t.sol new file mode 100644 index 0000000..7b47413 --- /dev/null +++ b/test/bench/Bench.t.sol @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Test, console} from "forge-std/Test.sol"; +import {ECDSA384} from "@solarity/libs/crypto/ECDSA384.sol"; +import {ECDSA384Bench} from "./ECDSA384Bench.sol"; +import {ECDSA384Curve} from "../../src/ECDSA384Curve.sol"; + +contract RealHarness { + function verify(bytes memory h, bytes memory s, bytes memory p) external view returns (bool) { + return ECDSA384.verify(ECDSA384Curve.p384(), h, s, p); + } +} + +contract BenchHarness { + // returns (#inversions, #other modexp calls) + function countVerify(bytes memory h, bytes memory s, bytes memory p) + external + returns (uint256 inv, uint256 other, bool ok) + { + assembly { + tstore(0, 0) + tstore(1, 0) + } + ECDSA384Bench.Parameters memory params = ECDSA384Bench.Parameters({ + a: ECDSA384Curve.CURVE_A, + b: ECDSA384Curve.CURVE_B, + gx: ECDSA384Curve.CURVE_GX, + gy: ECDSA384Curve.CURVE_GY, + p: ECDSA384Curve.CURVE_P, + n: ECDSA384Curve.CURVE_N, + lowSmax: ECDSA384Curve.CURVE_LOW_S_MAX + }); + ok = ECDSA384Bench.verify(params, h, s, p); + assembly { + inv := tload(0) + other := tload(1) + } + } + + function profileVerify(bytes memory h, bytes memory s, bytes memory p) + external + returns (uint256[5] memory invByPhase, uint256[5] memory otherByPhase, bool ok) + { + assembly { + tstore(0, 0) + tstore(1, 0) + } + ECDSA384Bench.Parameters memory params = ECDSA384Bench.Parameters({ + a: ECDSA384Curve.CURVE_A, + b: ECDSA384Curve.CURVE_B, + gx: ECDSA384Curve.CURVE_GX, + gy: ECDSA384Curve.CURVE_GY, + p: ECDSA384Curve.CURVE_P, + n: ECDSA384Curve.CURVE_N, + lowSmax: ECDSA384Curve.CURVE_LOW_S_MAX + }); + ok = ECDSA384Bench.verify(params, h, s, p); + + uint256 prevInv; + uint256 prevOther; + for (uint256 i = 0; i < 5; ++i) { + uint256 inv; + uint256 other; + assembly { + inv := tload(add(10, i)) + other := tload(add(20, i)) + } + invByPhase[i] = inv - prevInv; + otherByPhase[i] = other - prevOther; + prevInv = inv; + prevOther = other; + } + } + + function collectInverseHints(bytes memory h, bytes memory s, bytes memory p) external returns (bytes memory hints) { + assembly { + tstore(0, 0) + tstore(1, 0) + tstore(2, 0) + tstore(7, 0) + tstore(8, 1) + } + ECDSA384Bench.Parameters memory params = _params(); + bool ok = ECDSA384Bench.verify(params, h, s, p); + require(ok, "collect verify failed"); + + uint256 count; + assembly { + count := tload(2) + tstore(8, 0) + } + hints = new bytes(count * 48); + for (uint256 i = 0; i < count; ++i) { + uint256 hi; + uint256 lo; + assembly { + let slot_ := add(1000, mul(i, 2)) + hi := tload(slot_) + lo := tload(add(slot_, 1)) + let dst_ := add(add(hints, 0x20), mul(i, 48)) + mstore(dst_, shl(128, hi)) + mstore(add(dst_, 0x10), lo) + } + } + } + + function countHintedVerify(bytes memory h, bytes memory s, bytes memory p, bytes memory hints) + external + returns (uint256 inv, uint256 other, uint256 consumed, bool ok) + { + assembly { + tstore(0, 0) + tstore(1, 0) + } + (ok, consumed) = ECDSA384Bench.verifyWithHintsConsumed(_params(), h, s, p, hints); + require(consumed == hints.length, "unused inverse hints"); + assembly { + inv := tload(0) + other := tload(1) + } + } + + function _params() internal pure returns (ECDSA384Bench.Parameters memory) { + return ECDSA384Bench.Parameters({ + a: ECDSA384Curve.CURVE_A, + b: ECDSA384Curve.CURVE_B, + gx: ECDSA384Curve.CURVE_GX, + gy: ECDSA384Curve.CURVE_GY, + p: ECDSA384Curve.CURVE_P, + n: ECDSA384Curve.CURVE_N, + lowSmax: ECDSA384Curve.CURVE_LOW_S_MAX + }); + } +} + +/// @notice Baseline benchmark: real per-verify gas + exact MODEXP census + EIP-7883 projection. +contract BenchTest is Test { + RealHarness real; + BenchHarness bench; + + // Synthetic-but-valid P384 signature (openssl secp384r1, SHA-384). Counts are + // input-independent up to ~Hamming-weight variance, so this is representative. + bytes h = hex"e6bb58cb85db069a2c0c310020f946d9c47e8ff6a43895ae824369325c74fb8d30897d7ea16447ef9975eb351a916a11"; + bytes s = + hex"1ab266914fb82615eba02dc55eb8d6e32e4adaa12a927786ab2353d9649f645e86b0166c990e36dbce9efcc743b998646025d5af666c17fb103d0c89f8a78aea9f8fff3703e21a3ab69f48b21d0bbf008f7c9702d54fd6f1af65223fe3936b3c"; + bytes p = + hex"edb97631be68370a653e5601fa2f12da63db7cdb1f6cf7413004f4de274f3d1c046de28a2530240080e7d84c436cb935ebb5c7a9fcd8027f5d97bf49fff3f9a7346270e10e0a529eeb117d75409f3c9acfde069a7a577a6a6b8048d78abfe5f5"; + + // Per-call MODEXP gas, derived from EIP-2565 / EIP-7883 for this library's + // operand profiles (verified against the EIP formulas): + // inversion : base=64, exp=64, mod=64, expHead bit-length 128 + // other : squaring/mulmul/reduce -> hits the min-gas floor + uint256 constant INV_2565 = 8170; // floor(8^2 * (8*32+127) / 3) + uint256 constant INV_7883 = 81792; // 2*8^2 * (16*32+127) + uint256 constant OTHER_2565 = 200; // EIP-2565 minimum + uint256 constant OTHER_7883 = 500; // EIP-7883 minimum + uint256 constant TX_CAP = 16_777_216; // EIP-7825 per-transaction gas cap (2^24) + + function setUp() public { + real = new RealHarness(); + bench = new BenchHarness(); + } + + function test_Baseline() public { + // 1) ground-truth current gas for a single verify (uninstrumented) + uint256 g0 = gasleft(); + bool ok = real.verify(h, s, p); + uint256 verifyGas2565 = g0 - gasleft(); + assertTrue(ok, "verify must pass"); + + // 2) exact MODEXP census (instrumented copy) + (uint256 inv, uint256 other,) = bench.countVerify(h, s, p); + (uint256[5] memory invByPhase, uint256[5] memory otherByPhase,) = bench.profileVerify(h, s, p); + + // 3) MODEXP gas under each pricing + uint256 mod2565 = inv * INV_2565 + other * OTHER_2565; + uint256 mod7883 = inv * INV_7883 + other * OTHER_7883; + + // 4) project post-Fusaka verify: only the MODEXP portion reprices, + // the rest of the verify (EVM arithmetic / memory) is unchanged. + uint256 verifyGas7883 = verifyGas2565 + (mod7883 - mod2565); + + console.log("==== SINGLE P384 ECDSA VERIFY ===="); + console.log("measured verify gas (EIP-2565) :", verifyGas2565); + console.log("MODEXP calls total :", inv + other); + console.log(" field inversions :", inv); + console.log(" other (sq/mul/reduce) :", other); + console.log("MODEXP gas EIP-2565 :", mod2565); + console.log("MODEXP gas EIP-7883 :", mod7883); + console.log(" modexp share of verify (2565,%):", (mod2565 * 100) / verifyGas2565); + console.log("PROJECTED verify gas (EIP-7883) :", verifyGas7883); + console.log(" blow-up factor x100 :", (verifyGas7883 * 100) / verifyGas2565); + console.log("per-tx cap (EIP-7825) :", TX_CAP); + console.log("verify fits in 1 tx post-Fusaka? :", verifyGas7883 <= TX_CAP ? 1 : 0); + console.log("min verifies that must be split :", (verifyGas7883 + TX_CAP - 1) / TX_CAP); + console.log("==== MODEXP PHASE BREAKDOWN ===="); + _logPhase("on-curve check", invByPhase[0], otherByPhase[0]); + _logPhase("scalar divs", invByPhase[1], otherByPhase[1]); + _logPhase("precompute table", invByPhase[2], otherByPhase[2]); + _logPhase("double scalar mult", invByPhase[3], otherByPhase[3]); + _logPhase("final mod", invByPhase[4], otherByPhase[4]); + } + + /// EXPERIMENT 001 (analytical): replace each on-chain field inversion with a + /// caller-supplied witness verified by ONE modmul (b * b_inv == 1 mod p). + /// Each moddiv goes from {1 inversion + 1 mulmul} to {2 mulmuls}: the 570 + /// inversions become 570 cheap mulmuls (floor-priced), nothing else changes. + function test_HintedInversionModel() public { + uint256 g0 = gasleft(); + bool ok = real.verify(h, s, p); + uint256 verifyGas2565 = g0 - gasleft(); + assertTrue(ok); + (uint256 inv, uint256 other,) = bench.countVerify(h, s, p); + + uint256 nonModexp = verifyGas2565 - (inv * INV_2565 + other * OTHER_2565); + // hinted profile: 0 inversions, (other + inv) floor-priced calls + uint256 calls = other + inv; + uint256 hinted2565 = nonModexp + calls * OTHER_2565; + uint256 hinted7883 = nonModexp + calls * OTHER_7883; + uint256 witnessBytes = inv * 48; + uint256 witnessCalldataGas = witnessBytes * 16; // worst case: all nonzero bytes + + console.log("==== EXPERIMENT 001: HINTED INVERSIONS (model) ===="); + console.log("non-MODEXP verify gas (fixed) :", nonModexp); + console.log("hinted verify gas EIP-2565 :", hinted2565); + console.log("hinted verify gas EIP-7883 :", hinted7883); + console.log(" + witness calldata (worst) gas :", witnessCalldataGas); + console.log(" = total post-Fusaka :", hinted7883 + witnessCalldataGas); + console.log("fits 1 tx post-Fusaka? :", (hinted7883 + witnessCalldataGas) <= TX_CAP ? 1 : 0); + console.log("witness bytes per verify :", witnessBytes); + } + + function test_HintedInversionPrototype() public { + bytes memory hints = bench.collectInverseHints(h, s, p); + + uint256 g0 = gasleft(); + (uint256 inv, uint256 other, uint256 consumed, bool ok) = bench.countHintedVerify(h, s, p, hints); + uint256 hintedGas2565 = g0 - gasleft(); + assertTrue(ok, "hinted verify must pass"); + assertEq(consumed, hints.length, "must consume all hints"); + + uint256 mod2565 = inv * INV_2565 + other * OTHER_2565; + uint256 mod7883 = inv * INV_7883 + other * OTHER_7883; + uint256 hintedGas7883 = hintedGas2565 + (mod7883 - mod2565); + uint256 witnessCalldataGas = hints.length * 16; // worst case: all nonzero bytes + + console.log("==== EXPERIMENT 002: HINTED INVERSIONS (prototype) ===="); + console.log("hinted verify gas EIP-2565 :", hintedGas2565); + console.log("MODEXP calls total :", inv + other); + console.log(" field inversions :", inv); + console.log(" other (sq/mul/reduce/check) :", other); + console.log("projected verify gas EIP-7883 :", hintedGas7883); + console.log(" + witness calldata (worst) gas :", witnessCalldataGas); + console.log(" = total post-Fusaka :", hintedGas7883 + witnessCalldataGas); + console.log("fits 1 tx post-Fusaka? :", (hintedGas7883 + witnessCalldataGas) <= TX_CAP ? 1 : 0); + console.log("witness bytes per verify :", hints.length); + } + + function test_HintedInversionRejectsMutatedHint() public { + bytes memory hints = bench.collectInverseHints(h, s, p); + hints[100] = bytes1(uint8(hints[100]) ^ 1); + + vm.expectRevert("bad inverse hint"); + bench.countHintedVerify(h, s, p, hints); + } + + function test_HintedInversionRejectsTruncatedHints() public { + bytes memory hints = bench.collectInverseHints(h, s, p); + assembly { + mstore(hints, sub(mload(hints), 1)) + } + + vm.expectRevert("inverse hint underflow"); + bench.countHintedVerify(h, s, p, hints); + } + + function test_HintedInversionRejectsSurplusHints() public { + bytes memory hints = abi.encodePacked(bench.collectInverseHints(h, s, p), bytes1(0x00)); + + vm.expectRevert("unused inverse hints"); + bench.countHintedVerify(h, s, p, hints); + } + + function _logPhase(string memory name, uint256 inv, uint256 other) internal pure { + console.log(name); + console.log(" inversions:", inv); + console.log(" other :", other); + console.log(" EIP-7883 MODEXP gas:", inv * INV_7883 + other * OTHER_7883); + } +} diff --git a/test/bench/ECDSA384Bench.sol b/test/bench/ECDSA384Bench.sol new file mode 100644 index 0000000..787d746 --- /dev/null +++ b/test/bench/ECDSA384Bench.sol @@ -0,0 +1,1081 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {MemoryUtils} from "@solarity/libs/utils/MemoryUtils.sol"; + +/** + * @notice Cryptography module + * + * This library provides functionality for ECDSA verification over any 384-bit curve. Currently, + * this is the most efficient implementation out there, consuming ~8.025 million gas per call. + * + * The approach is Strauss-Shamir double scalar multiplication with 6 bits of precompute + affine coordinates. + * For reference, naive implementation uses ~400 billion gas, which is 50000 times more expensive. + * + * We also tried using projective coordinates, however, the gas consumption rose to ~9 million gas. + */ +library ECDSA384Bench { + using MemoryUtils for *; + using U384Bench for *; + + /** + * @notice 384-bit curve parameters. + */ + struct Parameters { + bytes a; + bytes b; + bytes gx; + bytes gy; + bytes p; + bytes n; + bytes lowSmax; + } + + struct _Parameters { + uint256 a; + uint256 b; + uint256 gx; + uint256 gy; + uint256 p; + uint256 n; + uint256 lowSmax; + } + + struct _Inputs { + uint256 r; + uint256 s; + uint256 x; + uint256 y; + } + + /** + * @notice The function to verify the ECDSA signature + * @param curveParams_ the 384-bit curve parameters. `lowSmax` is `n / 2`. + * @param hashedMessage_ the already hashed message to be verified. + * @param signature_ the ECDSA signature. Equals to `bytes(r) + bytes(s)`. + * @param pubKey_ the full public key of a signer. Equals to `bytes(x) + bytes(y)`. + * + * Note that signatures only from the lower part of the curve are accepted. + * If your `s > n / 2`, change it to `s = n - s`. + */ + function verify( + Parameters memory curveParams_, + bytes memory hashedMessage_, + bytes memory signature_, + bytes memory pubKey_ + ) internal returns (bool) { + (bool ok_,) = _verify(curveParams_, hashedMessage_, signature_, pubKey_, "", false); + return ok_; + } + + function verifyWithHints( + Parameters memory curveParams_, + bytes memory hashedMessage_, + bytes memory signature_, + bytes memory pubKey_, + bytes memory inverseHints_ + ) internal returns (bool ok_) { + uint256 consumed_; + (ok_, consumed_) = _verify(curveParams_, hashedMessage_, signature_, pubKey_, inverseHints_, true); + require(consumed_ == inverseHints_.length, "unused inverse hints"); + } + + function verifyWithHintsConsumed( + Parameters memory curveParams_, + bytes memory hashedMessage_, + bytes memory signature_, + bytes memory pubKey_, + bytes memory inverseHints_ + ) internal returns (bool ok_, uint256 consumed_) { + return _verify(curveParams_, hashedMessage_, signature_, pubKey_, inverseHints_, true); + } + + function _verify( + Parameters memory curveParams_, + bytes memory hashedMessage_, + bytes memory signature_, + bytes memory pubKey_, + bytes memory inverseHints_, + bool useHints_ + ) private returns (bool ok_, uint256 consumed_) { + unchecked { + _Inputs memory inputs_; + + (inputs_.r, inputs_.s) = U384Bench.init2(signature_); + (inputs_.x, inputs_.y) = U384Bench.init2(pubKey_); + + _Parameters memory params_ = _Parameters({ + a: curveParams_.a.init(), + b: curveParams_.b.init(), + gx: curveParams_.gx.init(), + gy: curveParams_.gy.init(), + p: curveParams_.p.init(), + n: curveParams_.n.init(), + lowSmax: curveParams_.lowSmax.init() + }); + + uint256 call = + useHints_ ? U384Bench.initCallWithHints(params_.p, inverseHints_) : U384Bench.initCall(params_.p); + + /// accept s only from the lower part of the curve + if ( + U384Bench.eqInteger(inputs_.r, 0) || U384Bench.cmp(inputs_.r, params_.n) >= 0 + || U384Bench.eqInteger(inputs_.s, 0) || U384Bench.cmp(inputs_.s, params_.lowSmax) > 0 + ) { + return (false, U384Bench.hintCursor(call)); + } + + if (!_isOnCurve(call, params_.p, params_.a, params_.b, inputs_.x, inputs_.y)) { + return (false, U384Bench.hintCursor(call)); + } + _checkpoint(0); + + /// allow compatibility with non-384-bit hash functions. + { + uint256 hashedMessageLength_ = hashedMessage_.length; + + if (hashedMessageLength_ < 48) { + bytes memory tmp_ = new bytes(48); + + MemoryUtils.unsafeCopy( + hashedMessage_.getDataPointer(), + tmp_.getDataPointer() + 48 - hashedMessageLength_, + hashedMessageLength_ + ); + + hashedMessage_ = tmp_; + } + } + + uint256 scalar1 = U384Bench.moddiv(call, hashedMessage_.init(), inputs_.s, params_.n); + uint256 scalar2 = U384Bench.moddiv(call, inputs_.r, inputs_.s, params_.n); + _checkpoint(1); + + { + uint256 three = U384Bench.init(3); + + /// We use 6-bit masks where the first 3 bits refer to `scalar1` and the last 3 bits refer to `scalar2`. + uint256[2][64] memory points_ = _precomputePointsTable( + call, params_.p, three, params_.a, params_.gx, params_.gy, inputs_.x, inputs_.y + ); + _checkpoint(2); + + (scalar1,) = _doubleScalarMultiplication(call, params_.p, three, params_.a, points_, scalar1, scalar2); + _checkpoint(3); + } + + U384Bench.modAssign(call, scalar1, params_.n); + _checkpoint(4); + + return (U384Bench.eq(scalar1, inputs_.r), U384Bench.hintCursor(call)); + } + } + + function _checkpoint(uint256 phase_) private { + assembly { + tstore(add(10, phase_), tload(0)) + tstore(add(20, phase_), tload(1)) + } + } + + /** + * @dev Check if a point in affine coordinates is on the curve. + */ + function _isOnCurve(uint256 call, uint256 p, uint256 a, uint256 b, uint256 x, uint256 y) private returns (bool) { + unchecked { + if (U384Bench.eqInteger(x, 0) || U384Bench.eq(x, p) || U384Bench.eqInteger(y, 0) || U384Bench.eq(y, p)) { + return false; + } + + uint256 LHS = U384Bench.modexp(call, y, 2); + uint256 RHS = U384Bench.modexp(call, x, 3); + + if (!U384Bench.eqInteger(a, 0)) { + RHS = U384Bench.modadd(RHS, U384Bench.modmul(call, x, a), p); // x^3 + a*x + } + + if (!U384Bench.eqInteger(b, 0)) { + RHS = U384Bench.modadd(RHS, b, p); // x^3 + a*x + b + } + + return U384Bench.eq(LHS, RHS); + } + } + + /** + * @dev Compute the Strauss-Shamir double scalar multiplication scalar1*G + scalar2*H. + */ + function _doubleScalarMultiplication( + uint256 call, + uint256 p, + uint256 three, + uint256 a, + uint256[2][64] memory points, + uint256 scalar1, + uint256 scalar2 + ) private returns (uint256 x, uint256 y) { + unchecked { + uint256 mask_; + uint256 scalar1Bits_; + uint256 scalar2Bits_; + + assembly { + scalar1Bits_ := mload(scalar1) + scalar2Bits_ := mload(scalar2) + } + + (x, y) = _twiceAffine(call, p, three, a, x, y); + + mask_ = ((scalar1Bits_ >> 183) << 3) | (scalar2Bits_ >> 183); + + if (mask_ != 0) { + (x, y) = _addAffine(call, p, three, a, points[mask_][0], points[mask_][1], x, y); + } + + for (uint256 word = 4; word <= 184; word += 3) { + (x, y) = _twice3Affine(call, p, three, a, x, y); + + mask_ = (((scalar1Bits_ >> (184 - word)) & 0x07) << 3) | ((scalar2Bits_ >> (184 - word)) & 0x07); + + if (mask_ != 0) { + (x, y) = _addAffine(call, p, three, a, points[mask_][0], points[mask_][1], x, y); + } + } + + assembly { + scalar1Bits_ := mload(add(scalar1, 0x20)) + scalar2Bits_ := mload(add(scalar2, 0x20)) + } + + (x, y) = _twiceAffine(call, p, three, a, x, y); + + mask_ = ((scalar1Bits_ >> 255) << 3) | (scalar2Bits_ >> 255); + + if (mask_ != 0) { + (x, y) = _addAffine(call, p, three, a, points[mask_][0], points[mask_][1], x, y); + } + + for (uint256 word = 4; word <= 256; word += 3) { + (x, y) = _twice3Affine(call, p, three, a, x, y); + + mask_ = (((scalar1Bits_ >> (256 - word)) & 0x07) << 3) | ((scalar2Bits_ >> (256 - word)) & 0x07); + + if (mask_ != 0) { + (x, y) = _addAffine(call, p, three, a, points[mask_][0], points[mask_][1], x, y); + } + } + } + } + + /** + * @dev Double an elliptic curve point in affine coordinates. + */ + function _twiceAffine(uint256 call, uint256 p, uint256 three, uint256 a, uint256 x1, uint256 y1) + private + returns (uint256 x2, uint256 y2) + { + unchecked { + if (x1 == 0) { + return (0, 0); + } + + if (U384Bench.eqInteger(y1, 0)) { + return (0, 0); + } + + uint256 m1 = U384Bench.modexp(call, x1, 2); + U384Bench.modmulAssign(call, m1, three); + U384Bench.modaddAssign(m1, a, p); + + uint256 m2 = U384Bench.modshl1(y1, p); + U384Bench.moddivAssign(call, m1, m2); + + x2 = U384Bench.modexp(call, m1, 2); + U384Bench.modsubAssign(x2, x1, p); + U384Bench.modsubAssign(x2, x1, p); + + y2 = U384Bench.modsub(x1, x2, p); + U384Bench.modmulAssign(call, y2, m1); + U384Bench.modsubAssign(y2, y1, p); + } + } + + /** + * @dev Doubles an elliptic curve point 3 times in affine coordinates. + */ + function _twice3Affine(uint256 call, uint256 p, uint256 three, uint256 a, uint256 x1, uint256 y1) + private + returns (uint256 x2, uint256 y2) + { + unchecked { + if (x1 == 0) { + return (0, 0); + } + + if (U384Bench.eqInteger(y1, 0)) { + return (0, 0); + } + + uint256 m1 = U384Bench.modexp(call, x1, 2); + U384Bench.modmulAssign(call, m1, three); + U384Bench.modaddAssign(m1, a, p); + + uint256 m2 = U384Bench.modshl1(y1, p); + U384Bench.moddivAssign(call, m1, m2); + + x2 = U384Bench.modexp(call, m1, 2); + U384Bench.modsubAssign(x2, x1, p); + U384Bench.modsubAssign(x2, x1, p); + + y2 = U384Bench.modsub(x1, x2, p); + U384Bench.modmulAssign(call, y2, m1); + U384Bench.modsubAssign(y2, y1, p); + + if (U384Bench.eqInteger(y2, 0)) { + return (0, 0); + } + + U384Bench.modexpAssignTo(call, m1, x2, 2); + U384Bench.modmulAssign(call, m1, three); + U384Bench.modaddAssign(m1, a, p); + + U384Bench.modshl1AssignTo(m2, y2, p); + U384Bench.moddivAssign(call, m1, m2); + + U384Bench.modexpAssignTo(call, x1, m1, 2); + U384Bench.modsubAssign(x1, x2, p); + U384Bench.modsubAssign(x1, x2, p); + + U384Bench.modsubAssignTo(y1, x2, x1, p); + U384Bench.modmulAssign(call, y1, m1); + U384Bench.modsubAssign(y1, y2, p); + + if (U384Bench.eqInteger(y1, 0)) { + return (0, 0); + } + + U384Bench.modexpAssignTo(call, m1, x1, 2); + U384Bench.modmulAssign(call, m1, three); + U384Bench.modaddAssign(m1, a, p); + + U384Bench.modshl1AssignTo(m2, y1, p); + U384Bench.moddivAssign(call, m1, m2); + + U384Bench.modexpAssignTo(call, x2, m1, 2); + U384Bench.modsubAssign(x2, x1, p); + U384Bench.modsubAssign(x2, x1, p); + + U384Bench.modsubAssignTo(y2, x1, x2, p); + U384Bench.modmulAssign(call, y2, m1); + U384Bench.modsubAssign(y2, y1, p); + } + } + + /** + * @dev Add two elliptic curve points in affine coordinates. + */ + function _addAffine( + uint256 call, + uint256 p, + uint256 three, + uint256 a, + uint256 x1, + uint256 y1, + uint256 x2, + uint256 y2 + ) private returns (uint256 x3, uint256 y3) { + unchecked { + if (x1 == 0 || x2 == 0) { + if (x1 == 0 && x2 == 0) { + return (0, 0); + } + + return x1 == 0 ? (x2.copy(), y2.copy()) : (x1.copy(), y1.copy()); + } + + if (U384Bench.eq(x1, x2)) { + if (U384Bench.eq(y1, y2)) { + return _twiceAffine(call, p, three, a, x1, y1); + } + + return (0, 0); + } + + uint256 m1 = U384Bench.modsub(y1, y2, p); + uint256 m2 = U384Bench.modsub(x1, x2, p); + + U384Bench.moddivAssign(call, m1, m2); + + x3 = U384Bench.modexp(call, m1, 2); + U384Bench.modsubAssign(x3, x1, p); + U384Bench.modsubAssign(x3, x2, p); + + y3 = U384Bench.modsub(x1, x3, p); + U384Bench.modmulAssign(call, y3, m1); + U384Bench.modsubAssign(y3, y1, p); + } + } + + function _precomputePointsTable( + uint256 call, + uint256 p, + uint256 three, + uint256 a, + uint256 gx, + uint256 gy, + uint256 hx, + uint256 hy + ) private returns (uint256[2][64] memory points_) { + unchecked { + (points_[0x01][0], points_[0x01][1]) = (hx.copy(), hy.copy()); + (points_[0x08][0], points_[0x08][1]) = (gx.copy(), gy.copy()); + + for (uint256 i = 0; i < 8; ++i) { + for (uint256 j = 0; j < 8; ++j) { + if (i + j < 2) { + continue; + } + + uint256 maskTo = (i << 3) | j; + + if (i != 0) { + uint256 maskFrom = ((i - 1) << 3) | j; + + (points_[maskTo][0], points_[maskTo][1]) = + _addAffine(call, p, three, a, points_[maskFrom][0], points_[maskFrom][1], gx, gy); + } else { + uint256 maskFrom = (i << 3) | (j - 1); + + (points_[maskTo][0], points_[maskTo][1]) = + _addAffine(call, p, three, a, points_[maskFrom][0], points_[maskFrom][1], hx, hy); + } + } + } + } + } +} + +/** + * @notice Low-level utility library that implements unsigned 384-bit arithmetics. + * + * Should not be used outside of this file. + */ +library U384Bench { + uint256 private constant SHORT_ALLOCATION = 64; + + uint256 private constant MUL_OFFSET = 288; + uint256 private constant EXP_OFFSET = 2 * 288; + uint256 private constant INV_OFFSET = 3 * 288; + uint256 private constant HINT_DATA_OFFSET = 0x480; + uint256 private constant HINT_LENGTH_OFFSET = 0x4A0; + uint256 private constant HINT_CURSOR_OFFSET = 0x4C0; + uint256 private constant HINT_ENABLED_OFFSET = 0x4E0; + uint256 private constant CALL_ALLOCATION = 0x500; + + function init(uint256 from_) internal returns (uint256 handler_) { + unchecked { + handler_ = _allocate(SHORT_ALLOCATION); + + assembly { + mstore(handler_, 0x00) + mstore(add(0x20, handler_), from_) + } + + return handler_; + } + } + + function init(bytes memory from_) internal returns (uint256 handler_) { + unchecked { + require(from_.length == 48, "U384: not 384"); + + handler_ = _allocate(SHORT_ALLOCATION); + + assembly { + mstore(handler_, 0x00) + mstore(add(handler_, 0x10), mload(add(from_, 0x20))) + mstore(add(handler_, 0x20), mload(add(from_, 0x30))) + } + + return handler_; + } + } + + function init2(bytes memory from2_) internal returns (uint256 handler1_, uint256 handler2_) { + unchecked { + require(from2_.length == 96, "U384: not 768"); + + handler1_ = _allocate(SHORT_ALLOCATION); + handler2_ = _allocate(SHORT_ALLOCATION); + + assembly { + mstore(handler1_, 0x00) + mstore(add(handler1_, 0x10), mload(add(from2_, 0x20))) + mstore(add(handler1_, 0x20), mload(add(from2_, 0x30))) + + mstore(handler2_, 0x00) + mstore(add(handler2_, 0x10), mload(add(from2_, 0x50))) + mstore(add(handler2_, 0x20), mload(add(from2_, 0x60))) + } + + return (handler1_, handler2_); + } + } + + function initCall(uint256 m_) internal returns (uint256 handler_) { + unchecked { + handler_ = _allocate(CALL_ALLOCATION); + + _sub(m_, init(2), handler_ + INV_OFFSET + 0xA0); + + assembly { + let call_ := add(handler_, MUL_OFFSET) + + mstore(call_, 0x60) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0xC0, call_), 0x01) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + call_ := add(handler_, EXP_OFFSET) + + mstore(call_, 0x40) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0xC0, call_), mload(m_)) + mstore(add(0xE0, call_), mload(add(m_, 0x20))) + + call_ := add(handler_, INV_OFFSET) + + mstore(call_, 0x40) + mstore(add(0x20, call_), 0x40) + mstore(add(0x40, call_), 0x40) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + mstore(add(handler_, HINT_DATA_OFFSET), 0) + mstore(add(handler_, HINT_LENGTH_OFFSET), 0) + mstore(add(handler_, HINT_CURSOR_OFFSET), 0) + mstore(add(handler_, HINT_ENABLED_OFFSET), 0) + } + } + } + + function initCallWithHints(uint256 m_, bytes memory inverseHints_) internal returns (uint256 handler_) { + handler_ = initCall(m_); + + assembly { + mstore(add(handler_, HINT_DATA_OFFSET), add(inverseHints_, 0x20)) + mstore(add(handler_, HINT_LENGTH_OFFSET), mload(inverseHints_)) + mstore(add(handler_, HINT_CURSOR_OFFSET), 0) + mstore(add(handler_, HINT_ENABLED_OFFSET), 1) + } + } + + function hintCursor(uint256 call_) internal returns (uint256 cursor_) { + assembly { + cursor_ := mload(add(call_, HINT_CURSOR_OFFSET)) + } + } + + function copy(uint256 handler_) internal returns (uint256 handlerCopy_) { + unchecked { + handlerCopy_ = _allocate(SHORT_ALLOCATION); + + assembly { + mstore(handlerCopy_, mload(handler_)) + mstore(add(handlerCopy_, 0x20), mload(add(handler_, 0x20))) + } + + return handlerCopy_; + } + } + + function eq(uint256 a_, uint256 b_) internal returns (bool eq_) { + assembly { + eq_ := and(eq(mload(a_), mload(b_)), eq(mload(add(a_, 0x20)), mload(add(b_, 0x20)))) + } + } + + function eqInteger(uint256 a_, uint256 bInteger_) internal returns (bool eq_) { + assembly { + eq_ := and(eq(mload(a_), 0), eq(mload(add(a_, 0x20)), bInteger_)) + } + } + + function cmp(uint256 a_, uint256 b_) internal returns (int256 cmp_) { + unchecked { + uint256 aWord_; + uint256 bWord_; + + assembly { + aWord_ := mload(a_) + bWord_ := mload(b_) + } + + if (aWord_ > bWord_) { + return 1; + } + + if (aWord_ < bWord_) { + return -1; + } + + assembly { + aWord_ := mload(add(a_, 0x20)) + bWord_ := mload(add(b_, 0x20)) + } + + if (aWord_ > bWord_) { + return 1; + } + + if (aWord_ < bWord_) { + return -1; + } + } + } + + function modAssign(uint256 call_, uint256 a_, uint256 m_) internal { + assembly { + mstore(call_, 0x40) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0x60, call_), mload(a_)) + mstore(add(0x80, call_), mload(add(a_, 0x20))) + mstore(add(0xA0, call_), 0x01) + mstore(add(0xC0, call_), mload(m_)) + mstore(add(0xE0, call_), mload(add(m_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0100, a_, 0x40)) + tstore(1, add(tload(1), 1)) + } + } + + function modexp(uint256 call_, uint256 b_, uint256 eInteger_) internal returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + assembly { + call_ := add(call_, EXP_OFFSET) + + mstore(add(0x60, call_), mload(b_)) + mstore(add(0x80, call_), mload(add(b_, 0x20))) + mstore(add(0xA0, call_), eInteger_) + + pop(staticcall(gas(), 0x5, call_, 0x0100, r_, 0x40)) + tstore(1, add(tload(1), 1)) + } + + return r_; + } + } + + function modexpAssignTo(uint256 call_, uint256 to_, uint256 b_, uint256 eInteger_) internal { + assembly { + call_ := add(call_, EXP_OFFSET) + + mstore(add(0x60, call_), mload(b_)) + mstore(add(0x80, call_), mload(add(b_, 0x20))) + mstore(add(0xA0, call_), eInteger_) + + pop(staticcall(gas(), 0x5, call_, 0x0100, to_, 0x40)) + tstore(1, add(tload(1), 1)) + } + } + + function modadd(uint256 a_, uint256 b_, uint256 m_) internal returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + _add(a_, b_, r_); + + if (cmp(r_, m_) >= 0) { + _subFrom(r_, m_); + } + + return r_; + } + } + + function modaddAssign(uint256 a_, uint256 b_, uint256 m_) internal { + unchecked { + _addTo(a_, b_); + + if (cmp(a_, m_) >= 0) { + return _subFrom(a_, m_); + } + } + } + + function modmul(uint256 call_, uint256 a_, uint256 b_) internal returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + _mul(a_, b_, call_ + MUL_OFFSET + 0x60); + + assembly { + call_ := add(call_, MUL_OFFSET) + + pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) + tstore(1, add(tload(1), 1)) + } + + return r_; + } + } + + function modmulAssign(uint256 call_, uint256 a_, uint256 b_) internal { + unchecked { + _mul(a_, b_, call_ + MUL_OFFSET + 0x60); + + assembly { + call_ := add(call_, MUL_OFFSET) + + pop(staticcall(gas(), 0x5, call_, 0x0120, a_, 0x40)) + tstore(1, add(tload(1), 1)) + } + } + } + + function modsub(uint256 a_, uint256 b_, uint256 m_) internal returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + if (cmp(a_, b_) >= 0) { + _sub(a_, b_, r_); + return r_; + } + + _add(a_, m_, r_); + _subFrom(r_, b_); + } + } + + function modsubAssign(uint256 a_, uint256 b_, uint256 m_) internal { + unchecked { + if (cmp(a_, b_) >= 0) { + _subFrom(a_, b_); + return; + } + + _addTo(a_, m_); + _subFrom(a_, b_); + } + } + + function modsubAssignTo(uint256 to_, uint256 a_, uint256 b_, uint256 m_) internal { + unchecked { + if (cmp(a_, b_) >= 0) { + _sub(a_, b_, to_); + return; + } + + _add(a_, m_, to_); + _subFrom(to_, b_); + } + } + + function modshl1(uint256 a_, uint256 m_) internal returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + _shl1(a_, r_); + + if (cmp(r_, m_) >= 0) { + _subFrom(r_, m_); + } + + return r_; + } + } + + function modshl1AssignTo(uint256 to_, uint256 a_, uint256 m_) internal { + unchecked { + _shl1(a_, to_); + + if (cmp(to_, m_) >= 0) { + _subFrom(to_, m_); + } + } + } + + /// @dev Stores modinv into `b_` and moddiv into `a_`. + function moddivAssign(uint256 call_, uint256 a_, uint256 b_) internal { + unchecked { + uint256 baseCall_ = call_; + if (_hintsEnabled(call_)) { + uint256 inv_ = _nextInverseHint(call_); + uint256 check_ = modmul(call_, b_, inv_); + require(eqInteger(check_, 1), "bad inverse hint"); + + assembly { + mstore(b_, mload(inv_)) + mstore(add(b_, 0x20), mload(add(inv_, 0x20))) + } + } else { + assembly { + call_ := add(call_, INV_OFFSET) + + mstore(add(0x60, call_), mload(b_)) + mstore(add(0x80, call_), mload(add(b_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0120, b_, 0x40)) + tstore(0, add(tload(0), 1)) + } + _collectInverse(b_); + } + + modmulAssign(baseCall_, a_, b_); + } + } + + function moddiv(uint256 call_, uint256 a_, uint256 b_, uint256 m_) internal returns (uint256 r_) { + unchecked { + r_ = modinv(call_, b_, m_); + + _mul(a_, r_, call_ + 0x60); + + assembly { + mstore(call_, 0x60) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0xC0, call_), 0x01) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) + tstore(1, add(tload(1), 1)) + } + } + } + + function modinv(uint256 call_, uint256 b_, uint256 m_) internal returns (uint256 r_) { + unchecked { + if (_hintsEnabled(call_)) { + r_ = _nextInverseHint(call_); + uint256 check_ = _modmulWithMod(call_, b_, r_, m_); + require(eqInteger(check_, 1), "bad inverse hint"); + return r_; + } + + r_ = _allocate(SHORT_ALLOCATION); + + _sub(m_, init(2), call_ + 0xA0); + + assembly { + mstore(call_, 0x40) + mstore(add(0x20, call_), 0x40) + mstore(add(0x40, call_), 0x40) + mstore(add(0x60, call_), mload(b_)) + mstore(add(0x80, call_), mload(add(b_, 0x20))) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) + tstore(0, add(tload(0), 1)) + } + _collectInverse(r_); + } + } + + function _hintsEnabled(uint256 call_) private returns (bool enabled_) { + assembly { + enabled_ := mload(add(call_, HINT_ENABLED_OFFSET)) + } + } + + function _nextInverseHint(uint256 call_) private returns (uint256 r_) { + uint256 cursor_; + uint256 length_; + assembly { + cursor_ := mload(add(call_, HINT_CURSOR_OFFSET)) + length_ := mload(add(call_, HINT_LENGTH_OFFSET)) + } + require(cursor_ + 48 <= length_, "inverse hint underflow"); + + r_ = _allocate(SHORT_ALLOCATION); + + assembly { + let src_ := add(mload(add(call_, HINT_DATA_OFFSET)), cursor_) + mstore(r_, shr(128, mload(src_))) + mstore(add(r_, 0x20), mload(add(src_, 0x10))) + mstore(add(call_, HINT_CURSOR_OFFSET), add(cursor_, 48)) + } + } + + function _collectInverse(uint256 inv_) private { + assembly { + if tload(8) { + let index_ := tload(2) + let slot_ := add(1000, mul(index_, 2)) + tstore(slot_, mload(inv_)) + tstore(add(slot_, 1), mload(add(inv_, 0x20))) + tstore(2, add(index_, 1)) + } + } + } + + function _modmulWithMod(uint256 call_, uint256 a_, uint256 b_, uint256 m_) private returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + _mul(a_, b_, call_ + 0x60); + + assembly { + mstore(call_, 0x60) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0xC0, call_), 0x01) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) + tstore(1, add(tload(1), 1)) + } + } + } + + function _shl1(uint256 a_, uint256 r_) internal { + assembly { + let a1_ := mload(add(a_, 0x20)) + + mstore(r_, or(shl(1, mload(a_)), shr(255, a1_))) + mstore(add(r_, 0x20), shl(1, a1_)) + } + } + + function _add(uint256 a_, uint256 b_, uint256 r_) private { + assembly { + let aWord_ := mload(add(a_, 0x20)) + let sum_ := add(aWord_, mload(add(b_, 0x20))) + + mstore(add(r_, 0x20), sum_) + + sum_ := gt(aWord_, sum_) + sum_ := add(sum_, add(mload(a_), mload(b_))) + + mstore(r_, sum_) + } + } + + function _sub(uint256 a_, uint256 b_, uint256 r_) private { + assembly { + let aWord_ := mload(add(a_, 0x20)) + let diff_ := sub(aWord_, mload(add(b_, 0x20))) + + mstore(add(r_, 0x20), diff_) + + diff_ := gt(diff_, aWord_) + diff_ := sub(sub(mload(a_), mload(b_)), diff_) + + mstore(r_, diff_) + } + } + + function _subFrom(uint256 a_, uint256 b_) private { + assembly { + let aWord_ := mload(add(a_, 0x20)) + let diff_ := sub(aWord_, mload(add(b_, 0x20))) + + mstore(add(a_, 0x20), diff_) + + diff_ := gt(diff_, aWord_) + diff_ := sub(sub(mload(a_), mload(b_)), diff_) + + mstore(a_, diff_) + } + } + + function _addTo(uint256 a_, uint256 b_) private { + assembly { + let aWord_ := mload(add(a_, 0x20)) + let sum_ := add(aWord_, mload(add(b_, 0x20))) + + mstore(add(a_, 0x20), sum_) + + sum_ := gt(aWord_, sum_) + sum_ := add(sum_, add(mload(a_), mload(b_))) + + mstore(a_, sum_) + } + } + + function _mul(uint256 a_, uint256 b_, uint256 r_) private { + assembly { + let a0_ := mload(a_) + let a1_ := shr(128, mload(add(a_, 0x20))) + let a2_ := and(mload(add(a_, 0x20)), 0xffffffffffffffffffffffffffffffff) + + let b0_ := mload(b_) + let b1_ := shr(128, mload(add(b_, 0x20))) + let b2_ := and(mload(add(b_, 0x20)), 0xffffffffffffffffffffffffffffffff) + + // r5 + let current_ := mul(a2_, b2_) + let r0_ := and(current_, 0xffffffffffffffffffffffffffffffff) + + // r4 + current_ := shr(128, current_) + + let temp_ := mul(a1_, b2_) + current_ := add(current_, temp_) + let curry_ := lt(current_, temp_) + + temp_ := mul(a2_, b1_) + current_ := add(current_, temp_) + curry_ := add(curry_, lt(current_, temp_)) + + mstore(add(r_, 0x40), add(shl(128, current_), r0_)) + + // r3 + current_ := add(shl(128, curry_), shr(128, current_)) + curry_ := 0 + + temp_ := mul(a0_, b2_) + current_ := add(current_, temp_) + curry_ := lt(current_, temp_) + + temp_ := mul(a1_, b1_) + current_ := add(current_, temp_) + curry_ := add(curry_, lt(current_, temp_)) + + temp_ := mul(a2_, b0_) + current_ := add(current_, temp_) + curry_ := add(curry_, lt(current_, temp_)) + + r0_ := and(current_, 0xffffffffffffffffffffffffffffffff) + + // r2 + current_ := add(shl(128, curry_), shr(128, current_)) + curry_ := 0 + + temp_ := mul(a0_, b1_) + current_ := add(current_, temp_) + curry_ := lt(current_, temp_) + + temp_ := mul(a1_, b0_) + current_ := add(current_, temp_) + curry_ := add(curry_, lt(current_, temp_)) + + mstore(add(r_, 0x20), add(shl(128, current_), r0_)) + + // r1 + current_ := add(shl(128, curry_), shr(128, current_)) + current_ := add(current_, mul(a0_, b0_)) + + mstore(r_, current_) + } + } + + function _allocate(uint256 bytes_) private returns (uint256 handler_) { + unchecked { + assembly { + handler_ := mload(0x40) + mstore(0x40, add(handler_, bytes_)) + } + + return handler_; + } + } +} diff --git a/test/bench/HintedNitroBench.sol b/test/bench/HintedNitroBench.sol new file mode 100644 index 0000000..7cfca6f --- /dev/null +++ b/test/bench/HintedNitroBench.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Asn1Decode, Asn1Ptr, LibAsn1Ptr} from "../../src/Asn1Decode.sol"; +import {CborDecode, CborElement, LibCborElement} from "../../src/CborDecode.sol"; +import {CertManager} from "../../src/CertManager.sol"; +import {ECDSA384Curve} from "../../src/ECDSA384Curve.sol"; +import {ICertManager} from "../../src/ICertManager.sol"; +import {LibBytes} from "../../src/LibBytes.sol"; +import {NitroValidator} from "../../src/NitroValidator.sol"; +import {Sha2Ext} from "../../src/Sha2Ext.sol"; +import {ECDSA384Bench} from "./ECDSA384Bench.sol"; + +contract HintedCertManagerBench is CertManager { + using Asn1Decode for bytes; + using LibAsn1Ptr for Asn1Ptr; + using LibBytes for bytes; + + function verifyCACertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) + external + returns (bytes32) + { + bytes32 certHash = keccak256(cert); + _verifyCertWithHints(cert, certHash, true, _loadVerified(parentCertHash), signatureHints); + return certHash; + } + + function verifyClientCertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) + external + returns (VerifiedCert memory) + { + return _verifyCertWithHints(cert, keccak256(cert), false, _loadVerified(parentCertHash), signatureHints); + } + + function loadVerifiedForBench(bytes32 certHash) external view returns (VerifiedCert memory) { + return _loadVerified(certHash); + } + + function _verifyCertWithHints( + bytes memory certificate, + bytes32 certHash, + bool ca, + VerifiedCert memory parent, + bytes memory signatureHints + ) internal returns (VerifiedCert memory cert) { + if (certHash != ROOT_CA_CERT_HASH) { + require(parent.pubKey.length > 0, "parent cert unverified"); + require(parent.notAfter >= block.timestamp, "parent cert expired"); + require(parent.ca, "parent cert is not a CA"); + require(!ca || parent.maxPathLen != 0, "maxPathLen exceeded"); + } + + cert = _loadVerified(certHash); + if (cert.pubKey.length != 0) { + require(cert.notAfter >= block.timestamp, "cert expired"); + require(cert.ca == ca, "cert is not a CA"); + return cert; + } + + Asn1Ptr root = certificate.root(); + Asn1Ptr tbsCertPtr = certificate.firstChildOf(root); + (uint64 notAfter, int64 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey) = + _parseTbs(certificate, tbsCertPtr, ca); + + require(parent.subjectHash == issuerHash, "issuer / subject mismatch"); + + if (parent.maxPathLen > 0 && (maxPathLen < 0 || maxPathLen >= parent.maxPathLen)) { + maxPathLen = parent.maxPathLen - 1; + } + + _verifyCertSignatureWithHints(certificate, tbsCertPtr, parent.pubKey, signatureHints); + + cert = VerifiedCert({ + ca: ca, notAfter: notAfter, maxPathLen: maxPathLen, subjectHash: subjectHash, pubKey: pubKey + }); + _saveVerified(certHash, cert); + + emit CertVerified(certHash); + } + + function _verifyCertSignatureWithHints( + bytes memory certificate, + Asn1Ptr ptr, + bytes memory pubKey, + bytes memory signatureHints + ) internal { + Asn1Ptr sigAlgoPtr = certificate.nextSiblingOf(ptr); + require(certificate.keccak(sigAlgoPtr.content(), sigAlgoPtr.length()) == CERT_ALGO_OID, "invalid cert sig algo"); + + bytes memory hash = Sha2Ext.sha384(certificate, ptr.header(), ptr.totalLength()); + bytes memory sigPacked = _certSignature(certificate, sigAlgoPtr); + + require(ECDSA384Bench.verifyWithHints(_benchParams(), hash, sigPacked, pubKey, signatureHints), "invalid sig"); + } + + function _certSignature(bytes memory certificate, Asn1Ptr sigAlgoPtr) + internal + pure + returns (bytes memory sigPacked) + { + Asn1Ptr sigPtr = certificate.nextSiblingOf(sigAlgoPtr); + Asn1Ptr sigBPtr = certificate.bitstring(sigPtr); + Asn1Ptr sigRoot = certificate.rootOf(sigBPtr); + Asn1Ptr sigRPtr = certificate.firstChildOf(sigRoot); + Asn1Ptr sigSPtr = certificate.nextSiblingOf(sigRPtr); + (uint128 rhi, uint256 rlo) = certificate.uint384At(sigRPtr); + (uint128 shi, uint256 slo) = certificate.uint384At(sigSPtr); + sigPacked = abi.encodePacked(rhi, rlo, shi, slo); + } + + function _benchParams() internal pure returns (ECDSA384Bench.Parameters memory) { + return ECDSA384Bench.Parameters({ + a: ECDSA384Curve.CURVE_A, + b: ECDSA384Curve.CURVE_B, + gx: ECDSA384Curve.CURVE_GX, + gy: ECDSA384Curve.CURVE_GY, + p: ECDSA384Curve.CURVE_P, + n: ECDSA384Curve.CURVE_N, + lowSmax: ECDSA384Curve.CURVE_LOW_S_MAX + }); + } +} + +contract P384HintCollectorBench { + using Asn1Decode for bytes; + using LibAsn1Ptr for Asn1Ptr; + + function collectVerifyHints(bytes memory hash, bytes memory signature, bytes memory pubKey) + public + returns (bytes memory hints) + { + (hints,) = collectVerifyProfile(hash, signature, pubKey); + } + + function collectVerifyProfile(bytes memory hash, bytes memory signature, bytes memory pubKey) + public + returns (bytes memory hints, uint256 hintedOtherCalls) + { + assembly { + tstore(0, 0) + tstore(1, 0) + tstore(2, 0) + tstore(7, 0) + tstore(8, 1) + } + + bool ok = ECDSA384Bench.verify(_benchParams(), hash, signature, pubKey); + require(ok, "collect verify failed"); + + uint256 count; + uint256 inv; + uint256 other; + assembly { + count := tload(2) + inv := tload(0) + other := tload(1) + tstore(8, 0) + } + require(count == inv, "inverse collection mismatch"); + hintedOtherCalls = inv + other; + + hints = new bytes(count * 48); + for (uint256 i = 0; i < count; ++i) { + uint256 hi; + uint256 lo; + assembly { + let slot_ := add(1000, mul(i, 2)) + hi := tload(slot_) + lo := tload(add(slot_, 1)) + let dst_ := add(add(hints, 0x20), mul(i, 48)) + mstore(dst_, shl(128, hi)) + mstore(add(dst_, 0x10), lo) + } + } + } + + function collectCertSignatureHints(bytes memory certificate, bytes memory parentPubKey) + external + returns (bytes memory) + { + (bytes memory hints,) = collectCertSignatureProfile(certificate, parentPubKey); + return hints; + } + + function collectCertSignatureProfile(bytes memory certificate, bytes memory parentPubKey) + public + returns (bytes memory, uint256) + { + Asn1Ptr root = certificate.root(); + Asn1Ptr tbsCertPtr = certificate.firstChildOf(root); + bytes memory hash = Sha2Ext.sha384(certificate, tbsCertPtr.header(), tbsCertPtr.totalLength()); + + Asn1Ptr sigAlgoPtr = certificate.nextSiblingOf(tbsCertPtr); + bytes memory sigPacked = _certSignature(certificate, sigAlgoPtr); + + return collectVerifyProfile(hash, sigPacked, parentPubKey); + } + + function _certSignature(bytes memory certificate, Asn1Ptr sigAlgoPtr) + internal + pure + returns (bytes memory sigPacked) + { + Asn1Ptr sigPtr = certificate.nextSiblingOf(sigAlgoPtr); + Asn1Ptr sigBPtr = certificate.bitstring(sigPtr); + Asn1Ptr sigRoot = certificate.rootOf(sigBPtr); + Asn1Ptr sigRPtr = certificate.firstChildOf(sigRoot); + Asn1Ptr sigSPtr = certificate.nextSiblingOf(sigRPtr); + (uint128 rhi, uint256 rlo) = certificate.uint384At(sigRPtr); + (uint128 shi, uint256 slo) = certificate.uint384At(sigSPtr); + sigPacked = abi.encodePacked(rhi, rlo, shi, slo); + } + + function _benchParams() internal pure returns (ECDSA384Bench.Parameters memory) { + return ECDSA384Bench.Parameters({ + a: ECDSA384Curve.CURVE_A, + b: ECDSA384Curve.CURVE_B, + gx: ECDSA384Curve.CURVE_GX, + gy: ECDSA384Curve.CURVE_GY, + p: ECDSA384Curve.CURVE_P, + n: ECDSA384Curve.CURVE_N, + lowSmax: ECDSA384Curve.CURVE_LOW_S_MAX + }); + } +} + +contract HintedNitroValidatorBench is NitroValidator { + using CborDecode for bytes; + using LibBytes for bytes; + using LibCborElement for CborElement; + + constructor(ICertManager certManager_) NitroValidator(certManager_) {} + + function validateAttestationWithHints( + bytes memory attestationTbs, + bytes memory signature, + bytes memory attestationSigHints + ) public returns (Ptrs memory) { + Ptrs memory ptrs = _parseAttestation(attestationTbs); + + require(ptrs.moduleID.length() > 0, "no module id"); + require(ptrs.timestamp > 0, "no timestamp"); + require(ptrs.cabundle.length > 0, "no cabundle"); + require(attestationTbs.keccak(ptrs.digest) == ATTESTATION_DIGEST, "invalid digest"); + require(1 <= ptrs.pcrs.length && ptrs.pcrs.length <= 32, "invalid pcrs"); + require( + ptrs.publicKey.isNull() || (1 <= ptrs.publicKey.length() && ptrs.publicKey.length() <= 1024), + "invalid pub key" + ); + require(ptrs.userData.isNull() || (ptrs.userData.length() <= 512), "invalid user data"); + require(ptrs.nonce.isNull() || (ptrs.nonce.length() <= 512), "invalid nonce"); + + for (uint256 i = 0; i < ptrs.pcrs.length; i++) { + require( + ptrs.pcrs[i].length() == 32 || ptrs.pcrs[i].length() == 48 || ptrs.pcrs[i].length() == 64, "invalid pcr" + ); + } + + bytes memory cert = attestationTbs.slice(ptrs.cert); + bytes[] memory cabundle = new bytes[](ptrs.cabundle.length); + for (uint256 i = 0; i < ptrs.cabundle.length; i++) { + require(1 <= ptrs.cabundle[i].length() && ptrs.cabundle[i].length() <= 1024, "invalid cabundle cert"); + cabundle[i] = attestationTbs.slice(ptrs.cabundle[i]); + } + + ICertManager.VerifiedCert memory parent = verifyCertBundle(cert, cabundle); + bytes memory hash = Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length); + _verifySignatureWithHints(parent.pubKey, hash, signature, attestationSigHints); + + return ptrs; + } + + function _verifySignatureWithHints( + bytes memory pubKey, + bytes memory hash, + bytes memory sig, + bytes memory signatureHints + ) internal { + require(ECDSA384Bench.verifyWithHints(_benchParams(), hash, sig, pubKey, signatureHints), "invalid sig"); + } + + function _benchParams() internal pure returns (ECDSA384Bench.Parameters memory) { + return ECDSA384Bench.Parameters({ + a: ECDSA384Curve.CURVE_A, + b: ECDSA384Curve.CURVE_B, + gx: ECDSA384Curve.CURVE_GX, + gy: ECDSA384Curve.CURVE_GY, + p: ECDSA384Curve.CURVE_P, + n: ECDSA384Curve.CURVE_N, + lowSmax: ECDSA384Curve.CURVE_LOW_S_MAX + }); + } +} diff --git a/test/bench/RealAttestationBench.t.sol b/test/bench/RealAttestationBench.t.sol new file mode 100644 index 0000000..293abc6 --- /dev/null +++ b/test/bench/RealAttestationBench.t.sol @@ -0,0 +1,676 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Test, console} from "forge-std/Test.sol"; +import {NitroValidator} from "../../src/NitroValidator.sol"; +import {CertManager} from "../../src/CertManager.sol"; +import {CertManagerHinted} from "../../src/CertManagerHinted.sol"; +import {ICertManager} from "../../src/ICertManager.sol"; +import {CborDecode} from "../../src/CborDecode.sol"; +import {NitroValidatorHinted} from "../../src/NitroValidatorHinted.sol"; +import {P384Verifier} from "../../src/P384Verifier.sol"; +import {Sha2Ext} from "../../src/Sha2Ext.sol"; +import {P384HintCollectorBench} from "./HintedNitroBench.sol"; + +contract NitroValidatorParseHarness is NitroValidator { + constructor(CertManager certManager) NitroValidator(certManager) {} + + function parseAttestation(bytes memory attestationTbs) external pure returns (Ptrs memory) { + return _parseAttestation(attestationTbs); + } +} + +contract RealAttestationBenchTest is Test { + using CborDecode for bytes; + + uint256 constant P384_VERIFY_2565 = 7_938_921; + uint256 constant P384_VERIFY_7883 = 50_646_861; + uint256 constant HINTED_P384_VERIFY_7883_WITH_CALLDATA = 6_040_809; + uint256 constant HINTED_MODEXP_FLOOR_DELTA = 300; // EIP-7883 floor 500 - EIP-2565 floor 200 + uint256 constant TX_CAP = 16_777_216; + uint256 constant EIP170_RUNTIME_LIMIT = 24_576; + + CertManager certManager; + NitroValidator validator; + NitroValidatorParseHarness parser; + CertManagerHinted hintedCertManager; + NitroValidatorHinted hintedValidator; + P384Verifier p384Verifier; + P384HintCollectorBench hintCollector; + + struct SequenceSummary { + ICertManager.VerifiedCert leaf; + uint256 txCount; + uint256 totalCurrentGas; + uint256 totalProjectedGas; + uint256 maxProjectedGas; + } + + struct TxGas { + uint256 currentGas; + uint256 projectedGas; + } + + function setUp() public { + vm.warp(1767472867); // 2026-01-03T20:41:07Z, matching the attestation timestamp. + certManager = new CertManager(); + validator = new NitroValidator(certManager); + parser = new NitroValidatorParseHarness(certManager); + p384Verifier = new P384Verifier(); + hintedCertManager = new CertManagerHinted(p384Verifier); + hintedValidator = new NitroValidatorHinted(hintedCertManager, p384Verifier); + hintCollector = new P384HintCollectorBench(); + } + + function test_RealAttestationBaseline() public { + bytes memory attestation = _decodeBase64(_realAttestationB64()); + attestation = _repairMissingPublicKeyBytes(attestation); + + uint256 g0 = gasleft(); + (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); + uint256 decodeGas = g0 - gasleft(); + + g0 = gasleft(); + validator.validateAttestation(attestationTbs, signature); + uint256 coldValidateGas = g0 - gasleft(); + + g0 = gasleft(); + validator.validateAttestation(attestationTbs, signature); + uint256 cachedValidateGas = g0 - gasleft(); + uint256 cachedPostFusakaUnoptimized = cachedValidateGas + (P384_VERIFY_7883 - P384_VERIFY_2565); + uint256 cachedPostFusakaHinted = cachedValidateGas - P384_VERIFY_2565 + HINTED_P384_VERIFY_7883_WITH_CALLDATA; + + console.log("==== REAL ATTESTATION BASELINE ===="); + console.log("attestation bytes :", attestation.length); + console.log("attestationTbs bytes :", attestationTbs.length); + console.log("signature bytes :", signature.length); + console.log("decodeAttestationTbs gas :", decodeGas); + console.log("validateAttestation gas (cold) :", coldValidateGas); + console.log("validateAttestation gas (cached) :", cachedValidateGas); + console.log("cached post-Fusaka unoptimized :", cachedPostFusakaUnoptimized); + console.log("cached post-Fusaka hinted+calldata:", cachedPostFusakaHinted); + console.log("cached hinted fits tx cap? :", cachedPostFusakaHinted <= TX_CAP ? 1 : 0); + } + + function test_RealAttestationPerCertSplitProjection() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + + bytes32 parentHash; + console.log("==== REAL ATTESTATION PER-CERT SPLIT ===="); + for (uint256 i = 0; i < ptrs.cabundle.length; ++i) { + bytes memory caCert = attestationTbs.slice(ptrs.cabundle[i]); + uint256 certG0 = gasleft(); + parentHash = certManager.verifyCACert(caCert, parentHash); + uint256 certGas = certG0 - gasleft(); + uint256 projected = i == 0 ? certGas : _projectOneHintedP384(certGas); + console.log("cabundle index :", i); + console.log(" current gas :", certGas); + console.log(" projected hinted post-Fusaka :", projected); + console.log(" fits tx cap? :", projected <= TX_CAP ? 1 : 0); + assertLe(projected, TX_CAP); + } + + bytes memory clientCert = attestationTbs.slice(ptrs.cert); + uint256 clientG0 = gasleft(); + certManager.verifyClientCert(clientCert, parentHash); + uint256 clientGas = clientG0 - gasleft(); + uint256 clientProjected = _projectOneHintedP384(clientGas); + console.log("client cert"); + console.log(" current gas :", clientGas); + console.log(" projected hinted post-Fusaka :", clientProjected); + console.log(" fits tx cap? :", clientProjected <= TX_CAP ? 1 : 0); + assertLe(clientProjected, TX_CAP); + } + + function test_ProductionShapedHintedPerCertSplit() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + + bytes32 parentHash; + bytes memory parentPubKey; + console.log("==== PRODUCTION-SHAPED HINTED PER-CERT SPLIT ===="); + for (uint256 i = 0; i < ptrs.cabundle.length; ++i) { + bytes memory caCert = attestationTbs.slice(ptrs.cabundle[i]); + bytes memory hints; + uint256 hintedOtherCalls; + if (i != 0) { + parentPubKey = hintedCertManager.loadVerified(parentHash).pubKey; + (hints, hintedOtherCalls) = hintCollector.collectCertSignatureProfile(caCert, parentPubKey); + } + + uint256 certG0 = gasleft(); + parentHash = hintedCertManager.verifyCACertWithHints(caCert, parentHash, hints); + uint256 certGas = certG0 - gasleft(); + uint256 projected = _projectHintedMeasured(certGas, hints.length, hintedOtherCalls); + console.log("cabundle index :", i); + console.log(" current hinted gas :", certGas); + console.log(" inverse hint bytes :", hints.length); + console.log(" hinted MODEXP floor calls :", hintedOtherCalls); + console.log(" projected hinted post-Fusaka :", projected); + console.log(" fits tx cap? :", projected <= TX_CAP ? 1 : 0); + assertLe(projected, TX_CAP); + } + + bytes memory clientCert = attestationTbs.slice(ptrs.cert); + parentPubKey = hintedCertManager.loadVerified(parentHash).pubKey; + (bytes memory clientHints, uint256 clientHintedOtherCalls) = + hintCollector.collectCertSignatureProfile(clientCert, parentPubKey); + + uint256 clientG0 = gasleft(); + hintedCertManager.verifyClientCertWithHints(clientCert, parentHash, clientHints); + uint256 clientGas = clientG0 - gasleft(); + uint256 clientProjected = _projectHintedMeasured(clientGas, clientHints.length, clientHintedOtherCalls); + console.log("client cert"); + console.log(" current hinted gas :", clientGas); + console.log(" inverse hint bytes :", clientHints.length); + console.log(" hinted MODEXP floor calls :", clientHintedOtherCalls); + console.log(" projected hinted post-Fusaka :", clientProjected); + console.log(" fits tx cap? :", clientProjected <= TX_CAP ? 1 : 0); + assertLe(clientProjected, TX_CAP); + } + + function test_ProductionShapedHintedCachedAttestation() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); + ICertManager.VerifiedCert memory leaf = _cacheCertBundleWithHints(attestationTbs); + + bytes memory hash = Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length); + (bytes memory attestationHints, uint256 hintedOtherCalls) = + hintCollector.collectVerifyProfile(hash, signature, leaf.pubKey); + + uint256 g0 = gasleft(); + hintedValidator.validateAttestationWithHints(attestationTbs, signature, attestationHints); + uint256 hintedGas = g0 - gasleft(); + uint256 projected = _projectHintedMeasured(hintedGas, attestationHints.length, hintedOtherCalls); + + console.log("==== PRODUCTION-SHAPED HINTED CACHED ATTESTATION ===="); + console.log("current hinted cached gas :", hintedGas); + console.log("inverse hint bytes :", attestationHints.length); + console.log("hinted MODEXP floor calls :", hintedOtherCalls); + console.log("projected hinted post-Fusaka :", projected); + console.log("fits tx cap? :", projected <= TX_CAP ? 1 : 0); + assertLe(projected, TX_CAP); + } + + function test_ProductionShapedHintedAttestationRejectsSurplusHint() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); + ICertManager.VerifiedCert memory leaf = _cacheCertBundleWithHints(attestationTbs); + + bytes memory hash = Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length); + bytes memory attestationHints = + abi.encodePacked(hintCollector.collectVerifyHints(hash, signature, leaf.pubKey), bytes1(0x00)); + + vm.expectRevert("unused inverse hints"); + hintedValidator.validateAttestationWithHints(attestationTbs, signature, attestationHints); + } + + function test_008_ProductionCandidateRejectsMutatedCertHint() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + (bytes memory caCert, bytes32 parentHash, bytes memory parentPubKey) = _firstNonRootCA(attestationTbs, ptrs); + (bytes memory hints,) = hintCollector.collectCertSignatureProfile(caCert, parentPubKey); + hints[10] = bytes1(uint8(hints[10]) ^ 1); + + vm.expectRevert("bad inverse hint"); + hintedCertManager.verifyCACertWithHints(caCert, parentHash, hints); + } + + function test_008_ProductionCandidateRejectsTruncatedCertHint() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + (bytes memory caCert, bytes32 parentHash, bytes memory parentPubKey) = _firstNonRootCA(attestationTbs, ptrs); + (bytes memory hints,) = hintCollector.collectCertSignatureProfile(caCert, parentPubKey); + assembly { + mstore(hints, sub(mload(hints), 1)) + } + + vm.expectRevert("inverse hint underflow"); + hintedCertManager.verifyCACertWithHints(caCert, parentHash, hints); + } + + function test_008_ProductionCandidateRejectsWrongParentHash() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + bytes memory caCert = attestationTbs.slice(ptrs.cabundle[1]); + + vm.expectRevert("parent cert unverified"); + hintedCertManager.verifyCACertWithHints(caCert, bytes32(0), ""); + } + + function test_008_ProductionCandidateRejectsExpiredCachedCert() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + (bytes memory caCert, bytes32 parentHash, bytes memory parentPubKey) = _firstNonRootCA(attestationTbs, ptrs); + bytes memory hints = hintCollector.collectCertSignatureHints(caCert, parentPubKey); + hintedCertManager.verifyCACertWithHints(caCert, parentHash, hints); + + vm.warp(1768953600); // 2026-01-21T00:00:00Z, after this fixture's first non-root CA expiry. + + vm.expectRevert("cert expired"); + hintedCertManager.verifyCACertWithHints(caCert, parentHash, ""); + } + + function test_008_ProductionCandidateRejectsCachedRoleMismatch() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + (bytes memory caCert, bytes32 parentHash, bytes memory parentPubKey) = _firstNonRootCA(attestationTbs, ptrs); + bytes memory hints = hintCollector.collectCertSignatureHints(caCert, parentPubKey); + hintedCertManager.verifyCACertWithHints(caCert, parentHash, hints); + + vm.expectRevert("cert is not a CA"); + hintedCertManager.verifyClientCertWithHints(caCert, parentHash, ""); + } + + function test_008_ProductionCandidateValidateRequiresWarmCache() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); + CertManagerHinted freshCertManager = new CertManagerHinted(p384Verifier); + NitroValidatorHinted freshValidator = new NitroValidatorHinted(freshCertManager, p384Verifier); + + vm.expectRevert("inverse hint underflow"); + freshValidator.validateAttestationWithHints(attestationTbs, signature, ""); + } + + function test_008_ProductionCandidateRejectsInvalidFinalSignature() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); + ICertManager.VerifiedCert memory leaf = _cacheCertBundleWithHints(attestationTbs); + bytes memory hash = Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length); + bytes memory attestationHints = hintCollector.collectVerifyHints(hash, signature, leaf.pubKey); + signature[0] = bytes1(uint8(signature[0]) ^ 1); + + vm.expectRevert(); + hintedValidator.validateAttestationWithHints(attestationTbs, signature, attestationHints); + } + + function test_009_DeployableHintedContractsFitEIP170() public view { + console.log("==== 009 DEPLOYABLE CONTRACT SIZES ===="); + console.log("P384Verifier runtime bytes :", address(p384Verifier).code.length); + console.log("CertManagerHinted runtime bytes :", address(hintedCertManager).code.length); + console.log("NitroValidatorHinted runtime bytes:", address(hintedValidator).code.length); + assertLe(address(p384Verifier).code.length, EIP170_RUNTIME_LIMIT); + assertLe(address(hintedCertManager).code.length, EIP170_RUNTIME_LIMIT); + assertLe(address(hintedValidator).code.length, EIP170_RUNTIME_LIMIT); + } + + function test_009_DeployableCertManagerDisablesUnhintedEntrypoints() public { + vm.expectRevert("use hinted cert verification"); + hintedCertManager.verifyCACert("", bytes32(0)); + + vm.expectRevert("use hinted cert verification"); + hintedCertManager.verifyClientCert("", bytes32(0)); + } + + function test_010_OffchainWitnessGeneratorMatchesSolidityCollector() public { + if (!vm.envOr("NITRO_RUN_FFI", false)) { + console.log("==== 010 OFFCHAIN WITNESS GENERATOR ===="); + console.log("skipped; rerun with NITRO_RUN_FFI=true forge test --ffi --match-test test_010"); + return; + } + + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + + bytes memory rootCert = attestationTbs.slice(ptrs.cabundle[0]); + bytes32 parentHash = keccak256(rootCert); + bytes memory parentPubKey = hintedCertManager.loadVerified(parentHash).pubKey; + uint256 signaturesChecked; + + for (uint256 i = 1; i < ptrs.cabundle.length; ++i) { + bytes memory caCert = attestationTbs.slice(ptrs.cabundle[i]); + bytes memory expectedHints = hintCollector.collectCertSignatureHints(caCert, parentPubKey); + bytes memory offchainHints = _ffiCertSignatureHints(caCert, parentPubKey); + assertEq(offchainHints, expectedHints, "offchain CA cert hints mismatch"); + + parentHash = hintedCertManager.verifyCACertWithHints(caCert, parentHash, offchainHints); + parentPubKey = hintedCertManager.loadVerified(parentHash).pubKey; + signaturesChecked += 1; + } + + bytes memory clientCert = attestationTbs.slice(ptrs.cert); + bytes memory expectedClientHints = hintCollector.collectCertSignatureHints(clientCert, parentPubKey); + bytes memory offchainClientHints = _ffiCertSignatureHints(clientCert, parentPubKey); + assertEq(offchainClientHints, expectedClientHints, "offchain client cert hints mismatch"); + ICertManager.VerifiedCert memory leaf = + hintedCertManager.verifyClientCertWithHints(clientCert, parentHash, offchainClientHints); + signaturesChecked += 1; + + _assertOffchainAttestationHints(attestation, attestationTbs, signature, leaf.pubKey); + signaturesChecked += 1; + + console.log("==== 010 OFFCHAIN WITNESS GENERATOR ===="); + console.log("signatures checked :", signaturesChecked); + } + + function test_007_FullColdAndWarmHintedSequence() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + + console.log("==== 007 FULL COLD + WARM HINTED SEQUENCE ===="); + console.log("cabundle certs :", ptrs.cabundle.length); + console.log("minimum cold tx count :", ptrs.cabundle.length + 1); + console.log("warm-cache tx count :", uint256(1)); + console.log("root is constructor-pinned tx? :", uint256(0)); + + SequenceSummary memory cold = _runColdCertCacheSequence(attestationTbs, ptrs); + + bytes memory hash = Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length); + (bytes memory attestationHints, uint256 attestationHintedOtherCalls) = + hintCollector.collectVerifyProfile(hash, signature, cold.leaf.pubKey); + + TxGas memory coldFinal = _runAttestationValidationTx( + cold.txCount + 1, + "validate attestation", + attestationTbs, + signature, + attestationHints.length, + attestationHintedOtherCalls, + attestationHints + ); + cold.txCount += 1; + cold.totalCurrentGas += coldFinal.currentGas; + cold.totalProjectedGas += coldFinal.projectedGas; + cold.maxProjectedGas = _max(cold.maxProjectedGas, coldFinal.projectedGas); + + console.log("cold sequence tx count :", cold.txCount); + console.log("cold total current gas :", cold.totalCurrentGas); + console.log("cold total projected gas :", cold.totalProjectedGas); + console.log("cold max projected tx gas :", cold.maxProjectedGas); + console.log("cold sequence all txs fit cap? :", cold.maxProjectedGas <= TX_CAP ? 1 : 0); + + _runAttestationValidationTx( + 1, + "warm validate", + attestationTbs, + signature, + attestationHints.length, + attestationHintedOtherCalls, + attestationHints + ); + } + + function _decodeBase64(string memory input) internal pure returns (bytes memory output) { + bytes memory data = bytes(input); + require(data.length % 4 == 0, "bad base64 length"); + + uint256 decodedLen = (data.length / 4) * 3; + if (data.length != 0 && data[data.length - 1] == "=") --decodedLen; + if (data.length > 1 && data[data.length - 2] == "=") --decodedLen; + + output = new bytes(decodedLen); + uint256 out; + + for (uint256 i = 0; i < data.length; i += 4) { + uint256 n = (_base64Value(data[i]) << 18) | (_base64Value(data[i + 1]) << 12) + | (_base64Value(data[i + 2]) << 6) | _base64Value(data[i + 3]); + + if (out < decodedLen) output[out++] = bytes1(uint8(n >> 16)); + if (out < decodedLen) output[out++] = bytes1(uint8(n >> 8)); + if (out < decodedLen) output[out++] = bytes1(uint8(n)); + } + } + + function _base64Value(bytes1 char) internal pure returns (uint256) { + uint8 c = uint8(char); + if (c >= 0x41 && c <= 0x5a) return c - 0x41; + if (c >= 0x61 && c <= 0x7a) return c - 0x61 + 26; + if (c >= 0x30 && c <= 0x39) return c - 0x30 + 52; + if (c == 0x2b) return 62; + if (c == 0x2f) return 63; + if (c == 0x3d) return 0; + revert("bad base64 char"); + } + + function _projectOneHintedP384(uint256 currentGas) internal pure returns (uint256) { + return currentGas - P384_VERIFY_2565 + HINTED_P384_VERIFY_7883_WITH_CALLDATA; + } + + function _projectHintedMeasured(uint256 currentHintedGas, uint256 hintBytes, uint256 hintedOtherCalls) + internal + pure + returns (uint256) + { + if (hintBytes == 0) { + return currentHintedGas; + } + + return currentHintedGas + hintedOtherCalls * HINTED_MODEXP_FLOOR_DELTA + hintBytes * 16; + } + + function _firstNonRootCA(bytes memory attestationTbs, NitroValidator.Ptrs memory ptrs) + internal + view + returns (bytes memory caCert, bytes32 parentHash, bytes memory parentPubKey) + { + caCert = attestationTbs.slice(ptrs.cabundle[1]); + bytes memory rootCert = attestationTbs.slice(ptrs.cabundle[0]); + parentHash = keccak256(rootCert); + parentPubKey = hintedCertManager.loadVerified(parentHash).pubKey; + } + + function _runColdCertCacheSequence(bytes memory attestationTbs, NitroValidator.Ptrs memory ptrs) + internal + returns (SequenceSummary memory summary) + { + bytes memory rootCert = attestationTbs.slice(ptrs.cabundle[0]); + bytes32 parentHash = keccak256(rootCert); + ICertManager.VerifiedCert memory parent = hintedCertManager.loadVerified(parentHash); + assertTrue(parent.pubKey.length > 0, "root must already be cached"); + assertTrue(parent.ca, "root must be cached as CA"); + console.log("root cert hash pinned :"); + console.logBytes32(parentHash); + + uint256 g0; + for (uint256 i = 1; i < ptrs.cabundle.length; ++i) { + bytes memory caCert = attestationTbs.slice(ptrs.cabundle[i]); + (bytes memory hints, uint256 hintedOtherCalls) = + hintCollector.collectCertSignatureProfile(caCert, parent.pubKey); + + g0 = gasleft(); + parentHash = hintedCertManager.verifyCACertWithHints(caCert, parentHash, hints); + uint256 currentGas = g0 - gasleft(); + parent = hintedCertManager.loadVerified(parentHash); + assertTrue(parent.pubKey.length > 0, "CA cert must be cached"); + assertTrue(parent.ca, "CA cert must be cached as CA"); + + uint256 projectedGas = _projectHintedMeasured(currentGas, hints.length, hintedOtherCalls); + summary.txCount += 1; + _logSequenceTx(summary.txCount, "cache CA cert", currentGas, hints.length, hintedOtherCalls, projectedGas); + _addTxToSummary(summary, currentGas, projectedGas); + } + + bytes memory clientCert = attestationTbs.slice(ptrs.cert); + (bytes memory clientHints, uint256 clientHintedOtherCalls) = + hintCollector.collectCertSignatureProfile(clientCert, parent.pubKey); + + g0 = gasleft(); + summary.leaf = hintedCertManager.verifyClientCertWithHints(clientCert, parentHash, clientHints); + uint256 clientCurrentGas = g0 - gasleft(); + + bytes32 leafHash = keccak256(clientCert); + ICertManager.VerifiedCert memory cachedLeaf = hintedCertManager.loadVerified(leafHash); + assertTrue(cachedLeaf.pubKey.length > 0, "client cert must be cached"); + assertFalse(cachedLeaf.ca, "client cert must be cached as client"); + + uint256 clientProjectedGas = + _projectHintedMeasured(clientCurrentGas, clientHints.length, clientHintedOtherCalls); + summary.txCount += 1; + _logSequenceTx( + summary.txCount, + "cache client cert", + clientCurrentGas, + clientHints.length, + clientHintedOtherCalls, + clientProjectedGas + ); + _addTxToSummary(summary, clientCurrentGas, clientProjectedGas); + } + + function _runAttestationValidationTx( + uint256 txIndex, + string memory label, + bytes memory attestationTbs, + bytes memory signature, + uint256 hintBytes, + uint256 hintedOtherCalls, + bytes memory attestationHints + ) internal returns (TxGas memory txGas) { + uint256 g0 = gasleft(); + hintedValidator.validateAttestationWithHints(attestationTbs, signature, attestationHints); + txGas.currentGas = g0 - gasleft(); + txGas.projectedGas = _projectHintedMeasured(txGas.currentGas, hintBytes, hintedOtherCalls); + _logSequenceTx(txIndex, label, txGas.currentGas, hintBytes, hintedOtherCalls, txGas.projectedGas); + assertLe(txGas.projectedGas, TX_CAP); + } + + function _addTxToSummary(SequenceSummary memory summary, uint256 currentGas, uint256 projectedGas) internal pure { + assert(projectedGas <= TX_CAP); + summary.totalCurrentGas += currentGas; + summary.totalProjectedGas += projectedGas; + summary.maxProjectedGas = _max(summary.maxProjectedGas, projectedGas); + } + + function _logSequenceTx( + uint256 txIndex, + string memory label, + uint256 currentGas, + uint256 hintBytes, + uint256 hintedOtherCalls, + uint256 projectedGas + ) internal pure { + console.log("sequence tx :", txIndex); + console.log(" label :", label); + console.log(" current hinted gas :", currentGas); + console.log(" inverse hint bytes :", hintBytes); + console.log(" hinted MODEXP floor calls :", hintedOtherCalls); + console.log(" projected post-Fusaka gas :", projectedGas); + console.log(" fits tx cap? :", projectedGas <= TX_CAP ? 1 : 0); + } + + function _max(uint256 a, uint256 b) internal pure returns (uint256) { + return a >= b ? a : b; + } + + function _cacheCertBundleWithHints(bytes memory attestationTbs) + internal + returns (ICertManager.VerifiedCert memory leaf) + { + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + + bytes32 parentHash; + bytes memory parentPubKey; + for (uint256 i = 0; i < ptrs.cabundle.length; ++i) { + bytes memory caCert = attestationTbs.slice(ptrs.cabundle[i]); + bytes memory hints; + if (i != 0) { + parentPubKey = hintedCertManager.loadVerified(parentHash).pubKey; + hints = hintCollector.collectCertSignatureHints(caCert, parentPubKey); + } + parentHash = hintedCertManager.verifyCACertWithHints(caCert, parentHash, hints); + } + + bytes memory clientCert = attestationTbs.slice(ptrs.cert); + parentPubKey = hintedCertManager.loadVerified(parentHash).pubKey; + bytes memory clientHints = hintCollector.collectCertSignatureHints(clientCert, parentPubKey); + leaf = hintedCertManager.verifyClientCertWithHints(clientCert, parentHash, clientHints); + } + + function _ffiCertSignatureHints(bytes memory cert, bytes memory parentPubKey) internal returns (bytes memory) { + string[] memory command = new string[](7); + command[0] = "node"; + command[1] = string.concat(vm.projectRoot(), "/bench/p384_hints.js"); + command[2] = "cert"; + command[3] = "--cert"; + command[4] = vm.toString(cert); + command[5] = "--pubkey"; + command[6] = vm.toString(parentPubKey); + return vm.ffi(command); + } + + function _ffiVerifyHints(bytes memory hash, bytes memory signature, bytes memory pubKey) + internal + returns (bytes memory) + { + string[] memory command = new string[](9); + command[0] = "node"; + command[1] = string.concat(vm.projectRoot(), "/bench/p384_hints.js"); + command[2] = "verify"; + command[3] = "--hash"; + command[4] = vm.toString(hash); + command[5] = "--signature"; + command[6] = vm.toString(signature); + command[7] = "--pubkey"; + command[8] = vm.toString(pubKey); + return vm.ffi(command); + } + + function _assertOffchainAttestationHints( + bytes memory attestation, + bytes memory attestationTbs, + bytes memory signature, + bytes memory pubKey + ) internal { + bytes memory hash = Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length); + bytes memory expectedHints = hintCollector.collectVerifyHints(hash, signature, pubKey); + bytes memory offchainHints = _ffiVerifyHints(hash, signature, pubKey); + assertEq(offchainHints, expectedHints, "offchain attestation hints mismatch"); + + bytes memory offchainCoseHints = _ffiAttestationHints(attestation, pubKey); + assertEq(offchainCoseHints, expectedHints, "offchain COSE attestation hints mismatch"); + + hintedValidator.validateAttestationWithHints(attestationTbs, signature, offchainHints); + console.log("final attestation hint bytes :", offchainHints.length); + } + + function _ffiAttestationHints(bytes memory attestation, bytes memory pubKey) internal returns (bytes memory) { + string[] memory command = new string[](7); + command[0] = "node"; + command[1] = string.concat(vm.projectRoot(), "/bench/p384_hints.js"); + command[2] = "attestation"; + command[3] = "--attestation"; + command[4] = vm.toString(attestation); + command[5] = "--pubkey"; + command[6] = vm.toString(pubKey); + return vm.ffi(command); + } + + function _repairMissingPublicKeyBytes(bytes memory attestation) internal pure returns (bytes memory repaired) { + // The pasted Base64 sample is missing "ic_" in the CBOR key + // "public_key", but the key length and outer COSE payload length still + // correspond to the complete bytes. Insert the missing 3 bytes so this + // fixture matches the signed Nitro document shape. + uint256 insertAt = 4338; + require( + attestation[insertAt - 4] == 0x70 && attestation[insertAt - 3] == 0x75 && attestation[insertAt - 2] == 0x62 + && attestation[insertAt - 1] == 0x6c && attestation[insertAt] == 0x6b + && attestation[insertAt + 1] == 0x65 && attestation[insertAt + 2] == 0x79, + "unexpected fixture public_key corruption" + ); + + repaired = new bytes(attestation.length + 3); + for (uint256 i = 0; i < insertAt; ++i) { + repaired[i] = attestation[i]; + } + repaired[insertAt] = 0x69; // i + repaired[insertAt + 1] = 0x63; // c + repaired[insertAt + 2] = 0x5f; // _ + for (uint256 i = insertAt; i < attestation.length; ++i) { + repaired[i + 3] = attestation[i]; + } + } + + function _realAttestationB64() internal pure returns (string memory) { + return "hEShATgioFkRFr9pbW9kdWxlX2lkeCdpLTAzNjhmYTY3ZTE1NmQ2ZDIzLWVuYzAxOWI4NTk2YjFhOWRhZDZmZGlnZXN0ZlNIQTM4NGl0aW1lc3RhbXAbAAABm4WXqEpkcGNyc7AAWDBLjUzyqZ4Fzhtb3a+dIctEbrDmBsW+vR6/ArRzoiFl97aLC7DRrFqQ8DEeSTUiz6sBWDADQ7BWzYSFyniQ3dgzR214RgrtKqFhVI5OJr7fMhcmaWJX1iPogF8/YFlGs9iwxqoCWDAW78wdaVLFuXN+sqsXUaCEEoNcUSWBi/tV9jZ8s83KSbE9Klttdx3p2xV4JC4yjG0DWDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEWDAkVhcjnhiujrUVDCvkUgDbHmseSD2UyB2DhhceqtbPZBockPFXHxhUJsgvd3g/6zcFWDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGWDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHWDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIWDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJWDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKWDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALWDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMWDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANWDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOWDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPWDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABrY2VydGlmaWNhdGVZAn8wggJ7MIICAaADAgECAhABm4WWsana1gAAAABpWX68MAoGCCqGSM49BAMDMIGOMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQLDANBV1MxOTA3BgNVBAMMMGktMDM2OGZhNjdlMTU2ZDZkMjMudXMtZWFzdC0xLmF3cy5uaXRyby1lbmNsYXZlczAeFw0yNjAxMDMyMDQwMjVaFw0yNjAxMDMyMzQwMjhaMIGTMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQLDANBV1MxPjA8BgNVBAMMNWktMDM2OGZhNjdlMTU2ZDZkMjMtZW5jMDE5Yjg1OTZiMWE5ZGFkNi51cy1lYXN0LTEuYXdzMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEmhjyhdI4lOYFcM1JZ0DMWZ5ATTXamTKk7KvnVEaeBvxhOWETS9VaMxaJmFsy/M3DAybcipdVNM8ZFZ+64QukW5sTmtLWa+m3ZMrRwJ/u/wbNYNAFQvyIpXNEIJNHyg2yox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDAKBggqhkjOPQQDAwNoADBlAjEArFAVNqsDoTI1kduVQRNWgC45sse6HKzn7fFzHCV5eR0y9qd+4G+QEjRItNrOskoDAjAoe2cAQDY6DYqJdODiW0GGS2057LfVhkbZ/0pUBp0UGmg2ihjuEA9R/9+Vze+i9/1oY2FidW5kbGWEWQIVMIICETCCAZagAwIBAgIRAPkxdWgbkK/hHUbMtOTn+FYwCgYIKoZIzj0EAwMwSTELMAkGA1UEBhMCVVMxDzANBgNVBAoMBkFtYXpvbjEMMAoGA1UECwwDQVdTMRswGQYDVQQDDBJhd3Mubml0cm8tZW5jbGF2ZXMwHhcNMTkxMDI4MTMyODA1WhcNNDkxMDI4MTQyODA1WjBJMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQLDANBV1MxGzAZBgNVBAMMEmF3cy5uaXRyby1lbmNsYXZlczB2MBAGByqGSM49AgEGBSuBBAAiA2IABPwCVOumCMHzaHDimtqQvkY4MpJzbolL//Zy2YlES1BR5TSksfbb48C8WBoyt7F2Bw7eEtaaP+ohG2bnUs990d0JX28TcPQXCEPZ3BABIeTPYwEoCWZEh8l5YoQwTcU/9KNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUkCW1DdkFR+eWw5b6cp3PmanfS5YwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCjfy+Rocm9Xue4YnwWmNJVA44fA0P5W2OpYow9OYCVRaEevL8uO1XYru5xtMPWrfMCMQCi85sWBbJwKKXdS6BptQFuZbT73o/gBh1qUxl/nNr12UO8Yfwr6wPLb+6NIwLz3/ZZAsMwggK/MIICRaADAgECAhEAoVxnkhX/5EIUR0JU/mgkszAKBggqhkjOPQQDAzBJMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQLDANBV1MxGzAZBgNVBAMMEmF3cy5uaXRyby1lbmNsYXZlczAeFw0yNTEyMzExNDA3NDZaFw0yNjAxMjAxNTA3NDVaMGQxCzAJBgNVBAYTAlVTMQ8wDQYDVQQKDAZBbWF6b24xDDAKBgNVBAsMA0FXUzE2MDQGA1UEAwwtMjQ3ODcyNGYwNTk2MDRkYy51cy1lYXN0LTEuYXdzLm5pdHJvLWVuY2xhdmVzMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAENGvYmUf2Zu1RgUKeXZ4kOQ2iOyYrJRIUlxcghQ1lqapWVO3zSV+5+eNNA8xWWctIn/j04QcvkoQcGwtgWmE9PK2F353yVXev73+8UP7UR2va6GN6Jo3WjYaSkGD6+wx8o4HVMIHSMBIGA1UdEwEB/wQIMAYBAf8CAQIwHwYDVR0jBBgwFoAUkCW1DdkFR+eWw5b6cp3PmanfS5YwHQYDVR0OBBYEFJTqdF/1lrjIIPpP3sGDG+a8W/gwMA4GA1UdDwEB/wQEAwIBhjBsBgNVHR8EZTBjMGGgX6BdhltodHRwOi8vYXdzLW5pdHJvLWVuY2xhdmVzLWNybC5zMy5hbWF6b25hd3MuY29tL2NybC9hYjQ5NjBjYy03ZDYzLTQyYmQtOWU5Zi01OTMzOGNiNjdmODQuY3JsMAoGCCqGSM49BAMDA2gAMGUCMG6GmZDF6g2450I4fY7VDfmqupxzew1v1+HUAEN5UldK4QcOqz5zQ/eY3x3ZBdUbBQIxAMic6eyO2VfB3MdZ6JgDYi1Y+ISD6mRUJFaaE/sc8bWNDwRKJ8B6Mjlu/QL5Mo3EXVkDGjCCAxYwggKboAMCAQICEQCr7sWkp1WYQKZObGiM61XIMAoGCCqGSM49BAMDMGQxCzAJBgNVBAYTAlVTMQ8wDQYDVQQKDAZBbWF6b24xDDAKBgNVBAsMA0FXUzE2MDQGA1UEAwwtMjQ3ODcyNGYwNTk2MDRkYy51cy1lYXN0LTEuYXdzLm5pdHJvLWVuY2xhdmVzMB4XDTI2MDEwMzA3MzIzOVoXDTI2MDEwOTAxMzIzOVowgYkxPDA6BgNVBAMMM2JlMzU4OWE1ZmYyYjJkY2Uuem9uYWwudXMtZWFzdC0xLmF3cy5uaXRyby1lbmNsYXZlczEMMAoGA1UECwwDQVdTMQ8wDQYDVQQKDAZBbWF6b24xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJXQTEQMA4GA1UEBwwHU2VhdHRsZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABEtWC58CCKUiKz/Fq+4Atb6egyU/IElbnxrbBD7Ss0j5C+2tMA7aAUpvx25ISmsSJLfTyPADGpORNouNz0nnYJOPs+BMaFwIQhuInPp+F54hRyfCuiUQnh/SgQrxbk33a6OB6jCB5zASBgNVHRMBAf8ECDAGAQH/AgEBMB8GA1UdIwQYMBaAFJTqdF/1lrjIIPpP3sGDG+a8W/gwMB0GA1UdDgQWBBRa9UD9QA2vPbhucliyB34zGsZmJjAOBgNVHQ8BAf8EBAMCAYYwgYAGA1UdHwR5MHcwdaBzoHGGb2h0dHA6Ly9jcmwtdXMtZWFzdC0xLWF3cy1uaXRyby1lbmNsYXZlcy5zMy51cy1lYXN0LTEuYW1hem9uYXdzLmNvbS9jcmwvYjAwODRhZTktNzU3MS00MTM5LWI5OTktMmI1NTQwNmUxMjEzLmNybDAKBggqhkjOPQQDAwNpADBmAjEAjHiUIxoUiVvot07XgWdYbr3P/k5l0z4g1WfabVEzJGAEwGjS1lyetIrDmF+OhKRAAjEAnqNW8+Ii/DzxnqX0UJOxytERpctSmyf+NK1JtgTWSH+pu31PIu2PQZaRwb9BxwV4WQLCMIICvjCCAkWgAwIBAgIVAOAIO3C3hMiXBulWYXVBNJ0BTo/eMAoGCCqGSM49BAMDMIGJMTwwOgYDVQQDDDNiZTM1ODlhNWZmMmIyZGNlLnpvbmFsLnVzLWVhc3QtMS5hd3Mubml0cm8tZW5jbGF2ZXMxDDAKBgNVBAsMA0FXUzEPMA0GA1UECgwGQW1hem9uMQswCQYDVQQGEwJVUzELMAkGA1UECAwCV0ExEDAOBgNVBAcMB1NlYXR0bGUwHhcNMjYwMTAzMTEwODI0WhcNMjYwMTA0MTEwODI0WjCBjjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxDzANBgNVBAoMBkFtYXpvbjEMMAoGA1UECwwDQVdTMTkwNwYDVQQDDDBpLTAzNjhmYTY3ZTE1NmQ2ZDIzLnVzLWVhc3QtMS5hd3Mubml0cm8tZW5jbGF2ZXMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ5vOn5UTKzCwWhli0SbYuWVEcJvsmwyLiFVKH+zD6mu28ehhR7LRVrTfw0bq8YfAEfTLWVR+FKT+T6Ak8vrN9rDDr1RCm91v0MomWHULpto9IdKN5wZsODbnOi3TqDR2WjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgIEMB0GA1UdDgQWBBR5Thr3TVB2Vd2FrlFz3fuwFBYjNDAfBgNVHSMEGDAWgBRa9UD9QA2vPbhucliyB34zGsZmJjAKBggqhkjOPQQDAwNnADBkAjAtbY46McZ8YZCWxXEd40syqh3gKBen4/kui5OMrus3c5ivk68g4qCV1MfP6ItR4EgCMFrDbmpKrReUupYJG8DEFmrvV6xpLzeyU2a2JmnAH+Vrmmb0bk9tw16b5x4NoBRR4GpwdWJsa2V59ml1c2VyX2RhdGFUaVU63GHW6fzey+HqSbsrUqYCOOBlbm9uY2X2/1hgrImkcLPLpwNrQUkD8H9lPb6uCw06CIkrekTlAiWQAEcV7ikJAkYIwatpg24mtnVTksLPLAJ6Qc1lIJK9RVWekiThnvMXzmKDPaJ4/3+sYESVnedTSu0Wkdfw/eGVubCG"; + } +} From ed580adeb5eca7b38ff17306309d5b58322d4802 Mon Sep 17 00:00:00 2001 From: Elena Nadolinski Date: Wed, 3 Jun 2026 11:59:00 -0400 Subject: [PATCH 02/10] happy path & doc updates --- .env.example | 9 + .gitignore | 7 +- .gitmodules | 3 +- bench/README.md | 84 -- bench/agent_log/000-baseline.md | 90 -- bench/agent_log/001-hinted-inversion-model.md | 62 - .../002-hinted-inversion-prototype.md | 73 -- .../003-48-byte-hints-negative-tests.md | 100 -- .../004-real-attestation-hot-path.md | 111 -- .../005-real-attestation-per-cert-split.md | 100 -- .../006-production-shaped-hinted-flow.md | 126 -- .../agent_log/007-full-cold-warm-sequence.md | 151 --- .../008-production-candidate-audit-docs.md | 159 --- .../009-deployable-external-p384-verifier.md | 160 --- .../010-offchain-witness-generator.md | 143 --- docs/hinted-p384-nitro-attestation.md | 541 +++++++++ lib/solidity-lib | 2 +- script/BaseSepoliaDemo.s.sol | 156 +++ src/CertManager.sol | 82 +- src/CertManagerHinted.sol | 11 - src/CertManagerHintedExternal.sol | 327 ----- src/ECDSA384Hinted.sol | 1047 ----------------- src/ICertManager.sol | 10 + src/IHintedCertManager.sol | 16 - src/IP384Verifier.sol | 2 +- src/NitroValidator.sol | 36 +- src/NitroValidatorHinted.sol | 14 - src/NitroValidatorHintedExternal.sol | 217 ---- src/P384Verifier.sol | 10 +- src/demo/CertManagerDemo.sol | 22 + test/CertManager.t.sol | 41 - test/IndefiniteLengthCbor.t.sol | 22 +- test/NitroValidator.t.sol | 31 - test/RootCertCheck.t.sol | 3 + test/bench/Bench.t.sol | 291 ----- test/bench/HintedNitroBench.sol | 293 ----- .../ECDSA384HintCollector.sol} | 171 +-- test/helpers/HintedNitroTestHelpers.sol | 110 ++ .../HintedNitroAttestation.t.sol} | 368 +++--- tools/hinted_attestation_calls.js | 410 +++++++ tools/nitro_attestation_input.js | 123 ++ {bench => tools}/p384_hints.js | 157 ++- 42 files changed, 1894 insertions(+), 3997 deletions(-) create mode 100644 .env.example delete mode 100644 bench/README.md delete mode 100644 bench/agent_log/000-baseline.md delete mode 100644 bench/agent_log/001-hinted-inversion-model.md delete mode 100644 bench/agent_log/002-hinted-inversion-prototype.md delete mode 100644 bench/agent_log/003-48-byte-hints-negative-tests.md delete mode 100644 bench/agent_log/004-real-attestation-hot-path.md delete mode 100644 bench/agent_log/005-real-attestation-per-cert-split.md delete mode 100644 bench/agent_log/006-production-shaped-hinted-flow.md delete mode 100644 bench/agent_log/007-full-cold-warm-sequence.md delete mode 100644 bench/agent_log/008-production-candidate-audit-docs.md delete mode 100644 bench/agent_log/009-deployable-external-p384-verifier.md delete mode 100644 bench/agent_log/010-offchain-witness-generator.md create mode 100644 docs/hinted-p384-nitro-attestation.md create mode 100644 script/BaseSepoliaDemo.s.sol delete mode 100644 src/CertManagerHinted.sol delete mode 100644 src/CertManagerHintedExternal.sol delete mode 100644 src/ECDSA384Hinted.sol delete mode 100644 src/IHintedCertManager.sol delete mode 100644 src/NitroValidatorHinted.sol delete mode 100644 src/NitroValidatorHintedExternal.sol create mode 100644 src/demo/CertManagerDemo.sol delete mode 100644 test/CertManager.t.sol delete mode 100644 test/NitroValidator.t.sol delete mode 100644 test/bench/Bench.t.sol delete mode 100644 test/bench/HintedNitroBench.sol rename test/{bench/ECDSA384Bench.sol => helpers/ECDSA384HintCollector.sol} (84%) create mode 100644 test/helpers/HintedNitroTestHelpers.sol rename test/{bench/RealAttestationBench.t.sol => hinted/HintedNitroAttestation.t.sol} (69%) create mode 100644 tools/hinted_attestation_calls.js create mode 100644 tools/nitro_attestation_input.js rename {bench => tools}/p384_hints.js (76%) diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..698d2be --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +BASE_SEPOLIA_RPC_URL=https://sepolia.base.org +PRIVATE_KEY= + +# Demo-only: lets the January 2026 fixture pass certificate expiry checks on a +# June 2026 Base Sepolia block. Production-named contracts keep zero grace. +DEMO_CERT_EXPIRY_GRACE_SECONDS=31536000 + +# Uses the repaired real attestation fixture embedded in the script. +USE_BUNDLED_REAL_ATTESTATION=true diff --git a/.gitignore b/.gitignore index 7771e5f..b5463fa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,12 +3,11 @@ cache/ out/ # Ignores development broadcast logs -!/broadcast -/broadcast/*/31337/ -/broadcast/**/dry-run/ +broadcast/ # Docs -docs/ +docs/* +!docs/hinted-p384-nitro-attestation.md # Dotenv file .env diff --git a/.gitmodules b/.gitmodules index bf48d31..1319979 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,5 @@ url = https://github.com/foundry-rs/forge-std [submodule "lib/solidity-lib"] path = lib/solidity-lib - url = https://github.com/dl-solarity/solidity-lib + url = https://github.com/leanthebean/solidity-lib + branch = hinted-p384-inversion diff --git a/bench/README.md b/bench/README.md deleted file mode 100644 index 57ea19f..0000000 --- a/bench/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# P384 / Fusaka gas benchmarking loop - -Goal: make on-chain AWS Nitro TEE attestation verification (ECDSA over secp384r1) -land **fully within the EIP-7825 per-transaction gas cap (16,777,216) on Base**, -surviving the Fusaka MODEXP repricing (EIP-7883). Each cert verify should fit in a -single tx; a full cold chain may span several txs but must land within ~5s. - -## The loop - -``` -baseline gas -> experiment -> agent_log/NNN-*.md -> recommended next steps -> execute & repeat -``` - -Every iteration is an entry in `agent_log/`. Each entry records: hypothesis, what -was changed, the measured/modelled numbers, whether it fits the cap, soundness -notes, and the recommended next experiment. - -## Methodology / tooling - -- **Ground-truth current gas**: `forge test` with `gasleft()` around a single - uninstrumented `ECDSA384.verify` (`test/bench/Bench.t.sol`). -- **Exact MODEXP census**: `test/bench/ECDSA384Bench.sol` is an *instrumented copy* - of the library (`lib/solidity-lib/.../ECDSA384.sol`) with transient-storage - counters that tally field inversions vs. other modexp calls per verify. The - submodule itself is never modified. -- **Post-Fusaka projection**: only the MODEXP portion reprices. We compute exact - per-call gas under EIP-2565 (current) and EIP-7883 (post-Fusaka) from the EIP - formulas, applied to this library's operand profiles, and add the unchanged - non-MODEXP remainder. -- A debug-trace census (`vm.startDebugTraceRecording`) was tried and **abandoned**: - materializing the step array for a ~8M-gas verify costs >1B gas / OOMs. The - instrumented-copy approach is the supported method. - -### Per-call MODEXP gas (this library's profiles) - -| call | base/exp/mod (bytes) | EIP-2565 | EIP-7883 | factor | -|------|----------------------|----------|----------|--------| -| field inversion (`modinv`/`moddivAssign`) | 64 / 64 / 64 | 8,170 | 81,792 | ~10× | -| squaring / mulmul / reduce / mod | ≤96 / ≤32 / 64 | 200 (floor) | 500 (floor) | 2.5× | - -EIP-7883 vs EIP-2565 for the inversion compounds three changes: complexity -multiplier `words²→2·words²`, exponent per-word `8→16`, and removal of `÷3`. - -## Run - -```sh -git submodule update --init --recursive -forge test --match-path "test/bench/Bench.t.sol" -vv -``` - -## Off-chain hints - -Experiment 010 adds a dependency-free Node witness generator: - -```sh -node bench/p384_hints.js verify --hash <0xhash> --signature <0xr_s> --pubkey <0xxy> -node bench/p384_hints.js cert --cert <0xder|base64|@file> --pubkey <0xparent_xy> -node bench/p384_hints.js attestation --attestation <0xcose|base64|@file> --pubkey <0xleaf_xy> -``` - -To cross-check the generator against the Solidity collector for the real Nitro -fixture: - -```sh -NITRO_RUN_FFI=true forge test --ffi \ - --match-path test/bench/RealAttestationBench.t.sol \ - --match-test test_010_OffchainWitnessGeneratorMatchesSolidityCollector -vv -``` - -## Numbers to beat - -- Current single verify: **7.94M** gas. -- Projected single verify post-Fusaka (unoptimized): **50.6M** gas — **3× over the cap**. -- Projected hinted single verify post-Fusaka: **6.04M** gas including worst-case - calldata for 48-byte inverse witnesses. -- Real attestation cached hot path with hinted P384 projection: **13.68M** gas - post-Fusaka including worst-case witness calldata. -- Real attestation non-root cert split transactions with hinted P384 projection: - **6.80M-7.03M** gas post-Fusaka each. -- Real attestation full minimum cold sequence for the current fixture: - **5 transactions**. The max projected tx is **13.68M** gas post-Fusaka. -- Real attestation warm-cache sequence for the current fixture: - **1 transaction**, projected at **13.64M** gas post-Fusaka. -- Per-tx cap (EIP-7825): **16,777,216**. diff --git a/bench/agent_log/000-baseline.md b/bench/agent_log/000-baseline.md deleted file mode 100644 index 9ef750b..0000000 --- a/bench/agent_log/000-baseline.md +++ /dev/null @@ -1,90 +0,0 @@ -# 000 — Baseline - -Date: 2026-06-02 -Status: ✅ complete - -## Question - -Given Fusaka's MODEXP repricing (EIP-7883) and the 16,777,216 per-tx gas cap -(EIP-7825), can the existing nitro-validator P384 path land on Base? Where does -the gas actually go? - -## Method - -- `forge test` + `gasleft()` for ground-truth current gas (uninstrumented). -- Instrumented copy `test/bench/ECDSA384Bench.sol` with transient counters for an - exact MODEXP census (inversions vs. other) — see `bench/README.md`. -- Post-Fusaka projection via exact EIP-2565 / EIP-7883 per-call formulas. - -## Results - -### Full attestation (cold, all certs, one tx) -- `validateAttestation`: **53.4M gas** (measured, `NitroValidator.t.sol`). - README quotes ~63M; current measured is 53.4M. - -### Single ECDSA-P384 verify (the hot unit) -| metric | value | -|--------|-------| -| measured gas (EIP-2565) | **7,938,921** | -| MODEXP calls | 3,048 | -| — field inversions | **570** | -| — other (sq/mul/reduce) | 2,478 | -| MODEXP gas now (EIP-2565) | 5,152,500 (64% of verify) | -| MODEXP gas post-Fusaka (EIP-7883) | 47,860,440 | -| **projected verify post-Fusaka** | **50,646,861 (6.37×)** | -| per-tx cap (EIP-7825) | 16,777,216 | -| fits in 1 tx post-Fusaka? | **NO — ~3× over** | - -Inversions are **97%** of the post-Fusaka MODEXP gas (570 × 81,792 = 46.6M). - -### Where the inversions live (phase breakdown) -| phase | inversions | other | post-Fusaka MODEXP gas | -|-------|-----------:|------:|-----------------------:| -| on-curve check | 0 | 3 | 1,500 | -| scalar divisions (2× moddiv) | 2 | 2 | 164,584 | -| precompute table | 61 | 187 | 5,082,812 | -| **double-scalar-mult (ladder)** | **507** | 2,285 | **42,611,044** | -| final mod | 0 | 1 | 500 | - -The scalar-mult ladder holds 89% of inversions / 84% of post-Fusaka MODEXP gas. -The 61 precompute-table inversions include the **fixed-base** (generator G) -multiples, which are constant and could be hoisted to compile-time constants -(no witness, no calldata) — a free secondary win. - -## Findings that correct the prior analysis - -1. **"Switch to Jacobian/projective is the dominant win" — FALSE for this repo.** - The library is already affine + Strauss-Shamir with 6-bit precompute, and its - own header states projective was tried and was *worse* (~9M vs ~8M) in pure EVM, - because EVM trades cheap MODEXP inversions for many more mulmuls. Going - projective is not free wins here; it may even regress. - -2. **"~3× repricing" — understated.** For the 64-byte operand profile this library - uses, a single inversion goes 8,170 → 81,792 ≈ **10×**, not 3×. - -3. **Per-cert splitting is already implemented and is NOT sufficient.** `CertManager` - already pins the AWS root as a constant, caches verified intermediates, supports - skip-if-verified, and exposes `verifyCACert(cert, parentHash)` for per-cert txs. - But a *single* ECDSA verify is atomic and is itself ~50.6M post-Fusaka — ~3× the - cap. **You cannot split one signature check across transactions** without making - the scalar-mult ladder resumable (storing point state in SSTORE between txs — - expensive and ugly). So splitting helps the chain, not the unit. - -4. **The architecture levers in the prior writeup are already done** (root pinning, - intermediate caching, 2-verify hot path). The remaining problem is purely the - per-verify cost. - -## Conclusion - -Optimization is **mandatory**, not optional: post-Fusaka, no single P384 verify -fits in a Base transaction. The target is unambiguous — **eliminate the 570 -on-chain field inversions** (97% of post-Fusaka MODEXP gas). - -## Recommended next step - -→ Experiment 001: model caller-supplied inverse *witnesses* verified by one modmul -each (eliminates on-chain `modinv`). See `001-hinted-inversion-model.md`. - -## Artifacts -- `test/bench/Bench.t.sol::test_Baseline` -- `test/bench/ECDSA384Bench.sol` (instrumented census copy) diff --git a/bench/agent_log/001-hinted-inversion-model.md b/bench/agent_log/001-hinted-inversion-model.md deleted file mode 100644 index ccb91e1..0000000 --- a/bench/agent_log/001-hinted-inversion-model.md +++ /dev/null @@ -1,62 +0,0 @@ -# 001 — Hinted inversions (analytical model) - -Date: 2026-06-02 -Status: ✅ modelled (empirically parameterized) — followed by 002/003 prototypes - -## Hypothesis - -Field inversion via Fermat (`a^(p-2) mod p`, a 64-byte-operand MODEXP) is 97% of -the post-Fusaka MODEXP gas. Inversion is *expensive to compute* but *cheap to -verify*: if the caller supplies `a_inv` as calldata, the contract confirms it with -a single modular multiply `a · a_inv ≡ 1 (mod p)`. All values are public -(verification, not signing), so there is no secrecy concern. Each `moddiv` changes -from {1 inversion + 1 mulmul} to {2 mulmuls}. - -Soundness: the witness MUST be fully constrained on-chain (`a·a_inv == 1 mod p`). -An unconstrained supplied inverse is a signature-forgery vector. - -## Model (parameterized by the measured census: 570 inv, 2478 other) - -`test/bench/Bench.t.sol::test_HintedInversionModel` - -| metric | value | -|--------|-------| -| non-MODEXP verify gas (unchanged) | 2,786,421 | -| hinted verify gas, EIP-2565 | **3,396,021** | -| hinted verify gas, EIP-7883 | **4,310,421** | -| + witness calldata (570×48 B, worst-case 16/B) | 437,760 | -| **= total post-Fusaka** | **4,748,181** | -| fits 1 tx post-Fusaka? | **YES** (cap 16,777,216) | -| witness bytes / verify | 27,360 | - -## Outcome - -- Post-Fusaka single verify: **50.6M → ~4.75M** (~11× reduction), comfortably under - the cap with ~3.5M of headroom for parsing/SHA-384/overhead. -- Bonus: at *current* pricing the hinted verify (~3.4M) is also ~2.3× cheaper than - today's 7.94M. -- Full cold chain (~7 verifies) → ~33M, splits per-cert into 2–3 txs, each well - under cap; hot path (2 verifies) ~10M fits a single tx. -- This makes the pure-EVM path viable and immune to the EIP-7883 *inversion* blow-up - without ZK. - -## Caveats / open questions for the prototype - -1. **Witness plumbing**: ~570 inverses must thread from calldata into each `moddiv` - site. Need a calldata layout + cursor; the offchain prover replays the verify to - emit them in order. -2. **Remaining 0x5 dependence**: the 3,048 remaining mulmuls/squarings still call - MODEXP at the 500-gas floor (~1.5M total post-Fusaka). Optional follow-up: - replace modexp-based reduction with pure-Yul Montgomery/Barrett to be *fully* - immune to 0x5 repricing. Probably unnecessary given the headroom — measure first. -3. **Calldata on L2**: 27 KB/verify also incurs Base L1-data-availability cost - (separate from execution gas). Worth a real cost estimate; blob calldata helps. -4. **Audit**: this modifies audited cryptographic core (`ECDSA384.sol`). Re-audit - the witness constraint and the existing `r` vs `r+n` edge case. - -## Follow-up - -- **002 (prototype)** completed a benchmark-only hinted verifier and measured real - overhead. -- **003** packed witnesses to 48 bytes and added malformed-hint tests. -- **004** should measure the hinted verifier inside the certificate / Nitro path. diff --git a/bench/agent_log/002-hinted-inversion-prototype.md b/bench/agent_log/002-hinted-inversion-prototype.md deleted file mode 100644 index 415b505..0000000 --- a/bench/agent_log/002-hinted-inversion-prototype.md +++ /dev/null @@ -1,73 +0,0 @@ -# 002 — Hinted inversions (prototype) - -Date: 2026-06-02 -Status: ✅ complete - -## Hypothesis - -The analytical hinted-inversion model from 001 should survive real Solidity -control flow: replace every Fermat inversion MODEXP with a caller-supplied inverse -witness, then constrain the witness on-chain with `a * a_inv == 1 mod m`. - -## Implementation - -Benchmark-only changes: - -- Added `ECDSA384Bench.verifyWithHints(...)`. -- Added a transient-storage hint cursor. -- Added inverse-hint collection for test replay. -- Added `BenchHarness.collectInverseHints(...)`. -- Added `BenchHarness.countHintedVerify(...)`. -- Added `BenchTest.test_HintedInversionPrototype()`. - -The first passing prototype used 64-byte witnesses because that matches Solarity's -internal two-word `U384` representation. This was intentionally not production -encoding; experiment 003 packs witnesses to 48 bytes. - -## Debug finding - -The first attempt failed with `bad inverse hint`. - -Root cause: scalar inversions are modulo the curve order `n`, while field -inversions are modulo the field prime `p`. The initial hint check verified every -inverse modulo `p`, which correctly rejected the two scalar inverse witnesses. - -Fix: add a modulus-specific multiplication check for `modinv(..., m_)`. - -## Results - -Command: - -```sh -forge test --match-path test/bench/Bench.t.sol --gas-report -vv -``` - -Prototype output with 64-byte witnesses: - -| metric | value | -|--------|------:| -| hinted verify gas, EIP-2565 | 5,440,654 | -| MODEXP calls | 3,048 | -| — field inversions | 0 | -| — other / checks | 3,048 | -| projected verify gas, EIP-7883 | 6,355,054 | -| + witness calldata, worst-case | 583,680 | -| **= total post-Fusaka** | **6,938,734** | -| fits 1 tx post-Fusaka? | YES | -| witness bytes / verify | 36,480 | - -## Outcome - -The hinted-inversion approach is empirically viable in a benchmark copy. Even -with unoptimized 64-byte witnesses and real witness-check overhead, one P384 -verify projects to about **6.94M** gas post-Fusaka including pessimistic calldata -gas, far below the 16,777,216 cap. - -## Recommended next step - -→ Experiment 003: pack witnesses to 48 bytes and add malformed-hint tests. - -## Artifacts - -- `test/bench/ECDSA384Bench.sol::verifyWithHints` -- `test/bench/Bench.t.sol::test_HintedInversionPrototype` diff --git a/bench/agent_log/003-48-byte-hints-negative-tests.md b/bench/agent_log/003-48-byte-hints-negative-tests.md deleted file mode 100644 index 2bb1361..0000000 --- a/bench/agent_log/003-48-byte-hints-negative-tests.md +++ /dev/null @@ -1,100 +0,0 @@ -# 003 — 48-byte hints and negative tests - -Date: 2026-06-02 -Status: ✅ complete - -## Hypothesis - -The hinted verifier should accept compact 48-byte P384 inverse witnesses while -still rejecting malformed witness streams. - -## Implementation - -Benchmark-only changes: - -- `collectInverseHints(...)` now writes each inverse as 48 big-endian bytes. -- `_nextInverseHint()` reconstructs Solarity's internal two-word `U384` - representation from each packed hint. -- Added malformed-hint tests: - - `test_HintedInversionRejectsMutatedHint` - - `test_HintedInversionRejectsTruncatedHints` - - `test_HintedInversionRejectsSurplusHints` - -## Results - -Command: - -```sh -forge test --match-path test/bench/Bench.t.sol --gas-report -vv -``` - -Prototype output with 48-byte witnesses: - -| metric | value | -|--------|------:| -| hinted verify gas, EIP-2565 | 5,391,727 | -| MODEXP calls | 3,048 | -| — field inversions | 0 | -| — other / checks | 3,048 | -| projected verify gas, EIP-7883 | 6,306,127 | -| + witness calldata, worst-case | 437,760 | -| **= total post-Fusaka** | **6,743,887** | -| fits 1 tx post-Fusaka? | YES | -| witness bytes / verify | 27,360 | - -Focused test results: - -- `test_Baseline`: pass -- `test_HintedInversionModel`: pass -- `test_HintedInversionPrototype`: pass -- `test_HintedInversionRejectsMutatedHint`: pass -- `test_HintedInversionRejectsTruncatedHints`: pass -- `test_HintedInversionRejectsSurplusHints`: pass - -Full suite: - -```sh -forge test --gas-report -``` - -Result: 32 passed, 0 failed. - -## Outcome - -The compact hinted verifier stays well under the post-Fusaka per-transaction cap. -The current prototype is about **6.74M gas per P384 verify** including pessimistic -calldata gas. This leaves roughly **10.0M gas** of margin under EIP-7825 for -certificate parsing, SHA-384, storage writes, and production API overhead. - -## Caveats - -- The implementation is benchmark-only and uses transient storage helpers for - collection/counters. -- The off-chain witness generator still needs to be built; the test collector - simulates replay-derived witnesses. -- The verifier still uses MODEXP for floor-priced modular multiplication / - reduction. That is acceptable for the cap, but it is not fully repricing-proof. -- The production API should not silently alter the existing verifier; it should - expose an explicit hinted verifier ABI. - -## Recommended next step - -→ Experiment 004: measure the hinted verifier inside the cert / Nitro path. - -Concrete plan: - -1. Add a benchmark-only `CertManager` variant that calls a hinted P384 verifier. -2. Use collected hints for one existing cert signature from `CertManager.t.sol`. -3. Measure `verifyCACert` and `verifyClientCert` current gas and projected - EIP-7883 gas. -4. Repeat for the hot `validateAttestation` path. -5. Decide whether the full per-cert transaction has enough margin after parsing, - SHA-384, calldata, and storage overhead. - -## Artifacts - -- `test/bench/ECDSA384Bench.sol` -- `test/bench/Bench.t.sol::test_HintedInversionPrototype` -- `test/bench/Bench.t.sol::test_HintedInversionRejectsMutatedHint` -- `test/bench/Bench.t.sol::test_HintedInversionRejectsTruncatedHints` -- `test/bench/Bench.t.sol::test_HintedInversionRejectsSurplusHints` diff --git a/bench/agent_log/004-real-attestation-hot-path.md b/bench/agent_log/004-real-attestation-hot-path.md deleted file mode 100644 index 842e485..0000000 --- a/bench/agent_log/004-real-attestation-hot-path.md +++ /dev/null @@ -1,111 +0,0 @@ -# 004 — Real attestation fixture and hot-path projection - -Date: 2026-06-02 -Status: ✅ complete - -## Question - -Does the provided real Nitro attestation validate against the current contracts, -and does the hinted-P384 direction still fit once real Nitro parsing, SHA-384, -certificate cache checks, and attestation signature verification are included? - -## Fixture finding - -The pasted Base64 sample has a 3-byte corruption in the CBOR payload: - -- The COSE payload byte-string header declares length `0x1116`. -- The `public_key` CBOR key is missing bytes `0x69 0x63 0x5f` (`"ic_"`), so it - appears as `publkey`. -- Because those 3 bytes are missing, the payload break (`0xff`) and signature - header (`0x58 0x60`) appear 3 bytes earlier than the declared COSE payload end. - -The benchmark fixture restores those missing bytes before validation. After that -normalization, the attestation validates successfully. - -Timestamp decoded from the attestation: - -- `2026-01-03T20:41:07.402Z` -- Foundry warp used: `1767472867` - -## Method - -Added: - -- `test/bench/RealAttestationBench.t.sol` - -The test: - -1. Decodes the provided Base64 in Solidity test code. -2. Repairs the missing `"ic_"` bytes in `public_key`. -3. Measures `decodeAttestationTbs`. -4. Measures cold `validateAttestation`. -5. Measures cached `validateAttestation` by calling it a second time, after CA / - client certs are stored in `CertManager`. -6. Projects the cached hot path under post-Fusaka unoptimized P384 and hinted P384. - -Projection constants from experiments 000/003: - -- Current P384 verify: `7,938,921` -- Unoptimized post-Fusaka P384 verify: `50,646,861` -- Hinted post-Fusaka P384 verify including worst-case 48-byte witness calldata: - `6,743,887` -- EIP-7825 transaction cap: `16,777,216` - -## Results - -Command: - -```sh -forge test --match-path test/bench/RealAttestationBench.t.sol -vv -``` - -Output: - -| metric | value | -|--------|------:| -| repaired attestation bytes | 4,482 | -| attestationTbs bytes | 4,395 | -| signature bytes | 96 | -| `decodeAttestationTbs` gas | 102,550 | -| `validateAttestation` gas, cold | 53,582,064 | -| `validateAttestation` gas, cached | 16,169,057 | -| cached post-Fusaka, unoptimized | 58,876,997 | -| cached post-Fusaka, hinted + calldata | 14,974,023 | -| cached hinted fits `16,777,216` cap? | YES | - -## Outcome - -The hinted-P384 path still looks viable after including real Nitro parsing, -SHA-384, certificate-cache checks, and the final attestation signature verify. -The steady-state hot path projects to **14.97M gas post-Fusaka**, including -pessimistic witness calldata, leaving about **1.80M gas** of margin under the -EIP-7825 cap. - -This is tighter than the isolated P384 benchmark, but still feasible. - -## Implications - -- Current cached validation is already close to the cap at **16.17M**; post-Fusaka - without hinted P384 is impossible. -- With hinted P384, the hot path can remain a single transaction if certificates - are already cached. -- Cold validation still needs to be split across cert transactions. The next - measurement should confirm each hinted per-cert transaction stays comfortably - below the cap. - -## Recommended next step - -→ Experiment 005: benchmark a hinted `CertManager` path. - -Concrete plan: - -1. Add a benchmark-only `CertManager` copy that accepts P384 inverse hints. -2. Measure `verifyCACertWithHints` and `verifyClientCertWithHints` on the certs - extracted from this real attestation. -3. Project and/or directly measure per-cert gas with 48-byte witnesses. -4. Confirm the cold chain can be submitted as sequential under-cap transactions. -5. Then estimate Base L1 data cost for the witness payloads. - -## Artifacts - -- `test/bench/RealAttestationBench.t.sol` diff --git a/bench/agent_log/005-real-attestation-per-cert-split.md b/bench/agent_log/005-real-attestation-per-cert-split.md deleted file mode 100644 index 7833ff4..0000000 --- a/bench/agent_log/005-real-attestation-per-cert-split.md +++ /dev/null @@ -1,100 +0,0 @@ -# 005 — Real attestation per-cert split projection - -Date: 2026-06-02 -Status: ✅ complete - -## Question - -If the real attestation cold path is split across certificate transactions, does -each transaction fit under the EIP-7825 cap once P384 verification is moved to the -hinted-inversion path? - -## Method - -Extended: - -- `test/bench/RealAttestationBench.t.sol` - -The test: - -1. Decodes and repairs the real Base64 attestation fixture from experiment 004. -2. Uses a benchmark-only parse harness to expose `_parseAttestation`. -3. Extracts each `cabundle` certificate and the leaf/client certificate. -4. Measures current `CertManager.verifyCACert` / `verifyClientCert` gas for each - cert transaction. -5. Projects each non-root certificate by replacing one current P384 verify with - the hinted post-Fusaka P384 cost including pessimistic witness calldata. - -Projection formula for certs that perform one P384 signature check: - -```text -projected = currentGas - 7,938,921 + 6,743,887 -``` - -The root cert transaction is already cached/pinned and performs no P384 verify, so -it is left unchanged. - -## Results - -Command: - -```sh -forge test --match-path test/bench/RealAttestationBench.t.sol -vv -``` - -Output: - -| step | current gas | projected hinted post-Fusaka | fits cap? | -|------|------------:|------------------------------:|:---------:| -| cabundle[0] root | 24,670 | 24,670 | YES | -| cabundle[1] | 9,304,997 | 8,109,963 | YES | -| cabundle[2] | 9,527,634 | 8,332,600 | YES | -| cabundle[3] | 9,288,116 | 8,093,082 | YES | -| client cert | 9,312,028 | 8,116,994 | YES | - -## Outcome - -The cold chain can be split into under-cap transactions with substantial margin. -Each non-root certificate transaction projects to roughly **8.1M-8.3M gas -post-Fusaka**, including pessimistic P384 witness calldata. - -Combined with experiment 004: - -- Cold path: split cert transactions fit individually. -- Cached hot path: final attestation validation projects to **14.97M**, also - under cap. - -This supports a pure-EVM architecture: - -1. Submit/cert-cache the AWS cabundle in order. -2. Submit the leaf/client cert with hints. -3. Submit the attestation validation with hints. - -## Caveats - -- This is still a projection for hinted cert verification, not a production - `CertManager.verifyCACertWithHints` implementation. -- The projection assumes one P384 verification per non-root cert, matching the - current `CertManager` flow. -- L1 data cost for witness payloads on Base is not included in execution gas and - should be priced separately. - -## Recommended next step - -→ Experiment 006: build a production-shaped hinted API sketch. - -Concrete plan: - -1. Define calldata ABI for `verifyCACertWithHints`, `verifyClientCertWithHints`, - and `validateAttestationWithHints`. -2. Decide whether hints are passed as one packed `bytes` stream per P384 verify or - grouped by certificate. -3. Implement a minimal off-chain witness generator that replays verification and - emits 48-byte inverses. -4. Replace the benchmark-only transient-storage hint cursor with normal calldata - cursor logic. -5. Re-run 004/005 using the production-shaped API. - -## Artifacts - -- `test/bench/RealAttestationBench.t.sol::test_RealAttestationPerCertSplitProjection` diff --git a/bench/agent_log/006-production-shaped-hinted-flow.md b/bench/agent_log/006-production-shaped-hinted-flow.md deleted file mode 100644 index 168b87b..0000000 --- a/bench/agent_log/006-production-shaped-hinted-flow.md +++ /dev/null @@ -1,126 +0,0 @@ -# 006 - Production-shaped hinted flow - -Date: 2026-06-02 -Status: complete - -## Question - -Can the hinted-inversion P384 path be represented as a production-shaped API for -the split Nitro flow, and do the real attestation certificate and cached -attestation transactions still fit under the 16,777,216 gas cap after EIP-7883? - -## Method - -Extended benchmark-only code: - -- `test/bench/ECDSA384Bench.sol` -- `test/bench/HintedNitroBench.sol` -- `test/bench/RealAttestationBench.t.sol` - -Changes: - -1. Replaced the verifier-side transient-storage hint cursor with an explicit - memory cursor stored in the `U384Bench` call context. -2. Kept transient storage only in the benchmark collector that generates witness - bytes for tests. -3. Added production-shaped benchmark APIs: - - `verifyCACertWithHints(cert, parentCertHash, signatureHints)` - - `verifyClientCertWithHints(cert, parentCertHash, signatureHints)` - - `validateAttestationWithHints(attestationTbs, signature, attestationSigHints)` -4. Added `P384HintCollectorBench`, which emits one packed `bytes` stream per P384 - verification. Each inverse witness is exactly 48 bytes, big-endian, and - consumed sequentially. -5. Projected post-Fusaka gas from measured hinted EIP-2565 gas by adding: - -```text -hinted MODEXP floor calls * (500 - 200) + inverseHintBytes * 16 -``` - -The calldata term is pessimistic execution gas for all-nonzero witness bytes. - -## Results - -Commands: - -```sh -forge test --match-path test/bench/Bench.t.sol -vv -forge test --match-path test/bench/RealAttestationBench.t.sol -vv -forge test -forge test --gas-report -``` - -All tests passed: `37 passed, 0 failed`. - -### Isolated P384 - -| metric | value | -|--------|------:| -| current unhinted verify | 7,938,921 | -| projected unhinted post-Fusaka | 50,646,861 | -| current hinted verify | 4,688,649 | -| hinted MODEXP floor calls | 3,048 | -| projected hinted post-Fusaka, excluding calldata | 5,603,049 | -| inverse witness bytes | 27,360 | -| projected hinted post-Fusaka, including calldata | 6,040,809 | - -### Real attestation, production-shaped per-cert split - -| step | current hinted gas | hint bytes | hinted MODEXP floor calls | projected post-Fusaka | fits cap? | -|------|-------------------:|-----------:|--------------------------:|----------------------:|:---------:| -| cabundle[0] root | 25,230 | 0 | 0 | 25,230 | YES | -| cabundle[1] | 6,074,268 | 27,456 | 3,056 | 7,430,364 | YES | -| cabundle[2] | 6,309,056 | 27,408 | 3,052 | 7,663,184 | YES | -| cabundle[3] | 6,075,746 | 27,408 | 3,052 | 7,429,874 | YES | -| client cert | 6,094,419 | 27,504 | 3,060 | 7,452,483 | YES | - -### Real attestation, cached hot path - -| metric | value | -|--------|------:| -| current hinted cached gas | 12,987,344 | -| inverse hint bytes | 27,312 | -| hinted MODEXP floor calls | 3,044 | -| projected post-Fusaka | 14,337,536 | -| fits cap? | YES | - -## Outcome - -The production-shaped hinted flow improves the prior replacement projection and -stays comfortably under the EIP-7825 per-transaction cap: - -- Cold split cert transactions: about 7.43M-7.66M gas post-Fusaka. -- Cached hot attestation transaction: about 14.34M gas post-Fusaka. - -This confirms that the near-term pure-EVM path is viable if the Nitro flow is -split into cert-cache transactions plus a cached attestation validation -transaction. - -## Soundness notes - -- Each supplied inverse is constrained on-chain by `denominator * inverse == 1` - modulo the relevant modulus. -- Truncated, mutated, and surplus hints are rejected in the isolated P384 tests. -- The production-shaped attestation path has a surplus-hint negative test. -- The benchmark witness collector still uses transient storage, but that is only - the test oracle. The verifier-under-measure carries hint state in memory. - -## Caveats - -- This is still benchmark-only code. Production `src/` remains untouched. -- The API currently takes `bytes memory`, matching existing contract style. A - deployment version should consider `bytes calldata` for external methods. -- The witness generator is not yet an off-chain CLI; it is a benchmark helper - that replays the verification and emits the exact packed inverse stream. -- Base L1 data fees for witness payloads are not included in execution gas. - -## Recommended next step - -Experiment 007 should turn this into an integration sketch: - -1. Add production interfaces for hinted cert caching and hinted attestation - validation. -2. Implement an off-chain witness generator CLI that emits one hint blob per - cert signature and one hint blob for the COSE attestation signature. -3. Price Base calldata/L1 data cost for the 27KB witness blobs. -4. Add a sequencing benchmark or script for the full cold flow: - root -> intermediate CAs -> client cert -> cached attestation. diff --git a/bench/agent_log/007-full-cold-warm-sequence.md b/bench/agent_log/007-full-cold-warm-sequence.md deleted file mode 100644 index 39fa371..0000000 --- a/bench/agent_log/007-full-cold-warm-sequence.md +++ /dev/null @@ -1,151 +0,0 @@ -# 007 - Full cold and warm hinted sequence - -Date: 2026-06-02 -Status: complete - -## Question - -Can we demonstrate the full Nitro attestation path as the transactions we would -submit on-chain, and distinguish the cold cert-cache setup from the warm-cache -reuse path? - -## Method - -Extended: - -- `test/bench/RealAttestationBench.t.sol` - -Added: - -- `test_007_FullColdAndWarmHintedSequence` - -The test starts from a fresh `HintedCertManagerBench`, whose constructor has only -the AWS Nitro root pinned. It then: - -1. Decodes the real attestation fixture into `attestationTbs` and `signature`. -2. Parses cabundle pointers from the attestation payload. -3. Confirms `cabundle[0]` is the pinned AWS Nitro root cert. -4. Submits the minimum cold sequence: - - non-root CA cert 1 - - non-root CA cert 2 - - non-root CA cert 3 - - client/leaf cert - - final attestation validation -5. Submits the warm-cache validation again as one transaction. - -Projection formula remains: - -```text -postFusakaGas = currentHintedGas - + hinted MODEXP floor calls * (500 - 200) - + inverseHintBytes * 16 -``` - -The final term is pessimistic execution gas for all-nonzero hint calldata. - -## Cold vs warm cache - -Cold cache means the non-root certificate chain and leaf/client cert have not -yet been verified into `CertManager.verified`. - -Warm cache means those cert hashes are already stored in `CertManager.verified` -and are still valid at `block.timestamp`. A later attestation can reuse the -cached certs if it uses the exact same DER cert bytes and the same cached -leaf/client certificate. The final attestation signature still needs its own -P384 verification and its own hint blob. - -The AWS Nitro root is not a cold-cache transaction in this repo because it is -pinned in `CertManager` at deployment. If we also submit a root no-op check for -operational symmetry, this fixture becomes 6 transactions instead of the minimum -5. - -## Results - -Command: - -```sh -forge test --match-path test/bench/RealAttestationBench.t.sol -vv -``` - -All focused tests passed: `6 passed, 0 failed`. - -Full suite: - -```sh -forge test -forge test --gas-report -``` - -Full suite passed: `38 passed, 0 failed`. - -### Cold sequence - -Minimum transaction count for this fixture: **5**. - -| tx | action | current hinted gas | hint bytes | hinted MODEXP floor calls | projected post-Fusaka | fits cap? | -|----|--------|-------------------:|-----------:|--------------------------:|----------------------:|:---------:| -| 1 | cache CA cert | 6,074,285 | 27,456 | 3,056 | 7,430,381 | YES | -| 2 | cache CA cert | 6,309,132 | 27,408 | 3,052 | 7,663,260 | YES | -| 3 | cache CA cert | 6,075,884 | 27,408 | 3,052 | 7,430,012 | YES | -| 4 | cache client cert | 6,094,655 | 27,504 | 3,060 | 7,452,719 | YES | -| 5 | validate attestation | 12,990,821 | 27,312 | 3,044 | 14,341,013 | YES | - -Cold sequence totals: - -| metric | value | -|--------|------:| -| current gas total | 37,544,777 | -| projected post-Fusaka total | 44,317,385 | -| max projected tx gas | 14,341,013 | -| per-tx cap | 16,777,216 | - -### Warm sequence - -Warm-cache transaction count: **1**. - -| tx | action | current hinted gas | hint bytes | hinted MODEXP floor calls | projected post-Fusaka | fits cap? | -|----|--------|-------------------:|-----------:|--------------------------:|----------------------:|:---------:| -| 1 | warm validate | 12,947,764 | 27,312 | 3,044 | 14,297,956 | YES | - -## Outcome - -The full cold path is under the EIP-7825 cap transaction-by-transaction after -EIP-7883, using the hinted P384 path: - -- Minimum cold flow for this fixture: **5 transactions**. -- Optional root no-op replay: **6 transactions**. -- Warm-cache flow: **1 transaction**. -- Largest projected transaction: **14.34M gas**, leaving about **2.44M gas** of - headroom under the 16.78M cap. - -The important architectural conclusion is that the full cold flow does not need -one giant transaction. The cert chain is converted into durable on-chain cache -state first, and the final validation transaction then reuses that cache. - -## Audit notes - -- The root cert is trusted only because its hash/pubkey are pinned by the - deployed `CertManager` constructor. -- Every non-root cert transaction verifies the parent is cached, unexpired, a CA, - and has remaining path length before writing the child cert to cache. -- The client cert is cached as non-CA and rejected if later loaded as a CA. -- The final validation still parses the attestation, validates required fields, - reloads the cert bundle through `verifyCertBundle`, and verifies the COSE - attestation signature. -- Cached cert reuse is constrained by exact `keccak256(cert)` identity and - `notAfter >= block.timestamp`. -- Hint blobs do not relax cryptographic checks; each inverse is constrained by - modular multiplication and surplus hints are rejected. - -## Recommended next step - -Experiment 008 should move from benchmark-only wrappers to production candidate -code and audit docs: - -1. Add production hinted interfaces while preserving the existing unhinted API. -2. Port the explicit-memory hint cursor into a production `ECDSA384` variant. -3. Add negative tests at the full-flow level: mutated cert hint, truncated cert - hint, wrong parent hash, expired cached cert, CA/client role mismatch, and - final attestation wrong-key rejection. -4. Start `docs/hinted-p384-nitro-attestation.md` with the threat model, ABI, - witness format, sequence diagrams, and invariants. diff --git a/bench/agent_log/008-production-candidate-audit-docs.md b/bench/agent_log/008-production-candidate-audit-docs.md deleted file mode 100644 index af990a6..0000000 --- a/bench/agent_log/008-production-candidate-audit-docs.md +++ /dev/null @@ -1,159 +0,0 @@ -# 008 - Production candidate and audit docs - -Date: 2026-06-02 -Status: complete - -## Question - -Can the benchmark-only hinted flow be moved into additive production candidate -contracts, with audit-oriented documentation and full-flow negative tests? - -## Method - -Added production candidate files: - -- `src/ECDSA384Hinted.sol` -- `src/IHintedCertManager.sol` -- `src/CertManagerHinted.sol` -- `src/NitroValidatorHinted.sol` - -Added audit documentation: - -- `docs/hinted-p384-nitro-attestation.md` - -Updated benchmark tests: - -- `test/bench/RealAttestationBench.t.sol` - -The original unhinted contracts remain unchanged. The hinted path is additive. - -## Implementation Summary - -`ECDSA384Hinted` is a production candidate derived from the benchmark verifier: - -- verifier-side transient-storage hint cursor removed, -- benchmark counters removed, -- benchmark inverse collector removed, -- hint state carried in the P384 call context memory, -- every supplied inverse constrained with modular multiplication, -- truncated and surplus hint streams rejected. - -`CertManagerHinted` adds: - -- `verifyCACertWithHints` -- `verifyClientCertWithHints` -- `loadVerified` - -`NitroValidatorHinted` adds: - -- `validateAttestationWithHints` - -The hinted validator uses empty hint streams for cert bundle checks: - -```text -verifyCACertWithHints(cert, parentHash, "") -verifyClientCertWithHints(cert, parentHash, "") -``` - -Cached certs return before signature verification. Missing certs reach hinted -P384 verification with an empty stream and revert, which prevents accidental -fallback to the unhinted P384 path. - -## Results - -Focused command: - -```sh -forge test --match-path test/bench/RealAttestationBench.t.sol -vv -``` - -Focused result: `13 passed, 0 failed`. - -Full verification: - -```sh -forge test -forge test --gas-report -``` - -Full suite result: `45 passed, 0 failed`. - -### Production Candidate Full Sequence - -| tx | action | current hinted gas | hint bytes | hinted MODEXP floor calls | projected post-Fusaka | fits cap? | -|----|--------|-------------------:|-----------:|--------------------------:|----------------------:|:---------:| -| 1 | cache CA cert | 5,424,159 | 27,456 | 3,056 | 6,780,255 | YES | -| 2 | cache CA cert | 5,659,854 | 27,408 | 3,052 | 7,013,982 | YES | -| 3 | cache CA cert | 5,426,606 | 27,408 | 3,052 | 6,780,734 | YES | -| 4 | cache client cert | 5,443,681 | 27,504 | 3,060 | 6,801,745 | YES | -| 5 | validate attestation | 12,351,354 | 27,312 | 3,044 | 13,701,546 | YES | - -Cold sequence totals: - -| metric | value | -|--------|------:| -| current gas total | 34,305,654 | -| projected post-Fusaka total | 41,078,262 | -| max projected tx gas | 13,701,546 | -| per-tx cap | 16,777,216 | - -Warm validation: - -| tx | action | current hinted gas | hint bytes | hinted MODEXP floor calls | projected post-Fusaka | fits cap? | -|----|--------|-------------------:|-----------:|--------------------------:|----------------------:|:---------:| -| 1 | warm validate | 12,308,297 | 27,312 | 3,044 | 13,658,489 | YES | - -## Negative Tests Added - -The production candidate rejects: - -- mutated cert hint: `bad inverse hint`, -- truncated cert hint: `inverse hint underflow`, -- wrong parent hash: `parent cert unverified`, -- expired cached cert: `cert expired`, -- cached CA/client role mismatch: `cert is not a CA`, -- missing warm cache: `inverse hint underflow`, -- invalid final attestation signature, -- surplus final attestation hint: `unused inverse hints`. - -## Deployment Caveat - -The production candidate is audit-friendly but not yet deployment-size optimized. -Gas-report deployment sizes: - -| contract | deployed size | -|----------|--------------:| -| `CertManager` | 25,315 bytes | -| `CertManagerHinted` | 30,947 bytes | -| `NitroValidatorHinted` | 25,745 bytes | - -This needs a dedicated size-reduction pass before Base deployment. The most -likely direction is moving P384 verification behind a shared verifier contract or -linked-library boundary so it is not embedded into multiple large contracts. - -## Outcome - -The production candidate preserves the post-Fusaka result with better measured -gas than the benchmark wrapper: - -- minimum cold flow: 5 transactions, -- optional root no-op flow: 6 transactions, -- warm flow: 1 transaction, -- largest projected transaction: 13.70M gas. - -The audit documentation now has the initial threat model, ABI, witness format, -invariants, cold/warm cache semantics, failure modes, gas table, and deployment -caveat. - -## Recommended Next Step - -Experiment 009 should focus on deployability and audit hardening: - -1. Reduce deployed bytecode size below chain limits. -2. Decide whether P384 should be an external verifier contract, linked library, - or split CertManager/NitroValidator deployment. -3. Add equivalence tests between `ECDSA384.verify` and `ECDSA384Hinted.verify` - across multiple signatures. -4. Turn the Solidity witness collector into an off-chain CLI. -5. Continue expanding the audit doc with exact code references and sequence - diagrams. diff --git a/bench/agent_log/009-deployable-external-p384-verifier.md b/bench/agent_log/009-deployable-external-p384-verifier.md deleted file mode 100644 index 7c0ae43..0000000 --- a/bench/agent_log/009-deployable-external-p384-verifier.md +++ /dev/null @@ -1,160 +0,0 @@ -# 009 - Deployable external P384 verifier - -Date: 2026-06-02 -Status: complete - -## Question - -Can the hinted production candidate be made deployable under EIP-170 while -preserving the post-Fusaka split-flow gas result? - -## Method - -The 008 contracts embedded hinted P384 verification into both the cert manager -and the attestation validator. That kept the code easy to audit, but left the -hinted contracts over the runtime bytecode limit. - -This experiment split P384 into a shared external verifier and made the hinted -manager/validator hinted-only: - -- `src/IP384Verifier.sol` -- `src/P384Verifier.sol` -- `src/CertManagerHintedExternal.sol` -- `src/NitroValidatorHintedExternal.sol` - -The canonical hinted names now wrap the externalized variants: - -- `src/CertManagerHinted.sol` -- `src/NitroValidatorHinted.sol` - -The benchmark test now deploys: - -```text -P384Verifier -CertManagerHinted(P384Verifier) -NitroValidatorHinted(CertManagerHinted, P384Verifier) -``` - -`CertManagerHinted` still implements `ICertManager` through -`IHintedCertManager`, but its unhinted `verifyCACert` and `verifyClientCert` -entrypoints intentionally revert with `use hinted cert verification`. - -## Results - -Source-only size command: - -```sh -forge build src --sizes -``` - -Deployable runtime sizes: - -| contract | runtime size | EIP-170 margin | -|----------|-------------:|---------------:| -| `P384Verifier` | 7,805 bytes | 16,771 bytes | -| `CertManagerHinted` | 18,496 bytes | 6,080 bytes | -| `NitroValidatorHinted` | 13,101 bytes | 11,475 bytes | - -The `test/bench` instrumentation contracts remain oversized and are intentionally -excluded from the deployable source-size check. - -Focused sequence command: - -```sh -forge test --match-path test/bench/RealAttestationBench.t.sol --match-test test_007_FullColdAndWarmHintedSequence -vv -``` - -Focused result: `1 passed, 0 failed`. - -Full verification: - -```sh -forge test -``` - -Full suite result: `47 passed, 0 failed`. - -Gas-report command: - -```sh -forge test --gas-report --match-path test/bench/RealAttestationBench.t.sol -``` - -Gas-report result: `15 passed, 0 failed`. - -### Deployable Full Sequence - -| tx | action | current hinted gas | hint bytes | hinted MODEXP floor calls | projected post-Fusaka | fits cap? | -|----|--------|-------------------:|-----------:|--------------------------:|----------------------:|:---------:| -| 1 | cache CA cert | 5,441,446 | 27,456 | 3,056 | 6,797,542 | YES | -| 2 | cache CA cert | 5,674,610 | 27,408 | 3,052 | 7,028,738 | YES | -| 3 | cache CA cert | 5,441,378 | 27,408 | 3,052 | 6,795,506 | YES | -| 4 | cache client cert | 5,458,159 | 27,504 | 3,060 | 6,816,223 | YES | -| 5 | validate attestation | 12,330,471 | 27,312 | 3,044 | 13,680,663 | YES | - -Cold sequence totals: - -| metric | value | -|--------|------:| -| current gas total | 34,346,064 | -| projected post-Fusaka total | 41,118,672 | -| max projected tx gas | 13,680,663 | -| per-tx cap | 16,777,216 | - -Warm validation: - -| tx | action | current hinted gas | hint bytes | hinted MODEXP floor calls | projected post-Fusaka | fits cap? | -|----|--------|-------------------:|-----------:|--------------------------:|----------------------:|:---------:| -| 1 | warm validate | 12,287,414 | 27,312 | 3,044 | 13,637,606 | YES | - -## Tests Added - -- `test_009_DeployableHintedContractsFitEIP170` -- `test_009_DeployableCertManagerDisablesUnhintedEntrypoints` - -These supplement the 008 negative tests for: - -- mutated cert hints, -- truncated cert hints, -- wrong parent hashes, -- expired cached certs, -- cached CA/client role mismatch, -- missing warm cache, -- invalid final attestation signatures, -- surplus final attestation hints. - -## Soundness Notes - -- Externalizing P384 does not trust the verifier caller. Hints are still consumed - inside `ECDSA384Hinted` and each inverse is constrained before use. -- `CertManagerHinted` disables the inherited unhinted interface methods, so a - caller cannot accidentally route through the old MODEXP-heavy path on the - hinted manager. -- `NitroValidatorHinted` has no unhinted validation method. Its cached cert - bundle checks still pass empty hint streams, which means missing cache entries - fail rather than falling back to cert signature verification. -- The external verifier address is immutable in both `CertManagerHinted` and - `NitroValidatorHinted`. -- The external call adds a small gas cost, but the max projected transaction - remains 3.10M gas below the 16.78M cap. - -## Outcome - -The deployable hinted architecture satisfies both constraints measured so far: - -- every deployable hinted runtime is below EIP-170, -- the full real-attestation cold and warm flows remain below the EIP-7825 - per-transaction cap after the EIP-7883 projection. - -## Recommended Next Step - -Experiment 010 should focus on turning the Solidity witness oracle into an -off-chain witness generator and hardening equivalence tests: - -1. Add a CLI that parses a DER cert or COSE attestation signature and emits the - exact 48-byte inverse hint stream expected by `P384Verifier`. -2. Cross-check CLI output against `P384HintCollectorBench` for the real fixture. -3. Add direct equivalence tests for `P384Verifier` vs the unhinted verifier on - known-good and known-bad signatures. -4. Expand `docs/hinted-p384-nitro-attestation.md` with code-reference tables for - each invariant before audit handoff. diff --git a/bench/agent_log/010-offchain-witness-generator.md b/bench/agent_log/010-offchain-witness-generator.md deleted file mode 100644 index 2e2ea4a..0000000 --- a/bench/agent_log/010-offchain-witness-generator.md +++ /dev/null @@ -1,143 +0,0 @@ -# 010 - Off-chain witness generator - -Date: 2026-06-02 -Status: complete - -## Question - -Can the Solidity-only witness oracle be replaced with an off-chain generator that -emits the exact 48-byte inverse hint stream expected by `P384Verifier`? - -## Method - -Added: - -- `bench/p384_hints.js` - -The generator is dependency-free Node.js: - -- uses built-in `BigInt` for P384 arithmetic, -- uses built-in `crypto` for SHA-384, -- mirrors the Solarity affine Strauss-Shamir P384 verification order, -- records every modular inverse denominator in the same order as - `ECDSA384Hinted`, -- emits each inverse as exactly 48-byte big-endian bytes. - -Supported commands: - -```sh -node bench/p384_hints.js verify --hash <0xhash> --signature <0xr_s> --pubkey <0xxy> -node bench/p384_hints.js cert --cert <0xder|base64|@file> --pubkey <0xparent_xy> -node bench/p384_hints.js attestation --attestation <0xcose|base64|@file> --pubkey <0xleaf_xy> -``` - -Added optional FFI cross-check: - -- `test_010_OffchainWitnessGeneratorMatchesSolidityCollector` - -The test is skipped in normal `forge test` runs. Enable it with: - -```sh -NITRO_RUN_FFI=true forge test --ffi \ - --match-path test/bench/RealAttestationBench.t.sol \ - --match-test test_010_OffchainWitnessGeneratorMatchesSolidityCollector -vv -``` - -## Results - -Syntax check: - -```sh -node --check bench/p384_hints.js -``` - -Result: pass. - -Default focused test: - -```sh -forge test --match-path test/bench/RealAttestationBench.t.sol --match-test test_010 -vv -``` - -Result: pass, with the FFI check skipped unless `NITRO_RUN_FFI=true`. - -Enabled FFI cross-check: - -```sh -NITRO_RUN_FFI=true forge test --ffi \ - --match-path test/bench/RealAttestationBench.t.sol \ - --match-test test_010_OffchainWitnessGeneratorMatchesSolidityCollector -vv -``` - -Result: `1 passed, 0 failed`. - -The enabled test checked 5 real-fixture signatures: - -| signature | source | -|-----------|--------| -| 1 | non-root CA cert | -| 2 | non-root CA cert | -| 3 | non-root CA cert | -| 4 | client/leaf cert | -| 5 | COSE attestation | - -For each signature, the Node generator output matched -`P384HintCollectorBench` byte-for-byte. The generated hints were then used to -cache the cert chain and validate the Nitro attestation through the deployable -hinted contracts. - -Final attestation hint stream size: `27,312` bytes. - -Full suite: - -```sh -forge test -``` - -Result: `48 passed, 0 failed`. - -Source-only deployability check: - -```sh -forge build src --sizes -``` - -Result: pass; hinted runtime sizes unchanged from 009. - -## Soundness Notes - -- The off-chain generator is not trusted by the contracts. A malicious or buggy - generator can only produce hints that are accepted if every inverse satisfies - the on-chain `denominator * inverse == 1 mod m` check. -- The generator must still match the verifier's deterministic hint order. The - FFI test proves this for the real fixture against the Solidity collector. -- The `cert` mode parses DER TBS and ECDSA `r,s`; the `attestation` mode - reconstructs Nitro's COSE `Sig_structure`. Both paths converge to the same - `verify` hint generator. -- The default test suite does not require FFI, so CI and normal local tests do - not depend on Node process execution. - -## Outcome - -The witness-generation loop is now practical: - -1. Generate hints off-chain with `bench/p384_hints.js`. -2. Submit cert-cache transactions with cert signature hints. -3. Submit the warm attestation validation transaction with attestation signature - hints. -4. Use the optional FFI test to prove local generator equivalence before audit or - deployment rehearsals. - -## Recommended Next Step - -Experiment 011 should harden audit evidence around equivalence and negative -behavior: - -1. Add direct `P384Verifier` equivalence tests against the original unhinted - verifier for known-good and known-bad signatures. -2. Add CLI negative tests outside Solidity for malformed DER, malformed COSE, - wrong pubkey, wrong signature, and truncated inputs. -3. Add code-reference tables to `docs/hinted-p384-nitro-attestation.md` mapping - every audit invariant to the exact Solidity and JS locations. -4. Consider a small deployment rehearsal script that prints the cold/warm - transaction sequence with generated hints and expected calldata sizes. diff --git a/docs/hinted-p384-nitro-attestation.md b/docs/hinted-p384-nitro-attestation.md new file mode 100644 index 0000000..e77f8b1 --- /dev/null +++ b/docs/hinted-p384-nitro-attestation.md @@ -0,0 +1,541 @@ +# Hinted P384 Nitro Attestation + +On-chain verification of AWS Nitro Enclave attestation documents that stays under the +Fusaka per-transaction gas cap — without a new chain precompile and without ZK. + +**Background.** An AWS Nitro Enclave produces a signed *attestation document* (a +COSE/CBOR structure carrying an X.509 certificate chain rooted at a pinned AWS CA) +that proves a given workload ran inside a genuine enclave. Verifying it on-chain means +checking that certificate chain and the document signature — all ECDSA over the NIST +P-384 curve (secp384r1). P-384 verification leans heavily on the EVM's MODEXP +precompile, and that is exactly what the Fusaka upgrade reprices. + +- **Problem:** the Fusaka MODEXP repricing makes a standard on-chain P-384 verifier + far too expensive to land on an OP-Stack L2 such as Base. +- **Idea:** the expensive step (modular inversion) is *hard to compute but easy to + check*, so the caller supplies it as a verified **hint**. +- **Result:** a full cold verification fits in **5 transactions, each ≤ ~13.8M gas** + post-Fusaka — under the 16,777,216 cap. + +> This guide is self-contained: it covers the construction, why it is sound, the +> complete set of code changes, measured gas, deployment, testing, and known caveats. + +--- + +## 1. Why this exists + +A P384 ECDSA verify is dominated by **modular inversions** computed on-chain via the +MODEXP precompile (`b⁻¹ = b^(p−2) mod p`, a 384-bit exponentiation). One signature +verify performs ~570 of them. + +Two Fusaka changes break the existing validator: + +- **EIP-7883** reprices large-operand MODEXP. A single P384 inversion goes from + 8,170 → 81,792 gas (~10×). +- **EIP-7825** caps a transaction at **16,777,216 gas** (2²⁴). + +The consequences: + +| | now | post-Fusaka | +|---|---:|---:| +| one signature verify | ~7.9M | **~50.6M** (≈3× the cap) | +| full attestation (5 verifies) | ~53M | **~267M** (≈16× the cap) | + +A single signature verify is atomic and already exceeds the cap by ~3×, so splitting +the chain per-certificate is **necessary but not sufficient** — the unit of work +itself no longer fits in a transaction. + +## 2. The core idea: checked hints + +Computing a modular inverse on-chain is expensive; **checking** a proposed inverse is +a single modular multiply. So the caller computes the inverses off-chain and passes +them in calldata as *hints*; the contract verifies each one before using it: + +``` +need a / b mod m (i.e. a · b⁻¹) + → read next hint `inv` + → require b · inv == 1 (mod m) // one multiply; rejects any wrong value + → use a · inv (mod m) +``` + +Nothing is trusted: a hint that fails the check reverts. The worst a malicious caller +can do is waste their own gas — never forge a verification (see *Why this is sound*, +§4). + +This does **not** reduce the *number* of MODEXP calls. It replaces each ~570 +expensive 384-bit inversions (the part EIP-7883 punishes ~10×) with a floor-priced +multiply. Per-signature gas drops from ~50.6M to ~4M post-Fusaka, which makes the +whole attestation splittable into transactions that each fit the cap — see §7. + +## 3. Architecture + +``` + caller + | | + verifyCACertWithHints / validateAttestationWithHints + verifyClientCertWithHints | + v v + +----------------------+ +----------------------+ + | CertManager |<-------| NitroValidator | + | verify + cache certs | cert | parse CBOR / COSE, | + | (root pinned) | chain | drive the cert chain | + +----------------------+ +----------------------+ + | | + | P384 signature + inverse hints + v v + +---------------------------------+ + | P384Verifier | + | verifyP384SignatureWithHints | + | (ECDSA-P384 + hint checking) | + +---------------------------------+ +``` + +Three deployable contracts: + +- **`P384Verifier`** — all ECDSA-P384 math and hint checking, behind one external + call. It is isolated so the parser/cache contracts stay under the EIP-170 code-size + limit. It uses the patched `ECDSA384` library in the `solidity-lib` submodule. + `CertManager` and `NitroValidator` hold **immutable** references to it. +- **`CertManager`** — parses/validates certificates, caches verified ones, and + pins the AWS Nitro root. Implements `ICertManager`. +- **`NitroValidator`** — parses the CBOR/COSE attestation and drives the + certificate chain through `CertManager`. + +**No unhinted fallback.** The hinted entrypoints are +`verifyCACertWithHints`, `verifyClientCertWithHints`, and +`validateAttestationWithHints`. `CertManager`'s no-hint +`verifyCACert` / `verifyClientCert` **revert** (`use hinted cert verification`), so a +caller cannot accidentally invoke the expensive path. `NitroValidator.validateAttestation` +also reverts (`use hinted attestation verification`). The hinted changes are applied +directly to the original `CertManager.sol` and `NitroValidator.sol` files, instead of +shipping parallel copied contracts, so reviewers can audit the line diff against the +previous implementation. + +## 4. How hints work + +### Invariant +For every division `a / b mod m` the verifier pulls the next hint `inv`, requires +`b · inv ≡ 1 (mod m)`, then uses `a · inv mod m` (§2). The modulus is contextual: + +- **`n`** (the scalar/group order) for the two ECDSA scalar divisions, and +- **`p`** (the field prime) for all elliptic-curve point arithmetic. + +The choice is structural, not a runtime flag: point divisions are hard-wired to `p` +and scalar divisions carry an explicit `n`, so the two can never be confused. + +### Hint stream +One packed byte stream per signature: + +``` +inverse_0 ‖ inverse_1 ‖ … ‖ inverse_{k-1} +``` + +- each inverse is exactly **48 bytes, big-endian**; +- consumed **sequentially** in the verifier's deterministic execution order; +- **not self-describing** — position `i` is bound to the `i`-th inversion by execution + order alone. There are no tags or lengths in the stream. + +### Count +`k` = the number of field inversions the verify actually performs. It is +**data-dependent** (the ladder's point additions depend on the scalar bit pattern), +so it varies slightly per signature — measured 569–573 on the production fixture. The +contract never assumes a fixed `k`; it enforces an exact match at runtime (below). + +### Trust and rejection +Hints are public, caller-proposed, and fully constrained before use, so they add no +trust. The verifier rejects: + +| condition | revert | +|-----------|--------| +| a hint fails `b · inv ≡ 1` | `bad inverse hint` | +| the stream runs out mid-verify | `inverse hint underflow` | +| the stream has leftover bytes | `unused inverse hints` | + +The last two together force the stream length to be **exactly** `48 · k`. + +### Why this is sound +Both moduli (`p` and `n`) are prime, so any nonzero element has a *unique* inverse. + +- A hint that passes `b · inv ≡ 1 (mod m)` therefore **is** the true inverse `b⁻¹` + (by uniqueness) — bit-identical to what the original `b^(m-2) mod m` would compute. + Every hinted division yields the same field element as the original. +- The only operations that changed are these divisions, and everything downstream + reduces mod `m`. So the hinted verifier returns the **same accept/reject decision** + as the original verifier for every input — it accepts exactly the same set of + signatures. Hinting introduces **no new forgery surface**. +- A zero or non-invertible denominator cannot be exploited: `0 · inv ≡ 1` is + impossible, so the check simply reverts. +- Because hints are public and fully constrained before use, a malicious caller can at + most cause a revert (wasted gas), never a false accept. (A non-canonical hint such + as `inv + m` also passes the check and is harmless, since every use reduces mod `m`.) + +The equivalence anchor for this argument is that with hints **disabled** the code is +identical to the upstream verifier; the hinted branches only *substitute a +pre-verified value* for the same inverse the original computes. + +### What a failed verification does +- With hints **disabled**, behavior is identical to the original verifier. +- A *well-formed but invalid* signature consumes its full hint stream and the verify + returns `false` normally. +- A signature that fails an **early** guard (scalar bounds or the on-curve check) + returns before consuming any hints. If a non-empty hint stream was supplied, the + exact-consumption guard then **reverts** (`unused inverse hints`) instead of + returning `false`. Both outcomes block acceptance, but integrators should expect a + revert — not a `false` return — for such inputs on the hinted entrypoint. + +## 5. Preparing calls off-chain + +An off-chain hint generator (Node.js `BigInt`, no dependencies) reproduces the +verifier's execution order and emits the packed stream. In this repository it is +`tools/p384_hints.js`: + +```sh +node tools/p384_hints.js verify --hash <0x 48B> --signature <0x r‖s> --pubkey <0x x‖y> +node tools/p384_hints.js cert --cert <0x DER | base64 | @file> --pubkey <0x parent x‖y> +node tools/p384_hints.js attestation --attestation <0x COSE | base64 | @file> --pubkey <0x leaf x‖y> +``` + +`cert` mode SHA-384-hashes the DER TBS certificate and packs the DER signature into +`r‖s`; `attestation` mode reconstructs the COSE `Sig_structure` Nitro signs and +hashes it. The generator only has to get the **order and count** right — every value +is re-checked on-chain (§4), so a generator bug causes a revert, never a false accept. + +The CLI is a reference implementation and demo convenience, not a requirement of the +on-chain design. In production, the caller service should implement the same +deterministic hint generation in its own off-chain stack (for example Go or Rust), +or use `tools/p384_hints.js` as a byte-for-byte reference while porting. The smart +contracts only see ordinary calldata: + +```solidity +verifyCACertWithHints(bytes cert, bytes32 parentCertHash, bytes signatureHints) +verifyClientCertWithHints(bytes cert, bytes32 parentCertHash, bytes signatureHints) +validateAttestationWithHints(bytes attestationTbs, bytes signature, bytes attestationHints) +``` + +So the production service prepares: + +1. the DER certificates from the Nitro `cabundle` and `certificate`; +2. the parent certificate hashes (`keccak256(derCert)`); +3. one packed inverse-hint stream per uncached certificate signature; +4. the COSE `Sig_structure` hash, the document signature, and its hint stream; and +5. the ABI-encoded calls for the cold or warm sequence in §6. + +The service can also call `loadVerified(certHash)` before submitting transactions to +choose the shortest path: full cold chain, cached CA chain plus new leaf, or fully +warm document validation. + +For integration testing and porting, `tools/hinted_attestation_calls.js` builds the +full transaction plan from one Nitro attestation: + +```sh +node tools/hinted_attestation_calls.js prepare \ + --attestation <0x COSE | base64 | @file> \ + --cert-manager <0x CertManager> \ + --validator <0x NitroValidator> +``` + +It outputs JSON with `cold` and `warm` arrays. Each item contains: + +- `to`: target contract address; +- `function`: Solidity function signature; +- `args`: decoded ABI arguments, including the packed hint stream; +- `calldata`: ready-to-submit ABI calldata; +- `hintBytes` / `hintCount`: the witness size. + +The bundled fixture can be prepared with: + +```sh +node tools/hinted_attestation_calls.js fixture \ + --cert-manager <0x CertManager> \ + --validator <0x NitroValidator> +``` + +This preparer is also reference tooling, not a production dependency. A production +Go or Rust service should implement the same deterministic steps in-process: + +1. decode the Nitro COSE_Sign1 envelope and payload; +2. extract the `cabundle` DER certificates and leaf `certificate`; +3. compute `keccak256(derCert)` identities for cache lookups and parent hashes; +4. compute inverse hints for every cert/document signature that will be verified in + the chosen transaction sequence; +5. ABI-pack the hinted contract calls; and +6. submit the calls in dependency order. + +In Go this maps naturally to `abi.Pack` plus a Keccak implementation from the +Ethereum stack; in Rust, to an ABI encoder such as `alloy-sol-types` / `ethers` +and the corresponding Keccak primitive. The Solidity contracts do not know or care +which language produced the bytes — malformed hints or mismatched calldata simply +revert on-chain. + +## 6. The attestation verification flow + +### Certificate chain +A Nitro attestation carries a CA bundle (`cabundle`) — in the reference attestation, +4 entries — whose first entry is the AWS Nitro root, plus the enclave's signing +certificate: + +- `cabundle[0]`: the **AWS Nitro root CA**. This is pinned in + `CertManager` at deployment as the trust anchor. It is **not** signature + verified on-chain, so it is **not** one of the expensive P384 verification + transactions. +- `cabundle[1]`: the **regional CA**, verified against the pinned root and cached. +- `cabundle[2]`: the **zonal CA**, verified against the regional CA and cached. +- `cabundle[3]`: the **issuer / instance CA**, verified against the zonal CA and + cached. +- `certificate`: the **client / leaf cert** that signed the attestation document, + verified against the issuer / instance CA and cached. +- the final **COSE document signature** is verified with the cached leaf cert's key. + +The common counting mistake is to include the root CA as a transaction. In this +construction the root is a pinned trust anchor, so a cold verification does **5 P384 +signature checks**, each in its own transaction: 3 non-root CAs, 1 leaf cert, and 1 +document signature. + +### Cold sequence (empty cache) +| tx | action | hints supplied | +|----|--------|----------------| +| - | pinned root CA trust anchor | none; no transaction | +| 1 | verify + cache regional CA | cert signature hints | +| 2 | verify + cache zonal CA | cert signature hints | +| 3 | verify + cache issuer / instance CA | cert signature hints | +| 4 | verify + cache client / leaf cert | cert signature hints | +| 5 | validate Nitro attestation document | attestation signature hints | + +### Warm sequence (cache populated) +Once the leaf and its CA chain are cached and unexpired, a later attestation signed by +the same leaf is a **single transaction** (`validateAttestationWithHints`) carrying +only the COSE signature hints. The cabundle certs are not re-verified — they are +reloaded by `keccak256(cert)` identity and their cached metadata is re-checked. + +Practical reuse cases: + +- **cold chain:** no relevant certs cached → 5 verification transactions; +- **CA chain cached, new leaf:** verify/cache the new leaf, then validate the + document → 2 transactions; +- **CA chain and leaf cached:** validate the document only → 1 transaction. + +**Cache reuse** is allowed when: the submitted DER hashes to a cached cert; the cert +is unexpired (`notAfter ≥ block.timestamp`); the cached CA/client role matches; parent +checks pass for non-root certs. The cache is global on-chain state — once any caller +verifies a cert, others reuse it until expiry. + +**Warm-only guard.** `validateAttestationWithHints` re-runs the cabundle checks with an +*empty* hint stream. Cached certs return before signature verification; a missing cert +sends the empty stream into P384 verification and reverts with +`inverse hint underflow`. This makes it impossible for the validator to silently fall +back to the expensive unhinted path during final validation. + +## 7. Measured gas + +Cold sequence, measured from successful Base Sepolia receipts after the Fusaka +upgrade. These numbers exclude the one-time contract deployment transactions and +include the five logical verification transactions from §6. The receipts were +captured from the equivalent hinted deployment before the auditability refactor folded +the changes into the original `CertManager` / `NitroValidator` names; rerun the demo +after redeploying if exact gas for this commit is needed. + +| tx | action | hint bytes | Base Sepolia gas used | +|----|--------|-----------:|----------------------:| +| 1 | cache regional CA | 27,456 | 6,825,140 | +| 2 | cache zonal CA | 27,408 | 7,053,669 | +| 3 | cache issuer / instance CA | 27,408 | 6,813,103 | +| 4 | cache client / leaf cert | 27,504 | 6,825,004 | +| 5 | validate Nitro attestation document | 27,312 | 13,775,541 | + +Warm-cache validation reuses the cached CA chain and cached leaf cert, so it repeats +only tx 5: + +| path | action | Base Sepolia gas used | +|------|--------|----------------------:| +| warm | validate Nitro attestation document | 13,775,541 | + +| metric | value | +|--------|------:| +| cold verification total | 41,292,457 | +| cache setup subtotal, tx 1-4 | 27,516,916 | +| **max verification tx** | **13,775,541** | +| warm validation tx | 13,775,541 | +| per-tx cap (EIP-7825) | 16,777,216 | +| headroom under cap | 3,001,675 | + +Every verification transaction landed under the cap. The document validation +transaction was sent with an explicit `16,500,000` gas limit after +`eth_estimateGas` returned `13,886,038`; the receipt used `13,775,541` gas. + +Representative Base Sepolia tx hashes: + +- cold document validation: + `0x2cb00a86b943a29cda28be89ad990d9ca29c502c8350ba1ab89e726d44d6702e`; +- warm document validation: + `0x0563932374215073fd92f8d79920af0f5d79be25c92d26bab910de8bb16a21c7`. + +## 8. Deployment + +Deploy order (both verifier references are immutable constructor args): + +1. `P384Verifier` +2. `CertManager(P384Verifier)` +3. `NitroValidator(CertManager, P384Verifier)` + +Runtime sizes (`forge build --sizes`); EIP-170 limit is 24,576 bytes: + +| contract | runtime size | margin | +|----------|-------------:|-------:| +| `P384Verifier` | 7,805 | 16,771 | +| `CertManager` | 19,372 | 5,204 | +| `NitroValidator` | 14,062 | 10,514 | + +(Test-only helper contracts are not part of the deployable contract set.) + +## 9. Testing & audit + +The hinted contracts are exercised against the real fixture and adversarial inputs. +Covered failure modes: mutated hint, truncated hint, surplus hint, wrong parent hash, +expired cached cert, CA/client role mismatch, missing warm cache, invalid final +signature, disabled unhinted entrypoints, EIP-170 fit, and off-chain↔on-chain hint +equivalence. + +| invariant | component | how it is tested | +|-----------|-----------|------------------| +| every supplied inverse is constrained before use | the two inversion sites | mutated / truncated / surplus hints are rejected | +| hints consumed in verifier order, with exact count | verifier + hint reader | byte-for-byte match against an independent off-chain collector | +| scalar inverses use `n`, field inverses use `p` | verifier | a known-good signature is accepted; a modulus swap would reject it | +| hinted verifier matches the original accept/reject set | `P384Verifier` | accepts a valid signature; rejects mutated hash / signature / public key | +| no unhinted fallback via hinted entrypoints | `CertManager` | the unhinted entrypoints revert | +| warm validation requires cached certs | `NitroValidator` | empty-hint final validation reverts when a cert is uncached | +| off-chain generator matches on-chain order, rejects bad input | off-chain generator | equivalence test + negative-input checks | + +The off-chain↔on-chain hint equivalence is checked under an FFI test (it shells out to +the generator and compares streams byte-for-byte), so the generator and the contract +can never silently diverge. + +**For auditors.** The entire trust delta versus the upstream library is the two +`if (hintsEnabled)` branches shown in the appendix; the soundness argument is in §4 +(*Why this is sound*). Reviewers should confirm: + +1. every supplied inverse is constrained by `b · inv ≡ 1` in the correct modulus + **before** it is used; +2. point inverses use the field prime `p` and scalar inverses use the group order `n`, + with no crossover; +3. the underflow and surplus guards together force the hint count to match exactly + (no truncated or leftover hints); +4. the hints-disabled path is byte-identical to the upstream verifier; and +5. the curve parameters (`p`, `n`, `G`, `a`, `b`, low-`s` bound) are correct. + +## 10. Caveats and notes + +- **Calldata cost is separate from the gas above.** The tables in §7 are *execution* + gas. Each signature also carries ~27 KB of hint calldata, which on an OP-Stack L2 + incurs an L1 data-availability fee on top of execution gas — budget for it + separately. It does not affect whether a transaction fits the per-transaction + execution-gas cap. +- **Acceptance rule is inherited unchanged.** The verifier accepts when + `x_R mod n == r`, exactly as the upstream library, including its handling of the + negligible (~2⁻¹⁹⁰) case where the recovered x-coordinate lies in `[n, p)`. Hinting + does not alter this. +- **Audit boundary.** Only the two inversion branches are new cryptographic code; the + rest of the verifier is the upstream library unchanged, and the certificate parser, + cache, and CBOR/COSE handling are the pre-existing validator logic. +- **The generator is liveness-critical, not trust-critical.** A bug in the off-chain + hint generator can only cause a revert (every value is re-checked on-chain), never a + false accept — but correct hints are required to verify at all, so the generator + must stay in sync with the verifier's execution order. Its DER/CBOR parsing should + be reviewed for robustness. + +## 11. On-chain demo + +A Base Sepolia run of the §6 cold sequence needs: an RPC URL, a funded broadcaster +key, the attestation bytes to submit, and a certificate expiry window. The bundled +fixture is a January 2026 real Nitro attestation, so the demo script uses +`CertManagerDemo` with an explicit expiry grace. Production deployments should +use `CertManager`, which keeps strict X.509 validity checks. + +The happy-path script is `script/BaseSepoliaDemo.s.sol`. It deploys +`P384Verifier`, `CertManagerDemo`, and `NitroValidator`; generates cert +hints with `tools/p384_hints.js cert`; submits the four cache transactions; generates +COSE hints with `tools/p384_hints.js attestation`; submits the final +`validateAttestationWithHints`; then submits one warm-cache validation against the +same cached chain and leaf. The script uses `vm.ffi` only because Foundry Solidity +scripts cannot run the off-chain parsing and BigInt witness code in-process. In the +production flow, the caller service prepares the same hints and ABI calldata before +submitting ordinary transactions. + +--- + +## Appendix: the code change + +The hinted verifier is the upstream `ECDSA384` verifier from the +`dl-solarity/solidity-lib` dependency, patched in place on the +`leanthebean/solidity-lib` fork with **one operation — modular inversion — made +hint-aware in two places**. Everything else (the Strauss–Shamir ladder, precompute +table, on-curve check, scalar bounds, final `x_R == r`) is unchanged. When hints are +disabled the code follows the original path, so the unhinted path is the equivalence +anchor. + +The entire trust delta is these two `if (hintsEnabled)` branches. + +**Point arithmetic — inverses mod `p`** (`moddivAssign`): + +```diff + function moddivAssign(uint256 call_, uint256 a_, uint256 b_) internal view { + unchecked { ++ uint256 baseCall_ = call_; ++ if (_hintsEnabled(call_)) { ++ uint256 inv_ = _nextInverseHint(call_); ++ uint256 check_ = modmul(call_, b_, inv_); // modmul modulus is baked = p ++ require(eqInteger(check_, 1), "bad inverse hint"); // b * inv == 1 (mod p) ++ assembly { // b_ <- inv ++ mstore(b_, mload(inv_)) ++ mstore(add(b_, 0x20), mload(add(inv_, 0x20))) ++ } ++ } else { + assembly { // ORIGINAL Fermat path (verbatim) + call_ := add(call_, INV_OFFSET) + mstore(add(0x60, call_), mload(b_)) + mstore(add(0x80, call_), mload(add(b_, 0x20))) + pop(staticcall(gas(), 0x5, call_, 0x0120, b_, 0x40)) + } ++ } +- modmulAssign(call_ - INV_OFFSET, a_, b_); ++ modmulAssign(baseCall_, a_, b_); // a <- a * b⁻¹ (mod p), unchanged + } + } +``` + +**ECDSA scalars — inverses mod `n`** (`modinv`, reached via `moddiv`): + +```diff + function modinv(uint256 call_, uint256 b_, uint256 m_) internal view returns (uint256 r_) { + unchecked { ++ if (_hintsEnabled(call_)) { ++ r_ = _nextInverseHint(call_); ++ uint256 check_ = _modmulWithMod(call_, b_, r_, m_); // explicit modulus m (= n) ++ require(eqInteger(check_, 1), "bad inverse hint"); // b * r == 1 (mod m) ++ return r_; ++ } + /* ... original Fermat path b^(m-2) mod m, unchanged ... */ + } + } +``` + +Both branches call the one new bounds-checked reader, which enforces the +no-truncation guard: + +```diff ++ function _nextInverseHint(uint256 call_) private pure returns (uint256 r_) { ++ /* read cursor_, length_ from scratch */ ++ require(cursor_ + 48 <= length_, "inverse hint underflow"); // never read past the stream ++ /* load the next 48 big-endian bytes into a field element; cursor += 48 */ ++ } +``` + +The remaining changes are **plumbing**, not logic, and none can affect the +accept/reject decision: + +- a few words of scratch memory holding the hint stream pointer, length, cursor, and + an enabled flag (`initCall` / `initCallWithHints`); +- the `verify` entrypoint split into `verify` (no hints, identical to the original) + and `verifyWithHints`, which adds the surplus guard + `require(consumed == length, "unused inverse hints")`. + +That is the complete set of changes versus the upstream verifier. diff --git a/lib/solidity-lib b/lib/solidity-lib index b947571..d4f594b 160000 --- a/lib/solidity-lib +++ b/lib/solidity-lib @@ -1 +1 @@ -Subproject commit b94757194de6436062c2d68118c0352be84ac4be +Subproject commit d4f594bc81da96121a3f7e652cce17f699f6cf00 diff --git a/script/BaseSepoliaDemo.s.sol b/script/BaseSepoliaDemo.s.sol new file mode 100644 index 0000000..abf6a48 --- /dev/null +++ b/script/BaseSepoliaDemo.s.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Script, console2} from "forge-std/Script.sol"; +import {CborDecode, CborElement, LibCborElement} from "../src/CborDecode.sol"; +import {CertManagerDemo} from "../src/demo/CertManagerDemo.sol"; +import {ICertManager} from "../src/ICertManager.sol"; +import {IP384Verifier} from "../src/IP384Verifier.sol"; +import {LibBytes} from "../src/LibBytes.sol"; +import {NitroValidator} from "../src/NitroValidator.sol"; +import {P384Verifier} from "../src/P384Verifier.sol"; + +contract NitroValidatorScriptParser is NitroValidator { + constructor() NitroValidator(ICertManager(address(1)), IP384Verifier(address(1))) {} + + function parseAttestation(bytes memory attestationTbs) external pure returns (Ptrs memory) { + return _parseAttestation(attestationTbs); + } +} + +contract BaseSepoliaDemo is Script { + using CborDecode for bytes; + using LibBytes for bytes; + using LibCborElement for CborElement; + + uint256 internal constant DEFAULT_DEMO_CERT_EXPIRY_GRACE_SECONDS = 365 days; + + struct Deployment { + P384Verifier p384Verifier; + CertManagerDemo certManager; + NitroValidator validator; + } + + function run() external { + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + uint256 graceSeconds = vm.envOr("DEMO_CERT_EXPIRY_GRACE_SECONDS", DEFAULT_DEMO_CERT_EXPIRY_GRACE_SECONDS); + + NitroValidatorScriptParser parser = new NitroValidatorScriptParser(); + bytes memory attestation = _loadAttestation(); + (bytes memory attestationTbs, bytes memory signature) = parser.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + + console2.log("base sepolia demo attestation bytes", attestation.length); + console2.log("attestationTbs bytes", attestationTbs.length); + console2.log("COSE signature bytes", signature.length); + console2.log("cabundle certs", ptrs.cabundle.length); + console2.log("demo cert expiry grace seconds", graceSeconds); + + vm.startBroadcast(privateKey); + + Deployment memory deployment; + deployment.p384Verifier = new P384Verifier(); + deployment.certManager = new CertManagerDemo(deployment.p384Verifier, graceSeconds); + deployment.validator = new NitroValidator(deployment.certManager, deployment.p384Verifier); + + console2.log("P384Verifier", address(deployment.p384Verifier)); + console2.log("CertManagerDemo", address(deployment.certManager)); + console2.log("NitroValidator", address(deployment.validator)); + + ICertManager.VerifiedCert memory leaf = _runColdHintedCache(deployment.certManager, attestationTbs, ptrs); + bytes memory attestationHints = _attestationHints(attestation, leaf.pubKey); + + deployment.validator.validateAttestationWithHints(attestationTbs, signature, attestationHints); + console2.log("hinted cold final validation submitted"); + + deployment.validator.validateAttestationWithHints(attestationTbs, signature, attestationHints); + console2.log("hinted warm validation submitted"); + + vm.stopBroadcast(); + } + + function _runColdHintedCache( + CertManagerDemo certManager, + bytes memory attestationTbs, + NitroValidator.Ptrs memory ptrs + ) internal returns (ICertManager.VerifiedCert memory leaf) { + bytes memory rootCert = attestationTbs.slice(ptrs.cabundle[0]); + bytes32 parentHash = keccak256(rootCert); + ICertManager.VerifiedCert memory parent = certManager.loadVerified(parentHash); + require(parent.pubKey.length > 0, "root not pinned"); + + console2.logBytes32(parentHash); + + for (uint256 i = 1; i < ptrs.cabundle.length; ++i) { + bytes memory caCert = attestationTbs.slice(ptrs.cabundle[i]); + bytes memory hints = _certHints(caCert, parent.pubKey); + parentHash = certManager.verifyCACertWithHints(caCert, parentHash, hints); + parent = certManager.loadVerified(parentHash); + require(parent.pubKey.length > 0, "CA not cached"); + console2.log("cached non-root CA index", i); + console2.logBytes32(parentHash); + } + + bytes memory clientCert = attestationTbs.slice(ptrs.cert); + bytes memory clientHints = _certHints(clientCert, parent.pubKey); + leaf = certManager.verifyClientCertWithHints(clientCert, parentHash, clientHints); + require(leaf.pubKey.length > 0, "leaf not cached"); + console2.log("cached client cert"); + console2.logBytes32(keccak256(clientCert)); + } + + function _loadAttestation() internal returns (bytes memory) { + if (vm.envOr("USE_BUNDLED_REAL_ATTESTATION", false)) { + string[] memory fixtureCommand = new string[](3); + fixtureCommand[0] = "node"; + fixtureCommand[1] = string.concat(vm.projectRoot(), "/tools/nitro_attestation_input.js"); + fixtureCommand[2] = "fixture"; + return vm.ffi(fixtureCommand); + } + + string memory input = vm.envOr("ATTESTATION_INPUT", string("")); + require(bytes(input).length != 0, "set ATTESTATION_INPUT or USE_BUNDLED_REAL_ATTESTATION"); + + bool repair = vm.envOr("REPAIR_MISSING_PUBLIC_KEY", false); + string[] memory inputCommand = new string[](repair ? 7 : 5); + inputCommand[0] = "node"; + inputCommand[1] = string.concat(vm.projectRoot(), "/tools/nitro_attestation_input.js"); + inputCommand[2] = "read"; + inputCommand[3] = "--input"; + inputCommand[4] = input; + if (repair) { + inputCommand[5] = "--repair"; + inputCommand[6] = "true"; + } + return vm.ffi(inputCommand); + } + + function _certHints(bytes memory cert, bytes memory parentPubKey) internal returns (bytes memory) { + // Demo-only FFI boundary. A production caller should compute these hints + // off-chain in its transaction-preparation service and pass them as + // calldata to verifyCACertWithHints / verifyClientCertWithHints. + string[] memory command = new string[](7); + command[0] = "node"; + command[1] = string.concat(vm.projectRoot(), "/tools/p384_hints.js"); + command[2] = "cert"; + command[3] = "--cert"; + command[4] = vm.toString(cert); + command[5] = "--pubkey"; + command[6] = vm.toString(parentPubKey); + return vm.ffi(command); + } + + function _attestationHints(bytes memory attestation, bytes memory leafPubKey) internal returns (bytes memory) { + // Demo-only FFI boundary. A production caller should compute these hints + // off-chain and pass them as calldata to validateAttestationWithHints. + string[] memory command = new string[](7); + command[0] = "node"; + command[1] = string.concat(vm.projectRoot(), "/tools/p384_hints.js"); + command[2] = "attestation"; + command[3] = "--attestation"; + command[4] = vm.toString(attestation); + command[5] = "--pubkey"; + command[6] = vm.toString(leafPubKey); + return vm.ffi(command); + } +} diff --git a/src/CertManager.sol b/src/CertManager.sol index ee7b2ab..e230e12 100644 --- a/src/CertManager.sol +++ b/src/CertManager.sol @@ -3,10 +3,9 @@ pragma solidity ^0.8.15; import {Sha2Ext} from "./Sha2Ext.sol"; import {Asn1Decode, Asn1Ptr, LibAsn1Ptr} from "./Asn1Decode.sol"; -import {ECDSA384} from "@solarity/libs/crypto/ECDSA384.sol"; -import {ECDSA384Curve} from "./ECDSA384Curve.sol"; import {LibBytes} from "./LibBytes.sol"; import {ICertManager} from "./ICertManager.sol"; +import {IP384Verifier} from "./IP384Verifier.sol"; // adapted from https://github.com/marlinprotocol/NitroProver/blob/f1d368d1f172ad3a55cd2aaaa98ad6a6e7dcde9d/src/CertManager.sol @@ -48,7 +47,11 @@ contract CertManager is ICertManager { // certHash -> VerifiedCert mapping(bytes32 => bytes) public verified; - constructor() { + IP384Verifier public immutable p384Verifier; + + constructor(IP384Verifier p384Verifier_) { + require(address(p384Verifier_) != address(0), "missing P384 verifier"); + p384Verifier = p384Verifier_; _saveVerified( ROOT_CA_CERT_HASH, VerifiedCert({ @@ -61,23 +64,44 @@ contract CertManager is ICertManager { ); } - function verifyCACert(bytes memory cert, bytes32 parentCertHash) external returns (bytes32) { - bytes32 certHash = keccak256(cert); - _verifyCert(cert, certHash, true, _loadVerified(parentCertHash)); - return certHash; + function verifyCACert(bytes memory, bytes32) external pure returns (bytes32) { + revert("use hinted cert verification"); } - function verifyClientCert(bytes memory cert, bytes32 parentCertHash) external returns (VerifiedCert memory) { - return _verifyCert(cert, keccak256(cert), false, _loadVerified(parentCertHash)); + function verifyClientCert(bytes memory, bytes32) external pure returns (VerifiedCert memory) { + revert("use hinted cert verification"); } - function _verifyCert(bytes memory certificate, bytes32 certHash, bool ca, VerifiedCert memory parent) - internal + function verifyCACertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) + external + returns (bytes32) + { + bytes32 certHash = keccak256(cert); + _verifyCert(cert, certHash, true, _loadVerified(parentCertHash), signatureHints); + return certHash; + } + + function verifyClientCertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) + external returns (VerifiedCert memory) { + return _verifyCert(cert, keccak256(cert), false, _loadVerified(parentCertHash), signatureHints); + } + + function loadVerified(bytes32 certHash) external view returns (VerifiedCert memory) { + return _loadVerified(certHash); + } + + function _verifyCert( + bytes memory certificate, + bytes32 certHash, + bool ca, + VerifiedCert memory parent, + bytes memory signatureHints + ) internal returns (VerifiedCert memory) { if (certHash != ROOT_CA_CERT_HASH) { require(parent.pubKey.length > 0, "parent cert unverified"); - require(parent.notAfter >= block.timestamp, "parent cert expired"); + require(!_certificateExpired(parent.notAfter), "parent cert expired"); require(parent.ca, "parent cert is not a CA"); require(!ca || parent.maxPathLen != 0, "maxPathLen exceeded"); } @@ -85,7 +109,7 @@ contract CertManager is ICertManager { // skip verification if already verified VerifiedCert memory cert = _loadVerified(certHash); if (cert.pubKey.length != 0) { - require(cert.notAfter >= block.timestamp, "cert expired"); + require(!_certificateExpired(cert.notAfter), "cert expired"); require(cert.ca == ca, "cert is not a CA"); return cert; } @@ -102,7 +126,7 @@ contract CertManager is ICertManager { maxPathLen = parent.maxPathLen - 1; } - _verifyCertSignature(certificate, tbsCertPtr, parent.pubKey); + _verifyCertSignatureWithHints(certificate, tbsCertPtr, parent.pubKey, signatureHints); cert = VerifiedCert({ ca: ca, notAfter: notAfter, maxPathLen: maxPathLen, subjectHash: subjectHash, pubKey: pubKey @@ -191,7 +215,11 @@ contract CertManager is ICertManager { notAfter = uint64(certificate.timestampAt(notAfterPtr)); require(notBefore <= block.timestamp, "certificate not valid yet"); - require(notAfter >= block.timestamp, "certificate not valid anymore"); + require(!_certificateExpired(notAfter), "certificate not valid anymore"); + } + + function _certificateExpired(uint256 notAfter) internal view virtual returns (bool) { + return notAfter < block.timestamp; } function _verifyExtensions(bytes memory certificate, Asn1Ptr extensionsPtr, bool ca) @@ -271,12 +299,26 @@ contract CertManager is ICertManager { } } - function _verifyCertSignature(bytes memory certificate, Asn1Ptr ptr, bytes memory pubKey) internal view { + function _verifyCertSignatureWithHints( + bytes memory certificate, + Asn1Ptr ptr, + bytes memory pubKey, + bytes memory signatureHints + ) internal view { Asn1Ptr sigAlgoPtr = certificate.nextSiblingOf(ptr); require(certificate.keccak(sigAlgoPtr.content(), sigAlgoPtr.length()) == CERT_ALGO_OID, "invalid cert sig algo"); bytes memory hash = Sha2Ext.sha384(certificate, ptr.header(), ptr.totalLength()); + bytes memory sigPacked = _certSignature(certificate, sigAlgoPtr); + require(p384Verifier.verifyP384SignatureWithHints(hash, sigPacked, pubKey, signatureHints), "invalid sig"); + } + + function _certSignature(bytes memory certificate, Asn1Ptr sigAlgoPtr) + internal + pure + returns (bytes memory sigPacked) + { Asn1Ptr sigPtr = certificate.nextSiblingOf(sigAlgoPtr); Asn1Ptr sigBPtr = certificate.bitstring(sigPtr); Asn1Ptr sigRoot = certificate.rootOf(sigBPtr); @@ -284,13 +326,7 @@ contract CertManager is ICertManager { Asn1Ptr sigSPtr = certificate.nextSiblingOf(sigRPtr); (uint128 rhi, uint256 rlo) = certificate.uint384At(sigRPtr); (uint128 shi, uint256 slo) = certificate.uint384At(sigSPtr); - bytes memory sigPacked = abi.encodePacked(rhi, rlo, shi, slo); - - _verifySignature(pubKey, hash, sigPacked); - } - - function _verifySignature(bytes memory pubKey, bytes memory hash, bytes memory sig) internal view { - require(ECDSA384.verify(ECDSA384Curve.p384(), hash, sig, pubKey), "invalid sig"); + sigPacked = abi.encodePacked(rhi, rlo, shi, slo); } function _saveVerified(bytes32 certHash, VerifiedCert memory cert) internal { diff --git a/src/CertManagerHinted.sol b/src/CertManagerHinted.sol deleted file mode 100644 index 1c7410a..0000000 --- a/src/CertManagerHinted.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import {CertManagerHintedExternal} from "./CertManagerHintedExternal.sol"; -import {IP384Verifier} from "./IP384Verifier.sol"; - -/// @notice Deployable hinted Nitro certificate manager. -/// @dev Kept as the canonical hinted name; P384 verification is delegated to `p384Verifier`. -contract CertManagerHinted is CertManagerHintedExternal { - constructor(IP384Verifier p384Verifier_) CertManagerHintedExternal(p384Verifier_) {} -} diff --git a/src/CertManagerHintedExternal.sol b/src/CertManagerHintedExternal.sol deleted file mode 100644 index 2e4324f..0000000 --- a/src/CertManagerHintedExternal.sol +++ /dev/null @@ -1,327 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import {Sha2Ext} from "./Sha2Ext.sol"; -import {Asn1Decode, Asn1Ptr, LibAsn1Ptr} from "./Asn1Decode.sol"; -import {IHintedCertManager} from "./IHintedCertManager.sol"; -import {IP384Verifier} from "./IP384Verifier.sol"; -import {LibBytes} from "./LibBytes.sol"; - -/// @notice Hinted-only Nitro certificate cache that keeps P384 verification in an external verifier contract. -/// @dev The unhinted ICertManager methods are intentionally disabled to avoid accidental MODEXP-heavy fallbacks. -contract CertManagerHintedExternal is IHintedCertManager { - using Asn1Decode for bytes; - using LibAsn1Ptr for Asn1Ptr; - using LibBytes for bytes; - - event CertVerified(bytes32 indexed certHash); - - bytes32 internal constant ROOT_CA_CERT_HASH = 0x311d96fcd5c5e0ccf72ef548e2ea7d4c0cd53ad7c4cc49e67471aed41d61f185; - uint64 internal constant ROOT_CA_CERT_NOT_AFTER = 2519044085; - int64 internal constant ROOT_CA_CERT_MAX_PATH_LEN = -1; - bytes32 internal constant ROOT_CA_CERT_SUBJECT_HASH = - 0x3c3e2e5f1dd14dee5db88341ba71521e939afdb7881aa24c9f1e1c007a2fa8b6; - bytes internal constant ROOT_CA_CERT_PUB_KEY = - hex"fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4"; - - bytes32 internal constant CERT_ALGO_OID = 0x53ce037f0dfaa43ef13b095f04e68a6b5e3f1519a01a3203a1e6440ba915b87e; - bytes32 internal constant EC_PUB_KEY_OID = 0xb60fee1fd85f867dd7c8d16884a49a20287ebe4c0fb49294e9825988aa8e42b4; - bytes32 internal constant SECP_384_R1_OID = 0xbd74344bb507daeb9ed315bc535f24a236ccab72c5cd6945fb0efe5c037e2097; - bytes32 internal constant BASIC_CONSTRAINTS_OID = - 0x6351d72a43cb42fb9a2531a28608c278c89629f8f025b5f5dc705f3fe45e950a; - bytes32 internal constant KEY_USAGE_OID = 0x45529d8772b07ebd6d507a1680da791f4a2192882bf89d518801579f7a5167d2; - - IP384Verifier public immutable p384Verifier; - mapping(bytes32 => bytes) private verified; - - constructor(IP384Verifier p384Verifier_) { - require(address(p384Verifier_) != address(0), "missing P384 verifier"); - p384Verifier = p384Verifier_; - _saveVerified( - ROOT_CA_CERT_HASH, - VerifiedCert({ - ca: true, - notAfter: ROOT_CA_CERT_NOT_AFTER, - maxPathLen: ROOT_CA_CERT_MAX_PATH_LEN, - subjectHash: ROOT_CA_CERT_SUBJECT_HASH, - pubKey: ROOT_CA_CERT_PUB_KEY - }) - ); - } - - function verifyCACert(bytes memory, bytes32) external pure returns (bytes32) { - revert("use hinted cert verification"); - } - - function verifyClientCert(bytes memory, bytes32) external pure returns (VerifiedCert memory) { - revert("use hinted cert verification"); - } - - function verifyCACertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) - external - returns (bytes32) - { - bytes32 certHash = keccak256(cert); - _verifyCertWithHints(cert, certHash, true, _loadVerified(parentCertHash), signatureHints); - return certHash; - } - - function verifyClientCertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) - external - returns (VerifiedCert memory) - { - return _verifyCertWithHints(cert, keccak256(cert), false, _loadVerified(parentCertHash), signatureHints); - } - - function loadVerified(bytes32 certHash) external view returns (VerifiedCert memory) { - return _loadVerified(certHash); - } - - function _verifyCertWithHints( - bytes memory certificate, - bytes32 certHash, - bool ca, - VerifiedCert memory parent, - bytes memory signatureHints - ) internal returns (VerifiedCert memory cert) { - if (certHash != ROOT_CA_CERT_HASH) { - require(parent.pubKey.length > 0, "parent cert unverified"); - require(parent.notAfter >= block.timestamp, "parent cert expired"); - require(parent.ca, "parent cert is not a CA"); - require(!ca || parent.maxPathLen != 0, "maxPathLen exceeded"); - } - - cert = _loadVerified(certHash); - if (cert.pubKey.length != 0) { - require(cert.notAfter >= block.timestamp, "cert expired"); - require(cert.ca == ca, "cert is not a CA"); - return cert; - } - - Asn1Ptr root = certificate.root(); - Asn1Ptr tbsCertPtr = certificate.firstChildOf(root); - (uint64 notAfter, int64 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey) = - _parseTbs(certificate, tbsCertPtr, ca); - - require(parent.subjectHash == issuerHash, "issuer / subject mismatch"); - - if (parent.maxPathLen > 0 && (maxPathLen < 0 || maxPathLen >= parent.maxPathLen)) { - maxPathLen = parent.maxPathLen - 1; - } - - _verifyCertSignatureWithHints(certificate, tbsCertPtr, parent.pubKey, signatureHints); - - cert = VerifiedCert({ - ca: ca, notAfter: notAfter, maxPathLen: maxPathLen, subjectHash: subjectHash, pubKey: pubKey - }); - _saveVerified(certHash, cert); - - emit CertVerified(certHash); - } - - function _parseTbs(bytes memory certificate, Asn1Ptr ptr, bool ca) - internal - view - returns (uint64 notAfter, int64 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey) - { - Asn1Ptr versionPtr = certificate.firstChildOf(ptr); - Asn1Ptr vPtr = certificate.firstChildOf(versionPtr); - Asn1Ptr serialPtr = certificate.nextSiblingOf(versionPtr); - Asn1Ptr sigAlgoPtr = certificate.nextSiblingOf(serialPtr); - - require(certificate.keccak(sigAlgoPtr.content(), sigAlgoPtr.length()) == CERT_ALGO_OID, "invalid cert sig algo"); - require(certificate.uintAt(vPtr) == 2, "version should be 3"); - - (notAfter, maxPathLen, issuerHash, subjectHash, pubKey) = _parseTbsInner(certificate, sigAlgoPtr, ca); - } - - function _parseTbsInner(bytes memory certificate, Asn1Ptr sigAlgoPtr, bool ca) - internal - view - returns (uint64 notAfter, int64 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey) - { - Asn1Ptr issuerPtr = certificate.nextSiblingOf(sigAlgoPtr); - issuerHash = certificate.keccak(issuerPtr.content(), issuerPtr.length()); - Asn1Ptr validityPtr = certificate.nextSiblingOf(issuerPtr); - Asn1Ptr subjectPtr = certificate.nextSiblingOf(validityPtr); - subjectHash = certificate.keccak(subjectPtr.content(), subjectPtr.length()); - Asn1Ptr subjectPublicKeyInfoPtr = certificate.nextSiblingOf(subjectPtr); - Asn1Ptr extensionsPtr = certificate.nextSiblingOf(subjectPublicKeyInfoPtr); - - if (certificate[extensionsPtr.header()] == 0x81) { - extensionsPtr = certificate.nextSiblingOf(extensionsPtr); - } - if (certificate[extensionsPtr.header()] == 0x82) { - extensionsPtr = certificate.nextSiblingOf(extensionsPtr); - } - - notAfter = _verifyValidity(certificate, validityPtr); - maxPathLen = _verifyExtensions(certificate, extensionsPtr, ca); - pubKey = _parsePubKey(certificate, subjectPublicKeyInfoPtr); - } - - function _parsePubKey(bytes memory certificate, Asn1Ptr subjectPublicKeyInfoPtr) - internal - pure - returns (bytes memory subjectPubKey) - { - Asn1Ptr pubKeyAlgoPtr = certificate.firstChildOf(subjectPublicKeyInfoPtr); - Asn1Ptr pubKeyAlgoIdPtr = certificate.firstChildOf(pubKeyAlgoPtr); - Asn1Ptr algoParamsPtr = certificate.nextSiblingOf(pubKeyAlgoIdPtr); - Asn1Ptr subjectPublicKeyPtr = certificate.nextSiblingOf(pubKeyAlgoPtr); - Asn1Ptr subjectPubKeyPtr = certificate.bitstring(subjectPublicKeyPtr); - - require( - certificate.keccak(pubKeyAlgoIdPtr.content(), pubKeyAlgoIdPtr.length()) == EC_PUB_KEY_OID, - "invalid cert algo id" - ); - require( - certificate.keccak(algoParamsPtr.content(), algoParamsPtr.length()) == SECP_384_R1_OID, - "invalid cert algo param" - ); - - uint256 end = subjectPubKeyPtr.content() + subjectPubKeyPtr.length(); - subjectPubKey = certificate.slice(end - 96, 96); - } - - function _verifyValidity(bytes memory certificate, Asn1Ptr validityPtr) internal view returns (uint64 notAfter) { - Asn1Ptr notBeforePtr = certificate.firstChildOf(validityPtr); - Asn1Ptr notAfterPtr = certificate.nextSiblingOf(notBeforePtr); - - uint256 notBefore = certificate.timestampAt(notBeforePtr); - notAfter = uint64(certificate.timestampAt(notAfterPtr)); - - require(notBefore <= block.timestamp, "certificate not valid yet"); - require(notAfter >= block.timestamp, "certificate not valid anymore"); - } - - function _verifyExtensions(bytes memory certificate, Asn1Ptr extensionsPtr, bool ca) - internal - pure - returns (int64 maxPathLen) - { - require(certificate[extensionsPtr.header()] == 0xa3, "invalid extensions"); - extensionsPtr = certificate.firstChildOf(extensionsPtr); - Asn1Ptr extensionPtr = certificate.firstChildOf(extensionsPtr); - uint256 end = extensionsPtr.content() + extensionsPtr.length(); - bool basicConstraintsFound = false; - bool keyUsageFound = false; - maxPathLen = -1; - - while (true) { - Asn1Ptr oidPtr = certificate.firstChildOf(extensionPtr); - bytes32 oid = certificate.keccak(oidPtr.content(), oidPtr.length()); - - if (oid == BASIC_CONSTRAINTS_OID || oid == KEY_USAGE_OID) { - Asn1Ptr valuePtr = certificate.nextSiblingOf(oidPtr); - - if (certificate[valuePtr.header()] == 0x01) { - require(valuePtr.length() == 1, "invalid critical bool value"); - valuePtr = certificate.nextSiblingOf(valuePtr); - } - - valuePtr = certificate.octetString(valuePtr); - - if (oid == BASIC_CONSTRAINTS_OID) { - basicConstraintsFound = true; - maxPathLen = _verifyBasicConstraintsExtension(certificate, valuePtr, ca); - } else { - keyUsageFound = true; - _verifyKeyUsageExtension(certificate, valuePtr, ca); - } - } - - if (extensionPtr.content() + extensionPtr.length() == end) { - break; - } - extensionPtr = certificate.nextSiblingOf(extensionPtr); - } - - require(basicConstraintsFound, "basicConstraints not found"); - require(keyUsageFound, "keyUsage not found"); - require(ca || maxPathLen == -1, "maxPathLen must be undefined for client cert"); - } - - function _verifyBasicConstraintsExtension(bytes memory certificate, Asn1Ptr valuePtr, bool ca) - internal - pure - returns (int64 maxPathLen) - { - maxPathLen = -1; - Asn1Ptr basicConstraintsPtr = certificate.firstChildOf(valuePtr); - bool isCA; - if (certificate[basicConstraintsPtr.header()] == 0x01) { - require(basicConstraintsPtr.length() == 1, "invalid isCA bool value"); - isCA = certificate[basicConstraintsPtr.content()] == 0xff; - basicConstraintsPtr = certificate.nextSiblingOf(basicConstraintsPtr); - } - require(ca == isCA, "isCA must be true for CA certs"); - if (certificate[basicConstraintsPtr.header()] == 0x02) { - maxPathLen = int64(uint64(certificate.uintAt(basicConstraintsPtr))); - } - } - - function _verifyKeyUsageExtension(bytes memory certificate, Asn1Ptr valuePtr, bool ca) internal pure { - uint256 value = certificate.bitstringUintAt(valuePtr); - if (ca) { - require(value & 0x04 == 0x04, "CertSign must be present"); - } else { - require(value & 0x80 == 0x80, "DigitalSignature must be present"); - } - } - - function _verifyCertSignatureWithHints( - bytes memory certificate, - Asn1Ptr ptr, - bytes memory pubKey, - bytes memory signatureHints - ) internal { - Asn1Ptr sigAlgoPtr = certificate.nextSiblingOf(ptr); - require(certificate.keccak(sigAlgoPtr.content(), sigAlgoPtr.length()) == CERT_ALGO_OID, "invalid cert sig algo"); - - bytes memory hash = Sha2Ext.sha384(certificate, ptr.header(), ptr.totalLength()); - bytes memory sigPacked = _certSignature(certificate, sigAlgoPtr); - - require(p384Verifier.verifyP384SignatureWithHints(hash, sigPacked, pubKey, signatureHints), "invalid sig"); - } - - function _certSignature(bytes memory certificate, Asn1Ptr sigAlgoPtr) - internal - pure - returns (bytes memory sigPacked) - { - Asn1Ptr sigPtr = certificate.nextSiblingOf(sigAlgoPtr); - Asn1Ptr sigBPtr = certificate.bitstring(sigPtr); - Asn1Ptr sigRoot = certificate.rootOf(sigBPtr); - Asn1Ptr sigRPtr = certificate.firstChildOf(sigRoot); - Asn1Ptr sigSPtr = certificate.nextSiblingOf(sigRPtr); - (uint128 rhi, uint256 rlo) = certificate.uint384At(sigRPtr); - (uint128 shi, uint256 slo) = certificate.uint384At(sigSPtr); - sigPacked = abi.encodePacked(rhi, rlo, shi, slo); - } - - function _saveVerified(bytes32 certHash, VerifiedCert memory cert) internal { - verified[certHash] = abi.encodePacked(cert.ca, cert.notAfter, cert.maxPathLen, cert.subjectHash, cert.pubKey); - } - - function _loadVerified(bytes32 certHash) internal view returns (VerifiedCert memory) { - bytes memory packed = verified[certHash]; - if (packed.length == 0) { - return VerifiedCert({ca: false, notAfter: 0, maxPathLen: 0, subjectHash: 0, pubKey: ""}); - } - uint8 ca; - uint64 notAfter; - int64 maxPathLen; - bytes32 subjectHash; - assembly { - ca := mload(add(packed, 0x1)) - notAfter := mload(add(packed, 0x9)) - maxPathLen := mload(add(packed, 0x11)) - subjectHash := mload(add(packed, 0x31)) - } - bytes memory pubKey = packed.slice(0x31, packed.length - 0x31); - return VerifiedCert({ - ca: ca != 0, notAfter: notAfter, maxPathLen: maxPathLen, subjectHash: subjectHash, pubKey: pubKey - }); - } -} diff --git a/src/ECDSA384Hinted.sol b/src/ECDSA384Hinted.sol deleted file mode 100644 index faf5680..0000000 --- a/src/ECDSA384Hinted.sol +++ /dev/null @@ -1,1047 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import {MemoryUtils} from "@solarity/libs/utils/MemoryUtils.sol"; - -/** - * @notice Cryptography module - * - * This library provides functionality for ECDSA verification over any 384-bit curve. Currently, - * this is the most efficient implementation out there, consuming ~8.025 million gas per call. - * - * The approach is Strauss-Shamir double scalar multiplication with 6 bits of precompute + affine coordinates. - * For reference, naive implementation uses ~400 billion gas, which is 50000 times more expensive. - * - * We also tried using projective coordinates, however, the gas consumption rose to ~9 million gas. - */ -library ECDSA384Hinted { - using MemoryUtils for *; - using U384Hinted for *; - - /** - * @notice 384-bit curve parameters. - */ - struct Parameters { - bytes a; - bytes b; - bytes gx; - bytes gy; - bytes p; - bytes n; - bytes lowSmax; - } - - struct _Parameters { - uint256 a; - uint256 b; - uint256 gx; - uint256 gy; - uint256 p; - uint256 n; - uint256 lowSmax; - } - - struct _Inputs { - uint256 r; - uint256 s; - uint256 x; - uint256 y; - } - - /** - * @notice The function to verify the ECDSA signature - * @param curveParams_ the 384-bit curve parameters. `lowSmax` is `n / 2`. - * @param hashedMessage_ the already hashed message to be verified. - * @param signature_ the ECDSA signature. Equals to `bytes(r) + bytes(s)`. - * @param pubKey_ the full public key of a signer. Equals to `bytes(x) + bytes(y)`. - * - * Note that signatures only from the lower part of the curve are accepted. - * If your `s > n / 2`, change it to `s = n - s`. - */ - function verify( - Parameters memory curveParams_, - bytes memory hashedMessage_, - bytes memory signature_, - bytes memory pubKey_ - ) internal returns (bool) { - (bool ok_,) = _verify(curveParams_, hashedMessage_, signature_, pubKey_, "", false); - return ok_; - } - - function verifyWithHints( - Parameters memory curveParams_, - bytes memory hashedMessage_, - bytes memory signature_, - bytes memory pubKey_, - bytes memory inverseHints_ - ) internal returns (bool ok_) { - uint256 consumed_; - (ok_, consumed_) = _verify(curveParams_, hashedMessage_, signature_, pubKey_, inverseHints_, true); - require(consumed_ == inverseHints_.length, "unused inverse hints"); - } - - function verifyWithHintsConsumed( - Parameters memory curveParams_, - bytes memory hashedMessage_, - bytes memory signature_, - bytes memory pubKey_, - bytes memory inverseHints_ - ) internal returns (bool ok_, uint256 consumed_) { - return _verify(curveParams_, hashedMessage_, signature_, pubKey_, inverseHints_, true); - } - - function _verify( - Parameters memory curveParams_, - bytes memory hashedMessage_, - bytes memory signature_, - bytes memory pubKey_, - bytes memory inverseHints_, - bool useHints_ - ) private returns (bool ok_, uint256 consumed_) { - unchecked { - _Inputs memory inputs_; - - (inputs_.r, inputs_.s) = U384Hinted.init2(signature_); - (inputs_.x, inputs_.y) = U384Hinted.init2(pubKey_); - - _Parameters memory params_ = _Parameters({ - a: curveParams_.a.init(), - b: curveParams_.b.init(), - gx: curveParams_.gx.init(), - gy: curveParams_.gy.init(), - p: curveParams_.p.init(), - n: curveParams_.n.init(), - lowSmax: curveParams_.lowSmax.init() - }); - - uint256 call = - useHints_ ? U384Hinted.initCallWithHints(params_.p, inverseHints_) : U384Hinted.initCall(params_.p); - - /// accept s only from the lower part of the curve - if ( - U384Hinted.eqInteger(inputs_.r, 0) || U384Hinted.cmp(inputs_.r, params_.n) >= 0 - || U384Hinted.eqInteger(inputs_.s, 0) || U384Hinted.cmp(inputs_.s, params_.lowSmax) > 0 - ) { - return (false, U384Hinted.hintCursor(call)); - } - - if (!_isOnCurve(call, params_.p, params_.a, params_.b, inputs_.x, inputs_.y)) { - return (false, U384Hinted.hintCursor(call)); - } - - /// allow compatibility with non-384-bit hash functions. - { - uint256 hashedMessageLength_ = hashedMessage_.length; - - if (hashedMessageLength_ < 48) { - bytes memory tmp_ = new bytes(48); - - MemoryUtils.unsafeCopy( - hashedMessage_.getDataPointer(), - tmp_.getDataPointer() + 48 - hashedMessageLength_, - hashedMessageLength_ - ); - - hashedMessage_ = tmp_; - } - } - - uint256 scalar1 = U384Hinted.moddiv(call, hashedMessage_.init(), inputs_.s, params_.n); - uint256 scalar2 = U384Hinted.moddiv(call, inputs_.r, inputs_.s, params_.n); - - { - uint256 three = U384Hinted.init(3); - - /// We use 6-bit masks where the first 3 bits refer to `scalar1` and the last 3 bits refer to `scalar2`. - uint256[2][64] memory points_ = _precomputePointsTable( - call, params_.p, three, params_.a, params_.gx, params_.gy, inputs_.x, inputs_.y - ); - - (scalar1,) = _doubleScalarMultiplication(call, params_.p, three, params_.a, points_, scalar1, scalar2); - } - - U384Hinted.modAssign(call, scalar1, params_.n); - - return (U384Hinted.eq(scalar1, inputs_.r), U384Hinted.hintCursor(call)); - } - } - - /** - * @dev Check if a point in affine coordinates is on the curve. - */ - function _isOnCurve(uint256 call, uint256 p, uint256 a, uint256 b, uint256 x, uint256 y) private returns (bool) { - unchecked { - if (U384Hinted.eqInteger(x, 0) || U384Hinted.eq(x, p) || U384Hinted.eqInteger(y, 0) || U384Hinted.eq(y, p)) - { - return false; - } - - uint256 LHS = U384Hinted.modexp(call, y, 2); - uint256 RHS = U384Hinted.modexp(call, x, 3); - - if (!U384Hinted.eqInteger(a, 0)) { - RHS = U384Hinted.modadd(RHS, U384Hinted.modmul(call, x, a), p); // x^3 + a*x - } - - if (!U384Hinted.eqInteger(b, 0)) { - RHS = U384Hinted.modadd(RHS, b, p); // x^3 + a*x + b - } - - return U384Hinted.eq(LHS, RHS); - } - } - - /** - * @dev Compute the Strauss-Shamir double scalar multiplication scalar1*G + scalar2*H. - */ - function _doubleScalarMultiplication( - uint256 call, - uint256 p, - uint256 three, - uint256 a, - uint256[2][64] memory points, - uint256 scalar1, - uint256 scalar2 - ) private returns (uint256 x, uint256 y) { - unchecked { - uint256 mask_; - uint256 scalar1Bits_; - uint256 scalar2Bits_; - - assembly { - scalar1Bits_ := mload(scalar1) - scalar2Bits_ := mload(scalar2) - } - - (x, y) = _twiceAffine(call, p, three, a, x, y); - - mask_ = ((scalar1Bits_ >> 183) << 3) | (scalar2Bits_ >> 183); - - if (mask_ != 0) { - (x, y) = _addAffine(call, p, three, a, points[mask_][0], points[mask_][1], x, y); - } - - for (uint256 word = 4; word <= 184; word += 3) { - (x, y) = _twice3Affine(call, p, three, a, x, y); - - mask_ = (((scalar1Bits_ >> (184 - word)) & 0x07) << 3) | ((scalar2Bits_ >> (184 - word)) & 0x07); - - if (mask_ != 0) { - (x, y) = _addAffine(call, p, three, a, points[mask_][0], points[mask_][1], x, y); - } - } - - assembly { - scalar1Bits_ := mload(add(scalar1, 0x20)) - scalar2Bits_ := mload(add(scalar2, 0x20)) - } - - (x, y) = _twiceAffine(call, p, three, a, x, y); - - mask_ = ((scalar1Bits_ >> 255) << 3) | (scalar2Bits_ >> 255); - - if (mask_ != 0) { - (x, y) = _addAffine(call, p, three, a, points[mask_][0], points[mask_][1], x, y); - } - - for (uint256 word = 4; word <= 256; word += 3) { - (x, y) = _twice3Affine(call, p, three, a, x, y); - - mask_ = (((scalar1Bits_ >> (256 - word)) & 0x07) << 3) | ((scalar2Bits_ >> (256 - word)) & 0x07); - - if (mask_ != 0) { - (x, y) = _addAffine(call, p, three, a, points[mask_][0], points[mask_][1], x, y); - } - } - } - } - - /** - * @dev Double an elliptic curve point in affine coordinates. - */ - function _twiceAffine(uint256 call, uint256 p, uint256 three, uint256 a, uint256 x1, uint256 y1) - private - returns (uint256 x2, uint256 y2) - { - unchecked { - if (x1 == 0) { - return (0, 0); - } - - if (U384Hinted.eqInteger(y1, 0)) { - return (0, 0); - } - - uint256 m1 = U384Hinted.modexp(call, x1, 2); - U384Hinted.modmulAssign(call, m1, three); - U384Hinted.modaddAssign(m1, a, p); - - uint256 m2 = U384Hinted.modshl1(y1, p); - U384Hinted.moddivAssign(call, m1, m2); - - x2 = U384Hinted.modexp(call, m1, 2); - U384Hinted.modsubAssign(x2, x1, p); - U384Hinted.modsubAssign(x2, x1, p); - - y2 = U384Hinted.modsub(x1, x2, p); - U384Hinted.modmulAssign(call, y2, m1); - U384Hinted.modsubAssign(y2, y1, p); - } - } - - /** - * @dev Doubles an elliptic curve point 3 times in affine coordinates. - */ - function _twice3Affine(uint256 call, uint256 p, uint256 three, uint256 a, uint256 x1, uint256 y1) - private - returns (uint256 x2, uint256 y2) - { - unchecked { - if (x1 == 0) { - return (0, 0); - } - - if (U384Hinted.eqInteger(y1, 0)) { - return (0, 0); - } - - uint256 m1 = U384Hinted.modexp(call, x1, 2); - U384Hinted.modmulAssign(call, m1, three); - U384Hinted.modaddAssign(m1, a, p); - - uint256 m2 = U384Hinted.modshl1(y1, p); - U384Hinted.moddivAssign(call, m1, m2); - - x2 = U384Hinted.modexp(call, m1, 2); - U384Hinted.modsubAssign(x2, x1, p); - U384Hinted.modsubAssign(x2, x1, p); - - y2 = U384Hinted.modsub(x1, x2, p); - U384Hinted.modmulAssign(call, y2, m1); - U384Hinted.modsubAssign(y2, y1, p); - - if (U384Hinted.eqInteger(y2, 0)) { - return (0, 0); - } - - U384Hinted.modexpAssignTo(call, m1, x2, 2); - U384Hinted.modmulAssign(call, m1, three); - U384Hinted.modaddAssign(m1, a, p); - - U384Hinted.modshl1AssignTo(m2, y2, p); - U384Hinted.moddivAssign(call, m1, m2); - - U384Hinted.modexpAssignTo(call, x1, m1, 2); - U384Hinted.modsubAssign(x1, x2, p); - U384Hinted.modsubAssign(x1, x2, p); - - U384Hinted.modsubAssignTo(y1, x2, x1, p); - U384Hinted.modmulAssign(call, y1, m1); - U384Hinted.modsubAssign(y1, y2, p); - - if (U384Hinted.eqInteger(y1, 0)) { - return (0, 0); - } - - U384Hinted.modexpAssignTo(call, m1, x1, 2); - U384Hinted.modmulAssign(call, m1, three); - U384Hinted.modaddAssign(m1, a, p); - - U384Hinted.modshl1AssignTo(m2, y1, p); - U384Hinted.moddivAssign(call, m1, m2); - - U384Hinted.modexpAssignTo(call, x2, m1, 2); - U384Hinted.modsubAssign(x2, x1, p); - U384Hinted.modsubAssign(x2, x1, p); - - U384Hinted.modsubAssignTo(y2, x1, x2, p); - U384Hinted.modmulAssign(call, y2, m1); - U384Hinted.modsubAssign(y2, y1, p); - } - } - - /** - * @dev Add two elliptic curve points in affine coordinates. - */ - function _addAffine( - uint256 call, - uint256 p, - uint256 three, - uint256 a, - uint256 x1, - uint256 y1, - uint256 x2, - uint256 y2 - ) private returns (uint256 x3, uint256 y3) { - unchecked { - if (x1 == 0 || x2 == 0) { - if (x1 == 0 && x2 == 0) { - return (0, 0); - } - - return x1 == 0 ? (x2.copy(), y2.copy()) : (x1.copy(), y1.copy()); - } - - if (U384Hinted.eq(x1, x2)) { - if (U384Hinted.eq(y1, y2)) { - return _twiceAffine(call, p, three, a, x1, y1); - } - - return (0, 0); - } - - uint256 m1 = U384Hinted.modsub(y1, y2, p); - uint256 m2 = U384Hinted.modsub(x1, x2, p); - - U384Hinted.moddivAssign(call, m1, m2); - - x3 = U384Hinted.modexp(call, m1, 2); - U384Hinted.modsubAssign(x3, x1, p); - U384Hinted.modsubAssign(x3, x2, p); - - y3 = U384Hinted.modsub(x1, x3, p); - U384Hinted.modmulAssign(call, y3, m1); - U384Hinted.modsubAssign(y3, y1, p); - } - } - - function _precomputePointsTable( - uint256 call, - uint256 p, - uint256 three, - uint256 a, - uint256 gx, - uint256 gy, - uint256 hx, - uint256 hy - ) private returns (uint256[2][64] memory points_) { - unchecked { - (points_[0x01][0], points_[0x01][1]) = (hx.copy(), hy.copy()); - (points_[0x08][0], points_[0x08][1]) = (gx.copy(), gy.copy()); - - for (uint256 i = 0; i < 8; ++i) { - for (uint256 j = 0; j < 8; ++j) { - if (i + j < 2) { - continue; - } - - uint256 maskTo = (i << 3) | j; - - if (i != 0) { - uint256 maskFrom = ((i - 1) << 3) | j; - - (points_[maskTo][0], points_[maskTo][1]) = - _addAffine(call, p, three, a, points_[maskFrom][0], points_[maskFrom][1], gx, gy); - } else { - uint256 maskFrom = (i << 3) | (j - 1); - - (points_[maskTo][0], points_[maskTo][1]) = - _addAffine(call, p, three, a, points_[maskFrom][0], points_[maskFrom][1], hx, hy); - } - } - } - } - } -} - -/** - * @notice Low-level utility library that implements unsigned 384-bit arithmetics. - * - * Should not be used outside of this file. - */ -library U384Hinted { - uint256 private constant SHORT_ALLOCATION = 64; - - uint256 private constant MUL_OFFSET = 288; - uint256 private constant EXP_OFFSET = 2 * 288; - uint256 private constant INV_OFFSET = 3 * 288; - uint256 private constant HINT_DATA_OFFSET = 0x480; - uint256 private constant HINT_LENGTH_OFFSET = 0x4A0; - uint256 private constant HINT_CURSOR_OFFSET = 0x4C0; - uint256 private constant HINT_ENABLED_OFFSET = 0x4E0; - uint256 private constant CALL_ALLOCATION = 0x500; - - function init(uint256 from_) internal returns (uint256 handler_) { - unchecked { - handler_ = _allocate(SHORT_ALLOCATION); - - assembly { - mstore(handler_, 0x00) - mstore(add(0x20, handler_), from_) - } - - return handler_; - } - } - - function init(bytes memory from_) internal returns (uint256 handler_) { - unchecked { - require(from_.length == 48, "U384: not 384"); - - handler_ = _allocate(SHORT_ALLOCATION); - - assembly { - mstore(handler_, 0x00) - mstore(add(handler_, 0x10), mload(add(from_, 0x20))) - mstore(add(handler_, 0x20), mload(add(from_, 0x30))) - } - - return handler_; - } - } - - function init2(bytes memory from2_) internal returns (uint256 handler1_, uint256 handler2_) { - unchecked { - require(from2_.length == 96, "U384: not 768"); - - handler1_ = _allocate(SHORT_ALLOCATION); - handler2_ = _allocate(SHORT_ALLOCATION); - - assembly { - mstore(handler1_, 0x00) - mstore(add(handler1_, 0x10), mload(add(from2_, 0x20))) - mstore(add(handler1_, 0x20), mload(add(from2_, 0x30))) - - mstore(handler2_, 0x00) - mstore(add(handler2_, 0x10), mload(add(from2_, 0x50))) - mstore(add(handler2_, 0x20), mload(add(from2_, 0x60))) - } - - return (handler1_, handler2_); - } - } - - function initCall(uint256 m_) internal returns (uint256 handler_) { - unchecked { - handler_ = _allocate(CALL_ALLOCATION); - - _sub(m_, init(2), handler_ + INV_OFFSET + 0xA0); - - assembly { - let call_ := add(handler_, MUL_OFFSET) - - mstore(call_, 0x60) - mstore(add(0x20, call_), 0x20) - mstore(add(0x40, call_), 0x40) - mstore(add(0xC0, call_), 0x01) - mstore(add(0xE0, call_), mload(m_)) - mstore(add(0x0100, call_), mload(add(m_, 0x20))) - - call_ := add(handler_, EXP_OFFSET) - - mstore(call_, 0x40) - mstore(add(0x20, call_), 0x20) - mstore(add(0x40, call_), 0x40) - mstore(add(0xC0, call_), mload(m_)) - mstore(add(0xE0, call_), mload(add(m_, 0x20))) - - call_ := add(handler_, INV_OFFSET) - - mstore(call_, 0x40) - mstore(add(0x20, call_), 0x40) - mstore(add(0x40, call_), 0x40) - mstore(add(0xE0, call_), mload(m_)) - mstore(add(0x0100, call_), mload(add(m_, 0x20))) - - mstore(add(handler_, HINT_DATA_OFFSET), 0) - mstore(add(handler_, HINT_LENGTH_OFFSET), 0) - mstore(add(handler_, HINT_CURSOR_OFFSET), 0) - mstore(add(handler_, HINT_ENABLED_OFFSET), 0) - } - } - } - - function initCallWithHints(uint256 m_, bytes memory inverseHints_) internal returns (uint256 handler_) { - handler_ = initCall(m_); - - assembly { - mstore(add(handler_, HINT_DATA_OFFSET), add(inverseHints_, 0x20)) - mstore(add(handler_, HINT_LENGTH_OFFSET), mload(inverseHints_)) - mstore(add(handler_, HINT_CURSOR_OFFSET), 0) - mstore(add(handler_, HINT_ENABLED_OFFSET), 1) - } - } - - function hintCursor(uint256 call_) internal returns (uint256 cursor_) { - assembly { - cursor_ := mload(add(call_, HINT_CURSOR_OFFSET)) - } - } - - function copy(uint256 handler_) internal returns (uint256 handlerCopy_) { - unchecked { - handlerCopy_ = _allocate(SHORT_ALLOCATION); - - assembly { - mstore(handlerCopy_, mload(handler_)) - mstore(add(handlerCopy_, 0x20), mload(add(handler_, 0x20))) - } - - return handlerCopy_; - } - } - - function eq(uint256 a_, uint256 b_) internal returns (bool eq_) { - assembly { - eq_ := and(eq(mload(a_), mload(b_)), eq(mload(add(a_, 0x20)), mload(add(b_, 0x20)))) - } - } - - function eqInteger(uint256 a_, uint256 bInteger_) internal returns (bool eq_) { - assembly { - eq_ := and(eq(mload(a_), 0), eq(mload(add(a_, 0x20)), bInteger_)) - } - } - - function cmp(uint256 a_, uint256 b_) internal returns (int256 cmp_) { - unchecked { - uint256 aWord_; - uint256 bWord_; - - assembly { - aWord_ := mload(a_) - bWord_ := mload(b_) - } - - if (aWord_ > bWord_) { - return 1; - } - - if (aWord_ < bWord_) { - return -1; - } - - assembly { - aWord_ := mload(add(a_, 0x20)) - bWord_ := mload(add(b_, 0x20)) - } - - if (aWord_ > bWord_) { - return 1; - } - - if (aWord_ < bWord_) { - return -1; - } - } - } - - function modAssign(uint256 call_, uint256 a_, uint256 m_) internal { - assembly { - mstore(call_, 0x40) - mstore(add(0x20, call_), 0x20) - mstore(add(0x40, call_), 0x40) - mstore(add(0x60, call_), mload(a_)) - mstore(add(0x80, call_), mload(add(a_, 0x20))) - mstore(add(0xA0, call_), 0x01) - mstore(add(0xC0, call_), mload(m_)) - mstore(add(0xE0, call_), mload(add(m_, 0x20))) - - pop(staticcall(gas(), 0x5, call_, 0x0100, a_, 0x40)) - } - } - - function modexp(uint256 call_, uint256 b_, uint256 eInteger_) internal returns (uint256 r_) { - unchecked { - r_ = _allocate(SHORT_ALLOCATION); - - assembly { - call_ := add(call_, EXP_OFFSET) - - mstore(add(0x60, call_), mload(b_)) - mstore(add(0x80, call_), mload(add(b_, 0x20))) - mstore(add(0xA0, call_), eInteger_) - - pop(staticcall(gas(), 0x5, call_, 0x0100, r_, 0x40)) - } - - return r_; - } - } - - function modexpAssignTo(uint256 call_, uint256 to_, uint256 b_, uint256 eInteger_) internal { - assembly { - call_ := add(call_, EXP_OFFSET) - - mstore(add(0x60, call_), mload(b_)) - mstore(add(0x80, call_), mload(add(b_, 0x20))) - mstore(add(0xA0, call_), eInteger_) - - pop(staticcall(gas(), 0x5, call_, 0x0100, to_, 0x40)) - } - } - - function modadd(uint256 a_, uint256 b_, uint256 m_) internal returns (uint256 r_) { - unchecked { - r_ = _allocate(SHORT_ALLOCATION); - - _add(a_, b_, r_); - - if (cmp(r_, m_) >= 0) { - _subFrom(r_, m_); - } - - return r_; - } - } - - function modaddAssign(uint256 a_, uint256 b_, uint256 m_) internal { - unchecked { - _addTo(a_, b_); - - if (cmp(a_, m_) >= 0) { - return _subFrom(a_, m_); - } - } - } - - function modmul(uint256 call_, uint256 a_, uint256 b_) internal returns (uint256 r_) { - unchecked { - r_ = _allocate(SHORT_ALLOCATION); - - _mul(a_, b_, call_ + MUL_OFFSET + 0x60); - - assembly { - call_ := add(call_, MUL_OFFSET) - - pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) - } - - return r_; - } - } - - function modmulAssign(uint256 call_, uint256 a_, uint256 b_) internal { - unchecked { - _mul(a_, b_, call_ + MUL_OFFSET + 0x60); - - assembly { - call_ := add(call_, MUL_OFFSET) - - pop(staticcall(gas(), 0x5, call_, 0x0120, a_, 0x40)) - } - } - } - - function modsub(uint256 a_, uint256 b_, uint256 m_) internal returns (uint256 r_) { - unchecked { - r_ = _allocate(SHORT_ALLOCATION); - - if (cmp(a_, b_) >= 0) { - _sub(a_, b_, r_); - return r_; - } - - _add(a_, m_, r_); - _subFrom(r_, b_); - } - } - - function modsubAssign(uint256 a_, uint256 b_, uint256 m_) internal { - unchecked { - if (cmp(a_, b_) >= 0) { - _subFrom(a_, b_); - return; - } - - _addTo(a_, m_); - _subFrom(a_, b_); - } - } - - function modsubAssignTo(uint256 to_, uint256 a_, uint256 b_, uint256 m_) internal { - unchecked { - if (cmp(a_, b_) >= 0) { - _sub(a_, b_, to_); - return; - } - - _add(a_, m_, to_); - _subFrom(to_, b_); - } - } - - function modshl1(uint256 a_, uint256 m_) internal returns (uint256 r_) { - unchecked { - r_ = _allocate(SHORT_ALLOCATION); - - _shl1(a_, r_); - - if (cmp(r_, m_) >= 0) { - _subFrom(r_, m_); - } - - return r_; - } - } - - function modshl1AssignTo(uint256 to_, uint256 a_, uint256 m_) internal { - unchecked { - _shl1(a_, to_); - - if (cmp(to_, m_) >= 0) { - _subFrom(to_, m_); - } - } - } - - /// @dev Stores modinv into `b_` and moddiv into `a_`. - function moddivAssign(uint256 call_, uint256 a_, uint256 b_) internal { - unchecked { - uint256 baseCall_ = call_; - if (_hintsEnabled(call_)) { - uint256 inv_ = _nextInverseHint(call_); - uint256 check_ = modmul(call_, b_, inv_); - require(eqInteger(check_, 1), "bad inverse hint"); - - assembly { - mstore(b_, mload(inv_)) - mstore(add(b_, 0x20), mload(add(inv_, 0x20))) - } - } else { - assembly { - call_ := add(call_, INV_OFFSET) - - mstore(add(0x60, call_), mload(b_)) - mstore(add(0x80, call_), mload(add(b_, 0x20))) - - pop(staticcall(gas(), 0x5, call_, 0x0120, b_, 0x40)) - } - } - - modmulAssign(baseCall_, a_, b_); - } - } - - function moddiv(uint256 call_, uint256 a_, uint256 b_, uint256 m_) internal returns (uint256 r_) { - unchecked { - r_ = modinv(call_, b_, m_); - - _mul(a_, r_, call_ + 0x60); - - assembly { - mstore(call_, 0x60) - mstore(add(0x20, call_), 0x20) - mstore(add(0x40, call_), 0x40) - mstore(add(0xC0, call_), 0x01) - mstore(add(0xE0, call_), mload(m_)) - mstore(add(0x0100, call_), mload(add(m_, 0x20))) - - pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) - } - } - } - - function modinv(uint256 call_, uint256 b_, uint256 m_) internal returns (uint256 r_) { - unchecked { - if (_hintsEnabled(call_)) { - r_ = _nextInverseHint(call_); - uint256 check_ = _modmulWithMod(call_, b_, r_, m_); - require(eqInteger(check_, 1), "bad inverse hint"); - return r_; - } - - r_ = _allocate(SHORT_ALLOCATION); - - _sub(m_, init(2), call_ + 0xA0); - - assembly { - mstore(call_, 0x40) - mstore(add(0x20, call_), 0x40) - mstore(add(0x40, call_), 0x40) - mstore(add(0x60, call_), mload(b_)) - mstore(add(0x80, call_), mload(add(b_, 0x20))) - mstore(add(0xE0, call_), mload(m_)) - mstore(add(0x0100, call_), mload(add(m_, 0x20))) - - pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) - } - } - } - - function _hintsEnabled(uint256 call_) private returns (bool enabled_) { - assembly { - enabled_ := mload(add(call_, HINT_ENABLED_OFFSET)) - } - } - - function _nextInverseHint(uint256 call_) private returns (uint256 r_) { - uint256 cursor_; - uint256 length_; - assembly { - cursor_ := mload(add(call_, HINT_CURSOR_OFFSET)) - length_ := mload(add(call_, HINT_LENGTH_OFFSET)) - } - require(cursor_ + 48 <= length_, "inverse hint underflow"); - - r_ = _allocate(SHORT_ALLOCATION); - - assembly { - let src_ := add(mload(add(call_, HINT_DATA_OFFSET)), cursor_) - mstore(r_, shr(128, mload(src_))) - mstore(add(r_, 0x20), mload(add(src_, 0x10))) - mstore(add(call_, HINT_CURSOR_OFFSET), add(cursor_, 48)) - } - } - - function _modmulWithMod(uint256 call_, uint256 a_, uint256 b_, uint256 m_) private returns (uint256 r_) { - unchecked { - r_ = _allocate(SHORT_ALLOCATION); - _mul(a_, b_, call_ + 0x60); - - assembly { - mstore(call_, 0x60) - mstore(add(0x20, call_), 0x20) - mstore(add(0x40, call_), 0x40) - mstore(add(0xC0, call_), 0x01) - mstore(add(0xE0, call_), mload(m_)) - mstore(add(0x0100, call_), mload(add(m_, 0x20))) - - pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) - } - } - } - - function _shl1(uint256 a_, uint256 r_) internal { - assembly { - let a1_ := mload(add(a_, 0x20)) - - mstore(r_, or(shl(1, mload(a_)), shr(255, a1_))) - mstore(add(r_, 0x20), shl(1, a1_)) - } - } - - function _add(uint256 a_, uint256 b_, uint256 r_) private { - assembly { - let aWord_ := mload(add(a_, 0x20)) - let sum_ := add(aWord_, mload(add(b_, 0x20))) - - mstore(add(r_, 0x20), sum_) - - sum_ := gt(aWord_, sum_) - sum_ := add(sum_, add(mload(a_), mload(b_))) - - mstore(r_, sum_) - } - } - - function _sub(uint256 a_, uint256 b_, uint256 r_) private { - assembly { - let aWord_ := mload(add(a_, 0x20)) - let diff_ := sub(aWord_, mload(add(b_, 0x20))) - - mstore(add(r_, 0x20), diff_) - - diff_ := gt(diff_, aWord_) - diff_ := sub(sub(mload(a_), mload(b_)), diff_) - - mstore(r_, diff_) - } - } - - function _subFrom(uint256 a_, uint256 b_) private { - assembly { - let aWord_ := mload(add(a_, 0x20)) - let diff_ := sub(aWord_, mload(add(b_, 0x20))) - - mstore(add(a_, 0x20), diff_) - - diff_ := gt(diff_, aWord_) - diff_ := sub(sub(mload(a_), mload(b_)), diff_) - - mstore(a_, diff_) - } - } - - function _addTo(uint256 a_, uint256 b_) private { - assembly { - let aWord_ := mload(add(a_, 0x20)) - let sum_ := add(aWord_, mload(add(b_, 0x20))) - - mstore(add(a_, 0x20), sum_) - - sum_ := gt(aWord_, sum_) - sum_ := add(sum_, add(mload(a_), mload(b_))) - - mstore(a_, sum_) - } - } - - function _mul(uint256 a_, uint256 b_, uint256 r_) private { - assembly { - let a0_ := mload(a_) - let a1_ := shr(128, mload(add(a_, 0x20))) - let a2_ := and(mload(add(a_, 0x20)), 0xffffffffffffffffffffffffffffffff) - - let b0_ := mload(b_) - let b1_ := shr(128, mload(add(b_, 0x20))) - let b2_ := and(mload(add(b_, 0x20)), 0xffffffffffffffffffffffffffffffff) - - // r5 - let current_ := mul(a2_, b2_) - let r0_ := and(current_, 0xffffffffffffffffffffffffffffffff) - - // r4 - current_ := shr(128, current_) - - let temp_ := mul(a1_, b2_) - current_ := add(current_, temp_) - let curry_ := lt(current_, temp_) - - temp_ := mul(a2_, b1_) - current_ := add(current_, temp_) - curry_ := add(curry_, lt(current_, temp_)) - - mstore(add(r_, 0x40), add(shl(128, current_), r0_)) - - // r3 - current_ := add(shl(128, curry_), shr(128, current_)) - curry_ := 0 - - temp_ := mul(a0_, b2_) - current_ := add(current_, temp_) - curry_ := lt(current_, temp_) - - temp_ := mul(a1_, b1_) - current_ := add(current_, temp_) - curry_ := add(curry_, lt(current_, temp_)) - - temp_ := mul(a2_, b0_) - current_ := add(current_, temp_) - curry_ := add(curry_, lt(current_, temp_)) - - r0_ := and(current_, 0xffffffffffffffffffffffffffffffff) - - // r2 - current_ := add(shl(128, curry_), shr(128, current_)) - curry_ := 0 - - temp_ := mul(a0_, b1_) - current_ := add(current_, temp_) - curry_ := lt(current_, temp_) - - temp_ := mul(a1_, b0_) - current_ := add(current_, temp_) - curry_ := add(curry_, lt(current_, temp_)) - - mstore(add(r_, 0x20), add(shl(128, current_), r0_)) - - // r1 - current_ := add(shl(128, curry_), shr(128, current_)) - current_ := add(current_, mul(a0_, b0_)) - - mstore(r_, current_) - } - } - - function _allocate(uint256 bytes_) private returns (uint256 handler_) { - unchecked { - assembly { - handler_ := mload(0x40) - mstore(0x40, add(handler_, bytes_)) - } - - return handler_; - } - } -} diff --git a/src/ICertManager.sol b/src/ICertManager.sol index 04927f6..bfef14e 100644 --- a/src/ICertManager.sol +++ b/src/ICertManager.sol @@ -13,4 +13,14 @@ interface ICertManager { function verifyCACert(bytes memory cert, bytes32 parentCertHash) external returns (bytes32); function verifyClientCert(bytes memory cert, bytes32 parentCertHash) external returns (VerifiedCert memory); + + function verifyCACertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) + external + returns (bytes32); + + function verifyClientCertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) + external + returns (VerifiedCert memory); + + function loadVerified(bytes32 certHash) external view returns (VerifiedCert memory); } diff --git a/src/IHintedCertManager.sol b/src/IHintedCertManager.sol deleted file mode 100644 index 3c56827..0000000 --- a/src/IHintedCertManager.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import {ICertManager} from "./ICertManager.sol"; - -interface IHintedCertManager is ICertManager { - function verifyCACertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) - external - returns (bytes32); - - function verifyClientCertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) - external - returns (VerifiedCert memory); - - function loadVerified(bytes32 certHash) external view returns (VerifiedCert memory); -} diff --git a/src/IP384Verifier.sol b/src/IP384Verifier.sol index 068341a..1512877 100644 --- a/src/IP384Verifier.sol +++ b/src/IP384Verifier.sol @@ -7,5 +7,5 @@ interface IP384Verifier { bytes memory signature, bytes memory pubKey, bytes memory inverseHints - ) external returns (bool); + ) external view returns (bool); } diff --git a/src/NitroValidator.sol b/src/NitroValidator.sol index f5b8f57..0b13ad5 100644 --- a/src/NitroValidator.sol +++ b/src/NitroValidator.sol @@ -4,8 +4,7 @@ pragma solidity ^0.8.15; import {ICertManager} from "./ICertManager.sol"; import {Sha2Ext} from "./Sha2Ext.sol"; import {CborDecode, CborElement, LibCborElement} from "./CborDecode.sol"; -import {ECDSA384} from "@solarity/libs/crypto/ECDSA384.sol"; -import {ECDSA384Curve} from "./ECDSA384Curve.sol"; +import {IP384Verifier} from "./IP384Verifier.sol"; import {LibBytes} from "./LibBytes.sol"; // adapted from https://github.com/marlinprotocol/NitroProver/blob/f1d368d1f172ad3a55cd2aaaa98ad6a6e7dcde9d/src/NitroProver.sol @@ -41,9 +40,13 @@ contract NitroValidator { } ICertManager public immutable certManager; + IP384Verifier public immutable p384Verifier; - constructor(ICertManager _certManager) { + constructor(ICertManager _certManager, IP384Verifier _p384Verifier) { + require(address(_certManager) != address(0), "missing cert manager"); + require(address(_p384Verifier) != address(0), "missing P384 verifier"); certManager = _certManager; + p384Verifier = _p384Verifier; } function decodeAttestationTbs(bytes memory attestation) @@ -70,7 +73,15 @@ contract NitroValidator { signature = attestation.slice(signaturePtr.start(), signaturePtr.length()); } - function validateAttestation(bytes memory attestationTbs, bytes memory signature) public returns (Ptrs memory) { + function validateAttestation(bytes memory, bytes memory) public pure returns (Ptrs memory) { + revert("use hinted attestation verification"); + } + + function validateAttestationWithHints( + bytes memory attestationTbs, + bytes memory signature, + bytes memory attestationSigHints + ) public returns (Ptrs memory) { Ptrs memory ptrs = _parseAttestation(attestationTbs); require(ptrs.moduleID.length() > 0, "no module id"); @@ -98,22 +109,25 @@ contract NitroValidator { cabundle[i] = attestationTbs.slice(ptrs.cabundle[i]); } - ICertManager.VerifiedCert memory parent = verifyCertBundle(cert, cabundle); + ICertManager.VerifiedCert memory parent = verifyCachedCertBundle(cert, cabundle); bytes memory hash = Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length); - _verifySignature(parent.pubKey, hash, signature); + require( + p384Verifier.verifyP384SignatureWithHints(hash, signature, parent.pubKey, attestationSigHints), + "invalid sig" + ); return ptrs; } - function verifyCertBundle(bytes memory certificate, bytes[] memory cabundle) + function verifyCachedCertBundle(bytes memory certificate, bytes[] memory cabundle) internal returns (ICertManager.VerifiedCert memory) { bytes32 parentHash; for (uint256 i = 0; i < cabundle.length; i++) { - parentHash = certManager.verifyCACert(cabundle[i], parentHash); + parentHash = certManager.verifyCACertWithHints(cabundle[i], parentHash, ""); } - return certManager.verifyClientCert(certificate, parentHash); + return certManager.verifyClientCertWithHints(certificate, parentHash, ""); } function _constructAttestationTbs( @@ -203,8 +217,4 @@ contract NitroValidator { return ptrs; } - - function _verifySignature(bytes memory pubKey, bytes memory hash, bytes memory sig) internal view { - require(ECDSA384.verify(ECDSA384Curve.p384(), hash, sig, pubKey), "invalid sig"); - } } diff --git a/src/NitroValidatorHinted.sol b/src/NitroValidatorHinted.sol deleted file mode 100644 index 7d8405d..0000000 --- a/src/NitroValidatorHinted.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import {IHintedCertManager} from "./IHintedCertManager.sol"; -import {IP384Verifier} from "./IP384Verifier.sol"; -import {NitroValidatorHintedExternal} from "./NitroValidatorHintedExternal.sol"; - -/// @notice Deployable hinted Nitro attestation validator. -/// @dev Kept as the canonical hinted name; P384 verification is delegated to `p384Verifier`. -contract NitroValidatorHinted is NitroValidatorHintedExternal { - constructor(IHintedCertManager certManager_, IP384Verifier p384Verifier_) - NitroValidatorHintedExternal(certManager_, p384Verifier_) - {} -} diff --git a/src/NitroValidatorHintedExternal.sol b/src/NitroValidatorHintedExternal.sol deleted file mode 100644 index cdd3664..0000000 --- a/src/NitroValidatorHintedExternal.sol +++ /dev/null @@ -1,217 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import {CborDecode, CborElement, LibCborElement} from "./CborDecode.sol"; -import {ICertManager} from "./ICertManager.sol"; -import {IHintedCertManager} from "./IHintedCertManager.sol"; -import {IP384Verifier} from "./IP384Verifier.sol"; -import {LibBytes} from "./LibBytes.sol"; -import {Sha2Ext} from "./Sha2Ext.sol"; - -/// @notice Hinted-only Nitro attestation validator that requires the cert chain to be cached first. -/// @dev Certificate and attestation P384 signatures are verified through an external verifier contract. -contract NitroValidatorHintedExternal { - using LibBytes for bytes; - using CborDecode for bytes; - using LibCborElement for CborElement; - - bytes32 internal constant ATTESTATION_TBS_PREFIX = - 0x63ce814bd924c1ef12c43686e4cbf48ed1639a78387b0570c23ca921e8ce071c; - bytes32 internal constant ATTESTATION_DIGEST = 0x501a3a7a4e0cf54b03f2488098bdd59bc1c2e8d741a300d6b25926d531733fef; - - bytes32 internal constant CERTIFICATE_KEY = 0x925cec779426f44d8d555e01d2683a3a765ce2fa7562ca7352aeb09dfc57ea6a; - bytes32 internal constant PUBLIC_KEY_KEY = 0xc7b28019ccfdbd30ffc65951d94bb85c9e2b8434111a000b5afd533ce65f57a4; - bytes32 internal constant MODULE_ID_KEY = 0x8ce577cf664c36ba5130242bf5790c2675e9f4e6986a842b607821bee25372ee; - bytes32 internal constant TIMESTAMP_KEY = 0x4ebf727c48eac2c66272456b06a885c5cc03e54d140f63b63b6fd10c1227958e; - bytes32 internal constant USER_DATA_KEY = 0x5e4ea5393e4327b3014bc32f2264336b0d1ee84a4cfd197c8ad7e1e16829a16a; - bytes32 internal constant CABUNDLE_KEY = 0x8a8cb7aa1da17ada103546ae6b4e13ccc2fafa17adf5f93925e0a0a4e5681a6a; - bytes32 internal constant DIGEST_KEY = 0x682a7e258d80bd2421d3103cbe71e3e3b82138116756b97b8256f061dc2f11fb; - bytes32 internal constant NONCE_KEY = 0x7ab1577440dd7bedf920cb6de2f9fc6bf7ba98c78c85a3fa1f8311aac95e1759; - bytes32 internal constant PCRS_KEY = 0x61585f8bc67a4b6d5891a4639a074964ac66fc2241dc0b36c157dc101325367a; - - struct Ptrs { - CborElement moduleID; - uint64 timestamp; - CborElement digest; - CborElement[] pcrs; - CborElement cert; - CborElement[] cabundle; - CborElement publicKey; - CborElement userData; - CborElement nonce; - } - - IHintedCertManager public immutable hintedCertManager; - IP384Verifier public immutable p384Verifier; - - constructor(IHintedCertManager certManager_, IP384Verifier p384Verifier_) { - require(address(certManager_) != address(0), "missing cert manager"); - require(address(p384Verifier_) != address(0), "missing P384 verifier"); - hintedCertManager = certManager_; - p384Verifier = p384Verifier_; - } - - function decodeAttestationTbs(bytes memory attestation) - external - pure - returns (bytes memory attestationTbs, bytes memory signature) - { - uint256 offset = 1; - if (attestation[0] == 0xD2) { - offset = 2; - } - - CborElement protectedPtr = attestation.byteStringAt(offset); - CborElement unprotectedPtr = attestation.nextMap(protectedPtr); - CborElement payloadPtr = attestation.nextByteString(unprotectedPtr); - CborElement signaturePtr = attestation.nextByteString(payloadPtr); - - uint256 rawProtectedLength = protectedPtr.end() - offset; - uint256 rawPayloadLength = payloadPtr.end() - unprotectedPtr.end(); - bytes memory rawProtectedBytes = attestation.slice(offset, rawProtectedLength); - bytes memory rawPayloadBytes = attestation.slice(unprotectedPtr.end(), rawPayloadLength); - attestationTbs = - _constructAttestationTbs(rawProtectedBytes, rawProtectedLength, rawPayloadBytes, rawPayloadLength); - signature = attestation.slice(signaturePtr.start(), signaturePtr.length()); - } - - function validateAttestationWithHints( - bytes memory attestationTbs, - bytes memory signature, - bytes memory attestationSigHints - ) public returns (Ptrs memory) { - Ptrs memory ptrs = _parseAttestation(attestationTbs); - - require(ptrs.moduleID.length() > 0, "no module id"); - require(ptrs.timestamp > 0, "no timestamp"); - require(ptrs.cabundle.length > 0, "no cabundle"); - require(attestationTbs.keccak(ptrs.digest) == ATTESTATION_DIGEST, "invalid digest"); - require(1 <= ptrs.pcrs.length && ptrs.pcrs.length <= 32, "invalid pcrs"); - require( - ptrs.publicKey.isNull() || (1 <= ptrs.publicKey.length() && ptrs.publicKey.length() <= 1024), - "invalid pub key" - ); - require(ptrs.userData.isNull() || (ptrs.userData.length() <= 512), "invalid user data"); - require(ptrs.nonce.isNull() || (ptrs.nonce.length() <= 512), "invalid nonce"); - - for (uint256 i = 0; i < ptrs.pcrs.length; i++) { - require( - ptrs.pcrs[i].length() == 32 || ptrs.pcrs[i].length() == 48 || ptrs.pcrs[i].length() == 64, "invalid pcr" - ); - } - - bytes memory cert = attestationTbs.slice(ptrs.cert); - bytes[] memory cabundle = new bytes[](ptrs.cabundle.length); - for (uint256 i = 0; i < ptrs.cabundle.length; i++) { - require(1 <= ptrs.cabundle[i].length() && ptrs.cabundle[i].length() <= 1024, "invalid cabundle cert"); - cabundle[i] = attestationTbs.slice(ptrs.cabundle[i]); - } - - ICertManager.VerifiedCert memory parent = verifyCachedCertBundle(cert, cabundle); - bytes memory hash = Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length); - require( - p384Verifier.verifyP384SignatureWithHints(hash, signature, parent.pubKey, attestationSigHints), - "invalid sig" - ); - - return ptrs; - } - - function verifyCachedCertBundle(bytes memory certificate, bytes[] memory cabundle) - internal - returns (ICertManager.VerifiedCert memory) - { - bytes32 parentHash; - for (uint256 i = 0; i < cabundle.length; i++) { - parentHash = hintedCertManager.verifyCACertWithHints(cabundle[i], parentHash, ""); - } - return hintedCertManager.verifyClientCertWithHints(certificate, parentHash, ""); - } - - function _constructAttestationTbs( - bytes memory rawProtectedBytes, - uint256 rawProtectedLength, - bytes memory rawPayloadBytes, - uint256 rawPayloadLength - ) internal pure returns (bytes memory attestationTbs) { - attestationTbs = new bytes(13 + rawProtectedLength + rawPayloadLength); - attestationTbs[0] = bytes1(uint8(4 << 5 | 4)); - attestationTbs[1] = bytes1(uint8(3 << 5 | 10)); - attestationTbs[12 + rawProtectedLength] = bytes1(uint8(2 << 5)); - - string memory sig = "Signature1"; - uint256 dest; - uint256 sigSrc; - uint256 protectedSrc; - uint256 payloadSrc; - assembly { - dest := add(attestationTbs, 32) - sigSrc := add(sig, 32) - protectedSrc := add(rawProtectedBytes, 32) - payloadSrc := add(rawPayloadBytes, 32) - } - - LibBytes.memcpy(dest + 2, sigSrc, 10); - LibBytes.memcpy(dest + 12, protectedSrc, rawProtectedLength); - LibBytes.memcpy(dest + 13 + rawProtectedLength, payloadSrc, rawPayloadLength); - } - - function _parseAttestation(bytes memory attestationTbs) internal pure returns (Ptrs memory) { - require(attestationTbs.keccak(0, 18) == ATTESTATION_TBS_PREFIX, "invalid attestation prefix"); - - CborElement payload = attestationTbs.byteStringAt(18); - CborElement current = attestationTbs.mapAt(payload.start()); - - Ptrs memory ptrs; - uint256 end = payload.end(); - while (current.end() < end) { - if (uint8(attestationTbs[current.end()]) == 0xff) break; - current = attestationTbs.nextTextString(current); - bytes32 keyHash = attestationTbs.keccak(current); - if (keyHash == MODULE_ID_KEY) { - current = attestationTbs.nextTextString(current); - ptrs.moduleID = current; - } else if (keyHash == DIGEST_KEY) { - current = attestationTbs.nextTextString(current); - ptrs.digest = current; - } else if (keyHash == CERTIFICATE_KEY) { - current = attestationTbs.nextByteString(current); - ptrs.cert = current; - } else if (keyHash == PUBLIC_KEY_KEY) { - current = attestationTbs.nextByteStringOrNull(current); - ptrs.publicKey = current; - } else if (keyHash == USER_DATA_KEY) { - current = attestationTbs.nextByteStringOrNull(current); - ptrs.userData = current; - } else if (keyHash == NONCE_KEY) { - current = attestationTbs.nextByteStringOrNull(current); - ptrs.nonce = current; - } else if (keyHash == TIMESTAMP_KEY) { - current = attestationTbs.nextPositiveInt(current); - ptrs.timestamp = uint64(current.value()); - } else if (keyHash == CABUNDLE_KEY) { - current = attestationTbs.nextArray(current); - ptrs.cabundle = new CborElement[](current.value()); - for (uint256 i = 0; i < ptrs.cabundle.length; i++) { - current = attestationTbs.nextByteString(current); - ptrs.cabundle[i] = current; - } - } else if (keyHash == PCRS_KEY) { - current = attestationTbs.nextMap(current); - ptrs.pcrs = new CborElement[](current.value()); - for (uint256 i = 0; i < ptrs.pcrs.length; i++) { - current = attestationTbs.nextPositiveInt(current); - uint256 key = current.value(); - require(key < ptrs.pcrs.length, "invalid pcr key value"); - require(CborElement.unwrap(ptrs.pcrs[key]) == 0, "duplicate pcr key"); - current = attestationTbs.nextByteString(current); - ptrs.pcrs[key] = current; - } - } else { - revert("invalid attestation key"); - } - } - - return ptrs; - } -} diff --git a/src/P384Verifier.sol b/src/P384Verifier.sol index 3a27773..9bed6c3 100644 --- a/src/P384Verifier.sol +++ b/src/P384Verifier.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.15; import {ECDSA384Curve} from "./ECDSA384Curve.sol"; -import {ECDSA384Hinted} from "./ECDSA384Hinted.sol"; import {IP384Verifier} from "./IP384Verifier.sol"; +import {ECDSA384} from "@solarity/libs/crypto/ECDSA384.sol"; contract P384Verifier is IP384Verifier { function verifyP384SignatureWithHints( @@ -11,12 +11,12 @@ contract P384Verifier is IP384Verifier { bytes memory signature, bytes memory pubKey, bytes memory inverseHints - ) external returns (bool) { - return ECDSA384Hinted.verifyWithHints(_p384(), hash, signature, pubKey, inverseHints); + ) external view returns (bool) { + return ECDSA384.verifyWithHints(_p384(), hash, signature, pubKey, inverseHints); } - function _p384() internal pure returns (ECDSA384Hinted.Parameters memory) { - return ECDSA384Hinted.Parameters({ + function _p384() internal pure returns (ECDSA384.Parameters memory) { + return ECDSA384.Parameters({ a: ECDSA384Curve.CURVE_A, b: ECDSA384Curve.CURVE_B, gx: ECDSA384Curve.CURVE_GX, diff --git a/src/demo/CertManagerDemo.sol b/src/demo/CertManagerDemo.sol new file mode 100644 index 0000000..15fb756 --- /dev/null +++ b/src/demo/CertManagerDemo.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {CertManager} from "../CertManager.sol"; +import {IP384Verifier} from "../IP384Verifier.sol"; + +/// @notice Demo-only certificate manager with configurable certificate expiry grace. +/// @dev This exists only to replay expired Nitro fixtures on testnets. Do not use in production or audit target deployments. +contract CertManagerDemo is CertManager { + uint256 public immutable certificateExpiryGraceSeconds; + + constructor(IP384Verifier p384Verifier_, uint256 certificateExpiryGraceSeconds_) CertManager(p384Verifier_) { + certificateExpiryGraceSeconds = certificateExpiryGraceSeconds_; + } + + function _certificateExpired(uint256 notAfter) internal view override returns (bool) { + if (notAfter >= block.timestamp) { + return false; + } + return block.timestamp - notAfter > certificateExpiryGraceSeconds; + } +} diff --git a/test/CertManager.t.sol b/test/CertManager.t.sol deleted file mode 100644 index 2416cfa..0000000 --- a/test/CertManager.t.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {CertManager} from "../src/CertManager.sol"; - -contract CertManagerTest is Test { - CertManager public certManager; - - function setUp() public { - vm.warp(1732580000); - certManager = new CertManager(); - } - - function test_VerifyCertBundle() public { - bytes memory cert = - hex"3082027c30820201a0030201020210019332852aac84300000000067450121300a06082a8648ce3d04030330818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30666661626464383636323664616631662e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3234313132353232353833385a170d3234313132363031353834315a308193310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753313e303c06035504030c35692d30666661626464383636323664616631662d656e63303139333332383532616163383433302e75732d656173742d312e6177733076301006072a8648ce3d020106052b8104002203620004dcae821ff99b2d890039bb0ac16e729439d842ad713ffe2609f8bc3f7dc8909cfed78e39cb5583e350b2719d52f7109ee56c988f4081a5789940a3e591b43c3697bb4b79409fc9dda34dacfaff2594e55eeb15086e268d73cc392dc187499768a31d301b300c0603551d130101ff04023000300b0603551d0f0404030206c0300a06082a8648ce3d0403030369003066023100896c399489c267213e069bd73e1ec4ef201a0bb4032472acfda46b96b506862d19384667c6ede4a3fb8dbfe5f26595d9023100a71c8937ee835d489a99b3b24817982fa8f1034728ceed3deae88fb193d98588bf411d009904fbd7ac6b31b5b23eb2b6"; - bytes[] memory cabundle = new bytes[](4); - cabundle[0] = - hex"3082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff6"; - cabundle[1] = - hex"308202bf30820244a00302010202100b93e39c65609c59e8144a2ad34ba3a0300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3234313132333036333235355a170d3234313231333037333235355a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d353133623665666332313639303264372e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004ee78108039725a03e0b63a5d7d1244f6294eb7631f305e360997c8e5c06c779f23cfaeb64cb9aeac8a031bfac9f4dafc3621b4367f003c08c0ce410c2118396cc5d56ec4e92e1b17f9709b2bffcef462f7bcb97d6ca11325c4a30156c9720de7a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e041604142b3d75d274a3cdd61b2c13f539e08c960ce757dd300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030369003066023100fce7a6c2b38e0a8ebf0d28348d74463458b84bfe8b2b95315dd4da665e8e83d4ab911852a4e92a8263ecf571d2df3b89023100ab92be511136be76aa313018f9f4825eaad602d0342d268e6da632767f68f55f761fa9fd2a7ee716c481c67f26e3f8f4"; - cabundle[2] = - hex"308203153082029aa003020102021020c20971680e956fc3c8ce925d784bc7300a06082a8648ce3d0403033064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d353133623665666332313639303264372e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3234313132353032323230345a170d3234313230313030323230345a308189313c303a06035504030c33626635396531633335623630386133382e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c653076301006072a8648ce3d020106052b8104002203620004df741cd0537abbbc37bb32b06c835f497df86933b6ac8b4ee15d1251cfde596a7953756bb2759896a4d50c7cfb7d50cfc62fd4010a8c0d4a58a6f38988de6707d5aeaef3e3ca523ffac31260cc7c33546dc667d52ba524c39bd0ed6b82c0652da381ea3081e730120603551d130101ff040830060101ff020101301f0603551d230418301680142b3d75d274a3cdd61b2c13f539e08c960ce757dd301d0603551d0e04160414e8b15a6bc0b83e3d9e50ab9b289fb5fa0c61eabf300e0603551d0f0101ff0404030201863081800603551d1f047930773075a073a071866f687474703a2f2f63726c2d75732d656173742d312d6177732d6e6974726f2d656e636c617665732e73332e75732d656173742d312e616d617a6f6e6177732e636f6d2f63726c2f39636665653133332d613562622d343431392d613462372d3730386661643563363866662e63726c300a06082a8648ce3d04030303690030660231009595351f7c4411011eb4cf1a18181c2ed6901e84c2971c781e2cdc2725d5135066fc8d96ac70c98fc27106cdb345a563023100f96927f5bc58f1c29f8ea06d9bb5eeae3a6e2e572aff9911a8c90ed6e00c1cc7c534b9fde367781807c35ba9427d05fe"; - cabundle[3] = - hex"308202bd30820244a00302010202142690c27f442c86646256455d3442f8998be152dc300a06082a8648ce3d040303308189313c303a06035504030c33626635396531633335623630386133382e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c65301e170d3234313132353133353135375a170d3234313132363133353135375a30818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30666661626464383636323664616631662e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004c041c328653a7028db8060f3f19f589197b8c1a17dc755a5629c0f47c3cd3414412fd38b7f87fc70a6f22a0c2698fe1f748eff998f783add861a6b2373fba37a31f9bf0ab75fd2c4e17cc0df8124ddb0a4513483e40721ebb15a80619696747aa366306430120603551d130101ff040830060101ff020100300e0603551d0f0101ff040403020204301d0603551d0e041604145555580de23b6d83eb6a5be1b4dbdb376f69e444301f0603551d23041830168014e8b15a6bc0b83e3d9e50ab9b289fb5fa0c61eabf300a06082a8648ce3d0403030367003064023072c53164609cba5d7a16914d5d2102a9e70009288aae1215cc5e8d70f2d2d4b49bffb0119ec523e620275729f09e566e02302e0f2b7998eb25fa493dc1300329f7f142337b38e76df0a32b8660f41599c5febae120e4ed2c60efbbaa842ba6db8d91"; - certManager.verifyCACert(cabundle[0], 0); - certManager.verifyCACert(cabundle[1], keccak256(cabundle[0])); - certManager.verifyCACert(cabundle[2], keccak256(cabundle[1])); - certManager.verifyCACert(cabundle[3], keccak256(cabundle[2])); - certManager.verifyClientCert(cert, keccak256(cabundle[3])); - } - - function test_VerifyCACert() public { - bytes memory parent = - hex"3082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff6"; - bytes memory cert = - hex"308202bf30820244a00302010202100b93e39c65609c59e8144a2ad34ba3a0300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3234313132333036333235355a170d3234313231333037333235355a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d353133623665666332313639303264372e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004ee78108039725a03e0b63a5d7d1244f6294eb7631f305e360997c8e5c06c779f23cfaeb64cb9aeac8a031bfac9f4dafc3621b4367f003c08c0ce410c2118396cc5d56ec4e92e1b17f9709b2bffcef462f7bcb97d6ca11325c4a30156c9720de7a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e041604142b3d75d274a3cdd61b2c13f539e08c960ce757dd300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030369003066023100fce7a6c2b38e0a8ebf0d28348d74463458b84bfe8b2b95315dd4da665e8e83d4ab911852a4e92a8263ecf571d2df3b89023100ab92be511136be76aa313018f9f4825eaad602d0342d268e6da632767f68f55f761fa9fd2a7ee716c481c67f26e3f8f4"; - certManager.verifyCACert(cert, keccak256(parent)); - } -} diff --git a/test/IndefiniteLengthCbor.t.sol b/test/IndefiniteLengthCbor.t.sol index 0f0eb4e..90b0059 100644 --- a/test/IndefiniteLengthCbor.t.sol +++ b/test/IndefiniteLengthCbor.t.sol @@ -5,6 +5,8 @@ import {Test} from "forge-std/Test.sol"; import {CborDecode, CborElement, LibCborElement} from "../src/CborDecode.sol"; import {NitroValidator} from "../src/NitroValidator.sol"; import {ICertManager} from "../src/ICertManager.sol"; +import {IP384Verifier} from "../src/IP384Verifier.sol"; +import {P384Verifier} from "../src/P384Verifier.sol"; // ────────────────────────────────────────────────────────────── // CBOR constants (RFC 8949) @@ -76,7 +78,7 @@ contract CborDecodeHarness { /// @notice Exposes NitroValidator._parseAttestation (internal pure) for testing. contract NitroValidatorHarness is NitroValidator { - constructor(ICertManager cm) NitroValidator(cm) {} + constructor(ICertManager cm, IP384Verifier p384Verifier) NitroValidator(cm, p384Verifier) {} function parseAttestation(bytes memory attestationTbs) external pure returns (Ptrs memory) { return _parseAttestation(attestationTbs); @@ -92,6 +94,22 @@ contract StubCertManager is ICertManager { function verifyClientCert(bytes memory, bytes32) external pure returns (VerifiedCert memory v) { return v; } + + function verifyCACertWithHints(bytes memory, bytes32, bytes memory) external pure returns (bytes32) { + return bytes32(0); + } + + function verifyClientCertWithHints(bytes memory, bytes32, bytes memory) + external + pure + returns (VerifiedCert memory v) + { + return v; + } + + function loadVerified(bytes32) external pure returns (VerifiedCert memory v) { + return v; + } } // ────────────────────────────────────────────────────────────── @@ -178,7 +196,7 @@ contract NitroValidatorIndefiniteLengthTest is Test { NitroValidatorHarness validator; function setUp() public { - validator = new NitroValidatorHarness(ICertManager(address(new StubCertManager()))); + validator = new NitroValidatorHarness(ICertManager(address(new StubCertManager())), new P384Verifier()); } // ── Shared assertion helpers ────────────────────────────── diff --git a/test/NitroValidator.t.sol b/test/NitroValidator.t.sol deleted file mode 100644 index 25a0629..0000000 --- a/test/NitroValidator.t.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {NitroValidator} from "../src/NitroValidator.sol"; -import {CertManager} from "../src/CertManager.sol"; - -contract NitroValidatorTest is Test { - NitroValidator public validator; - - function setUp() public { - vm.warp(1732990000); - CertManager certManager = new CertManager(); - validator = new NitroValidator(certManager); - } - - function test_ValidateAttestation() public { - bytes memory attestationTbs = - hex"846a5369676e61747572653144a101382240591144a9696d6f64756c655f69647827692d30646533386232623638353363633965382d656e633031393336383565376665653764383566646967657374665348413338346974696d657374616d701b000001937de1c5436470637273b0005830ec74bfbe7f7445a6c7610e152935e028276f638042b74797b119648e13f7a3675796b721034c320f140ea001b41aeae2015830fa2593b59f3e4fc7daba5cbdddfd3449d67cd02d43bb1128885e8f38b914d081dccdb68fff6d5b7a76bcb866a18a74a302583056ba201a72e36cd051e95e5c4724c899039b711770f4d9d4fe7a1de007119a10b364badcd35e90f728a5bdc9109057230358303c9cadd84f0d027d6a5370c3de4af9179824fd6f3f02ebab723ee4439c75d8f5183e1c55f523415d44e9e6580b06655204583098bdf1bde262272618ccd73279e8ee00dd2c36974bd253de55413a25ceb2cd7221421207c2c09dde609f87481b6f6c940558300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000658300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000758300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000858300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000958300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b63657274696669636174655902803082027c30820201a00302010202100193685e7fee7d8500000000674b3bd8300a06082a8648ce3d04030330818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30646533386232623638353363633965382e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3234313133303136323234355a170d3234313133303139323234385a308193310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753313e303c06035504030c35692d30646533386232623638353363633965382d656e63303139333638356537666565376438352e75732d656173742d312e6177733076301006072a8648ce3d020106052b810400220362000461d930c61be969237398264901d6a37282cfd42c0694d012d9143cc86a339d567913dae552bad2f10d47c50d4e670247f0344983cbdc2d2e0045d4ccbdff59ef7a26ebf1be83a81e24a651c92008fe9f465757792a0877fba02c8b5e1eb2ed90a31d301b300c0603551d130101ff04023000300b0603551d0f0404030206c0300a06082a8648ce3d0403030369003066023100e48f39a39b444a6e5ea7a38b808198a2318dd531ed62faf4a9223f71f27dff4a5e495e32dd10f250bbaf1f892a4d328f023100d09fc8e48e233b9e972eecb94798865664dbeb0d75b29041f482777a4b7cae133483dcc9d35509c4967be51db37a745468636162756e646c65845902153082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff65902c2308202be30820244a003020102021056bfc987fd05ac99c475061b1a65eedc300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3234313132383036303734355a170d3234313231383037303734355a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d636264383238303866646138623434642e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b81040022036200040713751f4391a24bf27d688c9fdde4b7eec0c4922af63f242186269602eca12354e79356170287baa07dd84fa89834726891f9b4b27032b3e86000d32471a79fbf1a30c1982ad4ed069ad96a7e11d9ae2b5cd6a93ad613ee559ed7f6385a9a89a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e04160414bfbd54a168f57f7391b66ca60a2836f30acfb9a1300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030368003065023100c05dfd13378b1eecd926b0c3ba8da01eec89ec5502ae7ca73cb958557ca323057962fff2681993a0ab223b6eacf11033023035664252d7f9e2c89c988cc4164d390f898a5e8ac2e99dc58595aa4c624e93face7964026a99b4bcca7088b51250ccc459031a308203163082029ba003020102021100cb286a4a4a09207f8b0c14950dcd6861300a06082a8648ce3d0403033064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d636264383238303866646138623434642e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3234313133303033313435345a170d3234313230363031313435345a308189313c303a06035504030c33343762313739376131663031386266302e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c653076301006072a8648ce3d020106052b810400220362000423959f700ef87dcbdba686449d944f2a89ad22aa03d73cf93d28853f2fb6a80b0cc714d3090e34cda8234eef8f804e46c0dcb216062afba3e2b36a693660d9965e2370308b8e1ffad8542ddbe3e733077481b0cbc747d8c7beb7612820d4fe95a381ea3081e730120603551d130101ff040830060101ff020101301f0603551d23041830168014bfbd54a168f57f7391b66ca60a2836f30acfb9a1301d0603551d0e04160414bbf52a3a42fdc4f301f72536b90e65aaa1b70a99300e0603551d0f0101ff0404030201863081800603551d1f047930773075a073a071866f687474703a2f2f63726c2d75732d656173742d312d6177732d6e6974726f2d656e636c617665732e73332e75732d656173742d312e616d617a6f6e6177732e636f6d2f63726c2f30366434386638652d326330382d343738312d613634352d6231646534303261656662382e63726c300a06082a8648ce3d0403030369003066023100fa31509230632a002939201eb5686b52d79f0276db5c2b954bed324caa5c3271a60d25e2e05a5e6700e488a074af4ecd02310084770462c2ef86dcdb11fa8a31dcf770866cbd28822b682a112b98c09a30e35e94affd3482bf8b01b59a0a7775b4af185902c3308202bf30820245a003020102021500c8925d382506d820d93d2c704a7523c4ba2ddfaa300a06082a8648ce3d040303308189313c303a06035504030c33343762313739376131663031386266302e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c65301e170d3234313133303132343133315a170d3234313230313132343133315a30818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30646533386232623638353363633965382e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004466754b5718024df3564bcd722361e7c65a4922eda7b1f826758e30afac40b04a281062897d085311fd509b70a6bbc5f8280f86ae2ff255ad147146fc97b7afb16064f0712d335c1d473b716be320be625e91c5870973084b3a0005bc020c7b2a366306430120603551d130101ff040830060101ff020100300e0603551d0f0101ff040403020204301d0603551d0e04160414345c86a9ec55bc30cafd923d6b73111d9c57abc0301f0603551d23041830168014bbf52a3a42fdc4f301f72536b90e65aaa1b70a99300a06082a8648ce3d0403030368003065023100aba82c02f40acb9846012bf070578217eeb2ebbfd16414948438cf67eeab6f64cdc5a152998766c88b2cdebd5a97ebd402307421611ed511567bc8e6a0a2805b981ef38dc3bd6a6c661522802b5c5d658cc4fcc9b5e8df148b161d366926896736836a7075626c69635f6b657958410433a4701fa871b188983d570e2c2d8cf98fd66eb19ba8ca7617bc8e20e152a5d7f0205eae76e608ce855077e4565be69db4471ef72857253742f9602c11ff04e569757365725f64617461f6656e6f6e6365f6"; - bytes memory signature = - hex"874e67088943e85654beb78443c747def2c3736bf93e2b52d033b3e936a04ead91f7b5a1229a1615f237f138f64399418b8046b6e40cd93e750b58f5e1aded45ebf3f103b9ea19a9b874142b576638dad2da142254ae913664649be22e0b83f9"; - validator.validateAttestation(attestationTbs, signature); - } - - function test_DecodeAttestationTbs() public { - bytes memory attestation = - hex"8444a1013822a0591144a9696d6f64756c655f69647827692d30646533386232623638353363633965382d656e633031393336383565376665653764383566646967657374665348413338346974696d657374616d701b000001937de1c5436470637273b0005830ec74bfbe7f7445a6c7610e152935e028276f638042b74797b119648e13f7a3675796b721034c320f140ea001b41aeae2015830fa2593b59f3e4fc7daba5cbdddfd3449d67cd02d43bb1128885e8f38b914d081dccdb68fff6d5b7a76bcb866a18a74a302583056ba201a72e36cd051e95e5c4724c899039b711770f4d9d4fe7a1de007119a10b364badcd35e90f728a5bdc9109057230358303c9cadd84f0d027d6a5370c3de4af9179824fd6f3f02ebab723ee4439c75d8f5183e1c55f523415d44e9e6580b06655204583098bdf1bde262272618ccd73279e8ee00dd2c36974bd253de55413a25ceb2cd7221421207c2c09dde609f87481b6f6c940558300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000658300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000758300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000858300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000958300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b63657274696669636174655902803082027c30820201a00302010202100193685e7fee7d8500000000674b3bd8300a06082a8648ce3d04030330818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30646533386232623638353363633965382e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3234313133303136323234355a170d3234313133303139323234385a308193310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753313e303c06035504030c35692d30646533386232623638353363633965382d656e63303139333638356537666565376438352e75732d656173742d312e6177733076301006072a8648ce3d020106052b810400220362000461d930c61be969237398264901d6a37282cfd42c0694d012d9143cc86a339d567913dae552bad2f10d47c50d4e670247f0344983cbdc2d2e0045d4ccbdff59ef7a26ebf1be83a81e24a651c92008fe9f465757792a0877fba02c8b5e1eb2ed90a31d301b300c0603551d130101ff04023000300b0603551d0f0404030206c0300a06082a8648ce3d0403030369003066023100e48f39a39b444a6e5ea7a38b808198a2318dd531ed62faf4a9223f71f27dff4a5e495e32dd10f250bbaf1f892a4d328f023100d09fc8e48e233b9e972eecb94798865664dbeb0d75b29041f482777a4b7cae133483dcc9d35509c4967be51db37a745468636162756e646c65845902153082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff65902c2308202be30820244a003020102021056bfc987fd05ac99c475061b1a65eedc300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3234313132383036303734355a170d3234313231383037303734355a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d636264383238303866646138623434642e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b81040022036200040713751f4391a24bf27d688c9fdde4b7eec0c4922af63f242186269602eca12354e79356170287baa07dd84fa89834726891f9b4b27032b3e86000d32471a79fbf1a30c1982ad4ed069ad96a7e11d9ae2b5cd6a93ad613ee559ed7f6385a9a89a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e04160414bfbd54a168f57f7391b66ca60a2836f30acfb9a1300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030368003065023100c05dfd13378b1eecd926b0c3ba8da01eec89ec5502ae7ca73cb958557ca323057962fff2681993a0ab223b6eacf11033023035664252d7f9e2c89c988cc4164d390f898a5e8ac2e99dc58595aa4c624e93face7964026a99b4bcca7088b51250ccc459031a308203163082029ba003020102021100cb286a4a4a09207f8b0c14950dcd6861300a06082a8648ce3d0403033064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d636264383238303866646138623434642e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3234313133303033313435345a170d3234313230363031313435345a308189313c303a06035504030c33343762313739376131663031386266302e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c653076301006072a8648ce3d020106052b810400220362000423959f700ef87dcbdba686449d944f2a89ad22aa03d73cf93d28853f2fb6a80b0cc714d3090e34cda8234eef8f804e46c0dcb216062afba3e2b36a693660d9965e2370308b8e1ffad8542ddbe3e733077481b0cbc747d8c7beb7612820d4fe95a381ea3081e730120603551d130101ff040830060101ff020101301f0603551d23041830168014bfbd54a168f57f7391b66ca60a2836f30acfb9a1301d0603551d0e04160414bbf52a3a42fdc4f301f72536b90e65aaa1b70a99300e0603551d0f0101ff0404030201863081800603551d1f047930773075a073a071866f687474703a2f2f63726c2d75732d656173742d312d6177732d6e6974726f2d656e636c617665732e73332e75732d656173742d312e616d617a6f6e6177732e636f6d2f63726c2f30366434386638652d326330382d343738312d613634352d6231646534303261656662382e63726c300a06082a8648ce3d0403030369003066023100fa31509230632a002939201eb5686b52d79f0276db5c2b954bed324caa5c3271a60d25e2e05a5e6700e488a074af4ecd02310084770462c2ef86dcdb11fa8a31dcf770866cbd28822b682a112b98c09a30e35e94affd3482bf8b01b59a0a7775b4af185902c3308202bf30820245a003020102021500c8925d382506d820d93d2c704a7523c4ba2ddfaa300a06082a8648ce3d040303308189313c303a06035504030c33343762313739376131663031386266302e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c65301e170d3234313133303132343133315a170d3234313230313132343133315a30818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30646533386232623638353363633965382e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004466754b5718024df3564bcd722361e7c65a4922eda7b1f826758e30afac40b04a281062897d085311fd509b70a6bbc5f8280f86ae2ff255ad147146fc97b7afb16064f0712d335c1d473b716be320be625e91c5870973084b3a0005bc020c7b2a366306430120603551d130101ff040830060101ff020100300e0603551d0f0101ff040403020204301d0603551d0e04160414345c86a9ec55bc30cafd923d6b73111d9c57abc0301f0603551d23041830168014bbf52a3a42fdc4f301f72536b90e65aaa1b70a99300a06082a8648ce3d0403030368003065023100aba82c02f40acb9846012bf070578217eeb2ebbfd16414948438cf67eeab6f64cdc5a152998766c88b2cdebd5a97ebd402307421611ed511567bc8e6a0a2805b981ef38dc3bd6a6c661522802b5c5d658cc4fcc9b5e8df148b161d366926896736836a7075626c69635f6b657958410433a4701fa871b188983d570e2c2d8cf98fd66eb19ba8ca7617bc8e20e152a5d7f0205eae76e608ce855077e4565be69db4471ef72857253742f9602c11ff04e569757365725f64617461f6656e6f6e6365f65860874e67088943e85654beb78443c747def2c3736bf93e2b52d033b3e936a04ead91f7b5a1229a1615f237f138f64399418b8046b6e40cd93e750b58f5e1aded45ebf3f103b9ea19a9b874142b576638dad2da142254ae913664649be22e0b83f9"; - (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); - validator.validateAttestation(attestationTbs, signature); - } -} diff --git a/test/RootCertCheck.t.sol b/test/RootCertCheck.t.sol index 8ba1269..17a5e63 100644 --- a/test/RootCertCheck.t.sol +++ b/test/RootCertCheck.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; import {CertManager} from "../src/CertManager.sol"; +import {P384Verifier} from "../src/P384Verifier.sol"; import {Asn1Decode, Asn1Ptr, LibAsn1Ptr} from "../src/Asn1Decode.sol"; contract RootCertCheckTest is Test, CertManager { @@ -13,6 +14,8 @@ contract RootCertCheckTest is Test, CertManager { bytes public constant ROOT_CA_CERT = hex"3082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff6"; + constructor() CertManager(new P384Verifier()) {} + function setUp() public { vm.warp(1732580000); } diff --git a/test/bench/Bench.t.sol b/test/bench/Bench.t.sol deleted file mode 100644 index 7b47413..0000000 --- a/test/bench/Bench.t.sol +++ /dev/null @@ -1,291 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; - -import {Test, console} from "forge-std/Test.sol"; -import {ECDSA384} from "@solarity/libs/crypto/ECDSA384.sol"; -import {ECDSA384Bench} from "./ECDSA384Bench.sol"; -import {ECDSA384Curve} from "../../src/ECDSA384Curve.sol"; - -contract RealHarness { - function verify(bytes memory h, bytes memory s, bytes memory p) external view returns (bool) { - return ECDSA384.verify(ECDSA384Curve.p384(), h, s, p); - } -} - -contract BenchHarness { - // returns (#inversions, #other modexp calls) - function countVerify(bytes memory h, bytes memory s, bytes memory p) - external - returns (uint256 inv, uint256 other, bool ok) - { - assembly { - tstore(0, 0) - tstore(1, 0) - } - ECDSA384Bench.Parameters memory params = ECDSA384Bench.Parameters({ - a: ECDSA384Curve.CURVE_A, - b: ECDSA384Curve.CURVE_B, - gx: ECDSA384Curve.CURVE_GX, - gy: ECDSA384Curve.CURVE_GY, - p: ECDSA384Curve.CURVE_P, - n: ECDSA384Curve.CURVE_N, - lowSmax: ECDSA384Curve.CURVE_LOW_S_MAX - }); - ok = ECDSA384Bench.verify(params, h, s, p); - assembly { - inv := tload(0) - other := tload(1) - } - } - - function profileVerify(bytes memory h, bytes memory s, bytes memory p) - external - returns (uint256[5] memory invByPhase, uint256[5] memory otherByPhase, bool ok) - { - assembly { - tstore(0, 0) - tstore(1, 0) - } - ECDSA384Bench.Parameters memory params = ECDSA384Bench.Parameters({ - a: ECDSA384Curve.CURVE_A, - b: ECDSA384Curve.CURVE_B, - gx: ECDSA384Curve.CURVE_GX, - gy: ECDSA384Curve.CURVE_GY, - p: ECDSA384Curve.CURVE_P, - n: ECDSA384Curve.CURVE_N, - lowSmax: ECDSA384Curve.CURVE_LOW_S_MAX - }); - ok = ECDSA384Bench.verify(params, h, s, p); - - uint256 prevInv; - uint256 prevOther; - for (uint256 i = 0; i < 5; ++i) { - uint256 inv; - uint256 other; - assembly { - inv := tload(add(10, i)) - other := tload(add(20, i)) - } - invByPhase[i] = inv - prevInv; - otherByPhase[i] = other - prevOther; - prevInv = inv; - prevOther = other; - } - } - - function collectInverseHints(bytes memory h, bytes memory s, bytes memory p) external returns (bytes memory hints) { - assembly { - tstore(0, 0) - tstore(1, 0) - tstore(2, 0) - tstore(7, 0) - tstore(8, 1) - } - ECDSA384Bench.Parameters memory params = _params(); - bool ok = ECDSA384Bench.verify(params, h, s, p); - require(ok, "collect verify failed"); - - uint256 count; - assembly { - count := tload(2) - tstore(8, 0) - } - hints = new bytes(count * 48); - for (uint256 i = 0; i < count; ++i) { - uint256 hi; - uint256 lo; - assembly { - let slot_ := add(1000, mul(i, 2)) - hi := tload(slot_) - lo := tload(add(slot_, 1)) - let dst_ := add(add(hints, 0x20), mul(i, 48)) - mstore(dst_, shl(128, hi)) - mstore(add(dst_, 0x10), lo) - } - } - } - - function countHintedVerify(bytes memory h, bytes memory s, bytes memory p, bytes memory hints) - external - returns (uint256 inv, uint256 other, uint256 consumed, bool ok) - { - assembly { - tstore(0, 0) - tstore(1, 0) - } - (ok, consumed) = ECDSA384Bench.verifyWithHintsConsumed(_params(), h, s, p, hints); - require(consumed == hints.length, "unused inverse hints"); - assembly { - inv := tload(0) - other := tload(1) - } - } - - function _params() internal pure returns (ECDSA384Bench.Parameters memory) { - return ECDSA384Bench.Parameters({ - a: ECDSA384Curve.CURVE_A, - b: ECDSA384Curve.CURVE_B, - gx: ECDSA384Curve.CURVE_GX, - gy: ECDSA384Curve.CURVE_GY, - p: ECDSA384Curve.CURVE_P, - n: ECDSA384Curve.CURVE_N, - lowSmax: ECDSA384Curve.CURVE_LOW_S_MAX - }); - } -} - -/// @notice Baseline benchmark: real per-verify gas + exact MODEXP census + EIP-7883 projection. -contract BenchTest is Test { - RealHarness real; - BenchHarness bench; - - // Synthetic-but-valid P384 signature (openssl secp384r1, SHA-384). Counts are - // input-independent up to ~Hamming-weight variance, so this is representative. - bytes h = hex"e6bb58cb85db069a2c0c310020f946d9c47e8ff6a43895ae824369325c74fb8d30897d7ea16447ef9975eb351a916a11"; - bytes s = - hex"1ab266914fb82615eba02dc55eb8d6e32e4adaa12a927786ab2353d9649f645e86b0166c990e36dbce9efcc743b998646025d5af666c17fb103d0c89f8a78aea9f8fff3703e21a3ab69f48b21d0bbf008f7c9702d54fd6f1af65223fe3936b3c"; - bytes p = - hex"edb97631be68370a653e5601fa2f12da63db7cdb1f6cf7413004f4de274f3d1c046de28a2530240080e7d84c436cb935ebb5c7a9fcd8027f5d97bf49fff3f9a7346270e10e0a529eeb117d75409f3c9acfde069a7a577a6a6b8048d78abfe5f5"; - - // Per-call MODEXP gas, derived from EIP-2565 / EIP-7883 for this library's - // operand profiles (verified against the EIP formulas): - // inversion : base=64, exp=64, mod=64, expHead bit-length 128 - // other : squaring/mulmul/reduce -> hits the min-gas floor - uint256 constant INV_2565 = 8170; // floor(8^2 * (8*32+127) / 3) - uint256 constant INV_7883 = 81792; // 2*8^2 * (16*32+127) - uint256 constant OTHER_2565 = 200; // EIP-2565 minimum - uint256 constant OTHER_7883 = 500; // EIP-7883 minimum - uint256 constant TX_CAP = 16_777_216; // EIP-7825 per-transaction gas cap (2^24) - - function setUp() public { - real = new RealHarness(); - bench = new BenchHarness(); - } - - function test_Baseline() public { - // 1) ground-truth current gas for a single verify (uninstrumented) - uint256 g0 = gasleft(); - bool ok = real.verify(h, s, p); - uint256 verifyGas2565 = g0 - gasleft(); - assertTrue(ok, "verify must pass"); - - // 2) exact MODEXP census (instrumented copy) - (uint256 inv, uint256 other,) = bench.countVerify(h, s, p); - (uint256[5] memory invByPhase, uint256[5] memory otherByPhase,) = bench.profileVerify(h, s, p); - - // 3) MODEXP gas under each pricing - uint256 mod2565 = inv * INV_2565 + other * OTHER_2565; - uint256 mod7883 = inv * INV_7883 + other * OTHER_7883; - - // 4) project post-Fusaka verify: only the MODEXP portion reprices, - // the rest of the verify (EVM arithmetic / memory) is unchanged. - uint256 verifyGas7883 = verifyGas2565 + (mod7883 - mod2565); - - console.log("==== SINGLE P384 ECDSA VERIFY ===="); - console.log("measured verify gas (EIP-2565) :", verifyGas2565); - console.log("MODEXP calls total :", inv + other); - console.log(" field inversions :", inv); - console.log(" other (sq/mul/reduce) :", other); - console.log("MODEXP gas EIP-2565 :", mod2565); - console.log("MODEXP gas EIP-7883 :", mod7883); - console.log(" modexp share of verify (2565,%):", (mod2565 * 100) / verifyGas2565); - console.log("PROJECTED verify gas (EIP-7883) :", verifyGas7883); - console.log(" blow-up factor x100 :", (verifyGas7883 * 100) / verifyGas2565); - console.log("per-tx cap (EIP-7825) :", TX_CAP); - console.log("verify fits in 1 tx post-Fusaka? :", verifyGas7883 <= TX_CAP ? 1 : 0); - console.log("min verifies that must be split :", (verifyGas7883 + TX_CAP - 1) / TX_CAP); - console.log("==== MODEXP PHASE BREAKDOWN ===="); - _logPhase("on-curve check", invByPhase[0], otherByPhase[0]); - _logPhase("scalar divs", invByPhase[1], otherByPhase[1]); - _logPhase("precompute table", invByPhase[2], otherByPhase[2]); - _logPhase("double scalar mult", invByPhase[3], otherByPhase[3]); - _logPhase("final mod", invByPhase[4], otherByPhase[4]); - } - - /// EXPERIMENT 001 (analytical): replace each on-chain field inversion with a - /// caller-supplied witness verified by ONE modmul (b * b_inv == 1 mod p). - /// Each moddiv goes from {1 inversion + 1 mulmul} to {2 mulmuls}: the 570 - /// inversions become 570 cheap mulmuls (floor-priced), nothing else changes. - function test_HintedInversionModel() public { - uint256 g0 = gasleft(); - bool ok = real.verify(h, s, p); - uint256 verifyGas2565 = g0 - gasleft(); - assertTrue(ok); - (uint256 inv, uint256 other,) = bench.countVerify(h, s, p); - - uint256 nonModexp = verifyGas2565 - (inv * INV_2565 + other * OTHER_2565); - // hinted profile: 0 inversions, (other + inv) floor-priced calls - uint256 calls = other + inv; - uint256 hinted2565 = nonModexp + calls * OTHER_2565; - uint256 hinted7883 = nonModexp + calls * OTHER_7883; - uint256 witnessBytes = inv * 48; - uint256 witnessCalldataGas = witnessBytes * 16; // worst case: all nonzero bytes - - console.log("==== EXPERIMENT 001: HINTED INVERSIONS (model) ===="); - console.log("non-MODEXP verify gas (fixed) :", nonModexp); - console.log("hinted verify gas EIP-2565 :", hinted2565); - console.log("hinted verify gas EIP-7883 :", hinted7883); - console.log(" + witness calldata (worst) gas :", witnessCalldataGas); - console.log(" = total post-Fusaka :", hinted7883 + witnessCalldataGas); - console.log("fits 1 tx post-Fusaka? :", (hinted7883 + witnessCalldataGas) <= TX_CAP ? 1 : 0); - console.log("witness bytes per verify :", witnessBytes); - } - - function test_HintedInversionPrototype() public { - bytes memory hints = bench.collectInverseHints(h, s, p); - - uint256 g0 = gasleft(); - (uint256 inv, uint256 other, uint256 consumed, bool ok) = bench.countHintedVerify(h, s, p, hints); - uint256 hintedGas2565 = g0 - gasleft(); - assertTrue(ok, "hinted verify must pass"); - assertEq(consumed, hints.length, "must consume all hints"); - - uint256 mod2565 = inv * INV_2565 + other * OTHER_2565; - uint256 mod7883 = inv * INV_7883 + other * OTHER_7883; - uint256 hintedGas7883 = hintedGas2565 + (mod7883 - mod2565); - uint256 witnessCalldataGas = hints.length * 16; // worst case: all nonzero bytes - - console.log("==== EXPERIMENT 002: HINTED INVERSIONS (prototype) ===="); - console.log("hinted verify gas EIP-2565 :", hintedGas2565); - console.log("MODEXP calls total :", inv + other); - console.log(" field inversions :", inv); - console.log(" other (sq/mul/reduce/check) :", other); - console.log("projected verify gas EIP-7883 :", hintedGas7883); - console.log(" + witness calldata (worst) gas :", witnessCalldataGas); - console.log(" = total post-Fusaka :", hintedGas7883 + witnessCalldataGas); - console.log("fits 1 tx post-Fusaka? :", (hintedGas7883 + witnessCalldataGas) <= TX_CAP ? 1 : 0); - console.log("witness bytes per verify :", hints.length); - } - - function test_HintedInversionRejectsMutatedHint() public { - bytes memory hints = bench.collectInverseHints(h, s, p); - hints[100] = bytes1(uint8(hints[100]) ^ 1); - - vm.expectRevert("bad inverse hint"); - bench.countHintedVerify(h, s, p, hints); - } - - function test_HintedInversionRejectsTruncatedHints() public { - bytes memory hints = bench.collectInverseHints(h, s, p); - assembly { - mstore(hints, sub(mload(hints), 1)) - } - - vm.expectRevert("inverse hint underflow"); - bench.countHintedVerify(h, s, p, hints); - } - - function test_HintedInversionRejectsSurplusHints() public { - bytes memory hints = abi.encodePacked(bench.collectInverseHints(h, s, p), bytes1(0x00)); - - vm.expectRevert("unused inverse hints"); - bench.countHintedVerify(h, s, p, hints); - } - - function _logPhase(string memory name, uint256 inv, uint256 other) internal pure { - console.log(name); - console.log(" inversions:", inv); - console.log(" other :", other); - console.log(" EIP-7883 MODEXP gas:", inv * INV_7883 + other * OTHER_7883); - } -} diff --git a/test/bench/HintedNitroBench.sol b/test/bench/HintedNitroBench.sol deleted file mode 100644 index 7cfca6f..0000000 --- a/test/bench/HintedNitroBench.sol +++ /dev/null @@ -1,293 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; - -import {Asn1Decode, Asn1Ptr, LibAsn1Ptr} from "../../src/Asn1Decode.sol"; -import {CborDecode, CborElement, LibCborElement} from "../../src/CborDecode.sol"; -import {CertManager} from "../../src/CertManager.sol"; -import {ECDSA384Curve} from "../../src/ECDSA384Curve.sol"; -import {ICertManager} from "../../src/ICertManager.sol"; -import {LibBytes} from "../../src/LibBytes.sol"; -import {NitroValidator} from "../../src/NitroValidator.sol"; -import {Sha2Ext} from "../../src/Sha2Ext.sol"; -import {ECDSA384Bench} from "./ECDSA384Bench.sol"; - -contract HintedCertManagerBench is CertManager { - using Asn1Decode for bytes; - using LibAsn1Ptr for Asn1Ptr; - using LibBytes for bytes; - - function verifyCACertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) - external - returns (bytes32) - { - bytes32 certHash = keccak256(cert); - _verifyCertWithHints(cert, certHash, true, _loadVerified(parentCertHash), signatureHints); - return certHash; - } - - function verifyClientCertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) - external - returns (VerifiedCert memory) - { - return _verifyCertWithHints(cert, keccak256(cert), false, _loadVerified(parentCertHash), signatureHints); - } - - function loadVerifiedForBench(bytes32 certHash) external view returns (VerifiedCert memory) { - return _loadVerified(certHash); - } - - function _verifyCertWithHints( - bytes memory certificate, - bytes32 certHash, - bool ca, - VerifiedCert memory parent, - bytes memory signatureHints - ) internal returns (VerifiedCert memory cert) { - if (certHash != ROOT_CA_CERT_HASH) { - require(parent.pubKey.length > 0, "parent cert unverified"); - require(parent.notAfter >= block.timestamp, "parent cert expired"); - require(parent.ca, "parent cert is not a CA"); - require(!ca || parent.maxPathLen != 0, "maxPathLen exceeded"); - } - - cert = _loadVerified(certHash); - if (cert.pubKey.length != 0) { - require(cert.notAfter >= block.timestamp, "cert expired"); - require(cert.ca == ca, "cert is not a CA"); - return cert; - } - - Asn1Ptr root = certificate.root(); - Asn1Ptr tbsCertPtr = certificate.firstChildOf(root); - (uint64 notAfter, int64 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey) = - _parseTbs(certificate, tbsCertPtr, ca); - - require(parent.subjectHash == issuerHash, "issuer / subject mismatch"); - - if (parent.maxPathLen > 0 && (maxPathLen < 0 || maxPathLen >= parent.maxPathLen)) { - maxPathLen = parent.maxPathLen - 1; - } - - _verifyCertSignatureWithHints(certificate, tbsCertPtr, parent.pubKey, signatureHints); - - cert = VerifiedCert({ - ca: ca, notAfter: notAfter, maxPathLen: maxPathLen, subjectHash: subjectHash, pubKey: pubKey - }); - _saveVerified(certHash, cert); - - emit CertVerified(certHash); - } - - function _verifyCertSignatureWithHints( - bytes memory certificate, - Asn1Ptr ptr, - bytes memory pubKey, - bytes memory signatureHints - ) internal { - Asn1Ptr sigAlgoPtr = certificate.nextSiblingOf(ptr); - require(certificate.keccak(sigAlgoPtr.content(), sigAlgoPtr.length()) == CERT_ALGO_OID, "invalid cert sig algo"); - - bytes memory hash = Sha2Ext.sha384(certificate, ptr.header(), ptr.totalLength()); - bytes memory sigPacked = _certSignature(certificate, sigAlgoPtr); - - require(ECDSA384Bench.verifyWithHints(_benchParams(), hash, sigPacked, pubKey, signatureHints), "invalid sig"); - } - - function _certSignature(bytes memory certificate, Asn1Ptr sigAlgoPtr) - internal - pure - returns (bytes memory sigPacked) - { - Asn1Ptr sigPtr = certificate.nextSiblingOf(sigAlgoPtr); - Asn1Ptr sigBPtr = certificate.bitstring(sigPtr); - Asn1Ptr sigRoot = certificate.rootOf(sigBPtr); - Asn1Ptr sigRPtr = certificate.firstChildOf(sigRoot); - Asn1Ptr sigSPtr = certificate.nextSiblingOf(sigRPtr); - (uint128 rhi, uint256 rlo) = certificate.uint384At(sigRPtr); - (uint128 shi, uint256 slo) = certificate.uint384At(sigSPtr); - sigPacked = abi.encodePacked(rhi, rlo, shi, slo); - } - - function _benchParams() internal pure returns (ECDSA384Bench.Parameters memory) { - return ECDSA384Bench.Parameters({ - a: ECDSA384Curve.CURVE_A, - b: ECDSA384Curve.CURVE_B, - gx: ECDSA384Curve.CURVE_GX, - gy: ECDSA384Curve.CURVE_GY, - p: ECDSA384Curve.CURVE_P, - n: ECDSA384Curve.CURVE_N, - lowSmax: ECDSA384Curve.CURVE_LOW_S_MAX - }); - } -} - -contract P384HintCollectorBench { - using Asn1Decode for bytes; - using LibAsn1Ptr for Asn1Ptr; - - function collectVerifyHints(bytes memory hash, bytes memory signature, bytes memory pubKey) - public - returns (bytes memory hints) - { - (hints,) = collectVerifyProfile(hash, signature, pubKey); - } - - function collectVerifyProfile(bytes memory hash, bytes memory signature, bytes memory pubKey) - public - returns (bytes memory hints, uint256 hintedOtherCalls) - { - assembly { - tstore(0, 0) - tstore(1, 0) - tstore(2, 0) - tstore(7, 0) - tstore(8, 1) - } - - bool ok = ECDSA384Bench.verify(_benchParams(), hash, signature, pubKey); - require(ok, "collect verify failed"); - - uint256 count; - uint256 inv; - uint256 other; - assembly { - count := tload(2) - inv := tload(0) - other := tload(1) - tstore(8, 0) - } - require(count == inv, "inverse collection mismatch"); - hintedOtherCalls = inv + other; - - hints = new bytes(count * 48); - for (uint256 i = 0; i < count; ++i) { - uint256 hi; - uint256 lo; - assembly { - let slot_ := add(1000, mul(i, 2)) - hi := tload(slot_) - lo := tload(add(slot_, 1)) - let dst_ := add(add(hints, 0x20), mul(i, 48)) - mstore(dst_, shl(128, hi)) - mstore(add(dst_, 0x10), lo) - } - } - } - - function collectCertSignatureHints(bytes memory certificate, bytes memory parentPubKey) - external - returns (bytes memory) - { - (bytes memory hints,) = collectCertSignatureProfile(certificate, parentPubKey); - return hints; - } - - function collectCertSignatureProfile(bytes memory certificate, bytes memory parentPubKey) - public - returns (bytes memory, uint256) - { - Asn1Ptr root = certificate.root(); - Asn1Ptr tbsCertPtr = certificate.firstChildOf(root); - bytes memory hash = Sha2Ext.sha384(certificate, tbsCertPtr.header(), tbsCertPtr.totalLength()); - - Asn1Ptr sigAlgoPtr = certificate.nextSiblingOf(tbsCertPtr); - bytes memory sigPacked = _certSignature(certificate, sigAlgoPtr); - - return collectVerifyProfile(hash, sigPacked, parentPubKey); - } - - function _certSignature(bytes memory certificate, Asn1Ptr sigAlgoPtr) - internal - pure - returns (bytes memory sigPacked) - { - Asn1Ptr sigPtr = certificate.nextSiblingOf(sigAlgoPtr); - Asn1Ptr sigBPtr = certificate.bitstring(sigPtr); - Asn1Ptr sigRoot = certificate.rootOf(sigBPtr); - Asn1Ptr sigRPtr = certificate.firstChildOf(sigRoot); - Asn1Ptr sigSPtr = certificate.nextSiblingOf(sigRPtr); - (uint128 rhi, uint256 rlo) = certificate.uint384At(sigRPtr); - (uint128 shi, uint256 slo) = certificate.uint384At(sigSPtr); - sigPacked = abi.encodePacked(rhi, rlo, shi, slo); - } - - function _benchParams() internal pure returns (ECDSA384Bench.Parameters memory) { - return ECDSA384Bench.Parameters({ - a: ECDSA384Curve.CURVE_A, - b: ECDSA384Curve.CURVE_B, - gx: ECDSA384Curve.CURVE_GX, - gy: ECDSA384Curve.CURVE_GY, - p: ECDSA384Curve.CURVE_P, - n: ECDSA384Curve.CURVE_N, - lowSmax: ECDSA384Curve.CURVE_LOW_S_MAX - }); - } -} - -contract HintedNitroValidatorBench is NitroValidator { - using CborDecode for bytes; - using LibBytes for bytes; - using LibCborElement for CborElement; - - constructor(ICertManager certManager_) NitroValidator(certManager_) {} - - function validateAttestationWithHints( - bytes memory attestationTbs, - bytes memory signature, - bytes memory attestationSigHints - ) public returns (Ptrs memory) { - Ptrs memory ptrs = _parseAttestation(attestationTbs); - - require(ptrs.moduleID.length() > 0, "no module id"); - require(ptrs.timestamp > 0, "no timestamp"); - require(ptrs.cabundle.length > 0, "no cabundle"); - require(attestationTbs.keccak(ptrs.digest) == ATTESTATION_DIGEST, "invalid digest"); - require(1 <= ptrs.pcrs.length && ptrs.pcrs.length <= 32, "invalid pcrs"); - require( - ptrs.publicKey.isNull() || (1 <= ptrs.publicKey.length() && ptrs.publicKey.length() <= 1024), - "invalid pub key" - ); - require(ptrs.userData.isNull() || (ptrs.userData.length() <= 512), "invalid user data"); - require(ptrs.nonce.isNull() || (ptrs.nonce.length() <= 512), "invalid nonce"); - - for (uint256 i = 0; i < ptrs.pcrs.length; i++) { - require( - ptrs.pcrs[i].length() == 32 || ptrs.pcrs[i].length() == 48 || ptrs.pcrs[i].length() == 64, "invalid pcr" - ); - } - - bytes memory cert = attestationTbs.slice(ptrs.cert); - bytes[] memory cabundle = new bytes[](ptrs.cabundle.length); - for (uint256 i = 0; i < ptrs.cabundle.length; i++) { - require(1 <= ptrs.cabundle[i].length() && ptrs.cabundle[i].length() <= 1024, "invalid cabundle cert"); - cabundle[i] = attestationTbs.slice(ptrs.cabundle[i]); - } - - ICertManager.VerifiedCert memory parent = verifyCertBundle(cert, cabundle); - bytes memory hash = Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length); - _verifySignatureWithHints(parent.pubKey, hash, signature, attestationSigHints); - - return ptrs; - } - - function _verifySignatureWithHints( - bytes memory pubKey, - bytes memory hash, - bytes memory sig, - bytes memory signatureHints - ) internal { - require(ECDSA384Bench.verifyWithHints(_benchParams(), hash, sig, pubKey, signatureHints), "invalid sig"); - } - - function _benchParams() internal pure returns (ECDSA384Bench.Parameters memory) { - return ECDSA384Bench.Parameters({ - a: ECDSA384Curve.CURVE_A, - b: ECDSA384Curve.CURVE_B, - gx: ECDSA384Curve.CURVE_GX, - gy: ECDSA384Curve.CURVE_GY, - p: ECDSA384Curve.CURVE_P, - n: ECDSA384Curve.CURVE_N, - lowSmax: ECDSA384Curve.CURVE_LOW_S_MAX - }); - } -} diff --git a/test/bench/ECDSA384Bench.sol b/test/helpers/ECDSA384HintCollector.sol similarity index 84% rename from test/bench/ECDSA384Bench.sol rename to test/helpers/ECDSA384HintCollector.sol index 787d746..bd93cb8 100644 --- a/test/bench/ECDSA384Bench.sol +++ b/test/helpers/ECDSA384HintCollector.sol @@ -14,9 +14,9 @@ import {MemoryUtils} from "@solarity/libs/utils/MemoryUtils.sol"; * * We also tried using projective coordinates, however, the gas consumption rose to ~9 million gas. */ -library ECDSA384Bench { +library ECDSA384HintCollectorLib { using MemoryUtils for *; - using U384Bench for *; + using U384HintCollector for *; /** * @notice 384-bit curve parameters. @@ -101,8 +101,8 @@ library ECDSA384Bench { unchecked { _Inputs memory inputs_; - (inputs_.r, inputs_.s) = U384Bench.init2(signature_); - (inputs_.x, inputs_.y) = U384Bench.init2(pubKey_); + (inputs_.r, inputs_.s) = U384HintCollector.init2(signature_); + (inputs_.x, inputs_.y) = U384HintCollector.init2(pubKey_); _Parameters memory params_ = _Parameters({ a: curveParams_.a.init(), @@ -114,19 +114,21 @@ library ECDSA384Bench { lowSmax: curveParams_.lowSmax.init() }); - uint256 call = - useHints_ ? U384Bench.initCallWithHints(params_.p, inverseHints_) : U384Bench.initCall(params_.p); + uint256 call = useHints_ + ? U384HintCollector.initCallWithHints(params_.p, inverseHints_) + : U384HintCollector.initCall(params_.p); /// accept s only from the lower part of the curve if ( - U384Bench.eqInteger(inputs_.r, 0) || U384Bench.cmp(inputs_.r, params_.n) >= 0 - || U384Bench.eqInteger(inputs_.s, 0) || U384Bench.cmp(inputs_.s, params_.lowSmax) > 0 + U384HintCollector.eqInteger(inputs_.r, 0) || U384HintCollector.cmp(inputs_.r, params_.n) >= 0 + || U384HintCollector.eqInteger(inputs_.s, 0) + || U384HintCollector.cmp(inputs_.s, params_.lowSmax) > 0 ) { - return (false, U384Bench.hintCursor(call)); + return (false, U384HintCollector.hintCursor(call)); } if (!_isOnCurve(call, params_.p, params_.a, params_.b, inputs_.x, inputs_.y)) { - return (false, U384Bench.hintCursor(call)); + return (false, U384HintCollector.hintCursor(call)); } _checkpoint(0); @@ -147,12 +149,12 @@ library ECDSA384Bench { } } - uint256 scalar1 = U384Bench.moddiv(call, hashedMessage_.init(), inputs_.s, params_.n); - uint256 scalar2 = U384Bench.moddiv(call, inputs_.r, inputs_.s, params_.n); + uint256 scalar1 = U384HintCollector.moddiv(call, hashedMessage_.init(), inputs_.s, params_.n); + uint256 scalar2 = U384HintCollector.moddiv(call, inputs_.r, inputs_.s, params_.n); _checkpoint(1); { - uint256 three = U384Bench.init(3); + uint256 three = U384HintCollector.init(3); /// We use 6-bit masks where the first 3 bits refer to `scalar1` and the last 3 bits refer to `scalar2`. uint256[2][64] memory points_ = _precomputePointsTable( @@ -164,10 +166,10 @@ library ECDSA384Bench { _checkpoint(3); } - U384Bench.modAssign(call, scalar1, params_.n); + U384HintCollector.modAssign(call, scalar1, params_.n); _checkpoint(4); - return (U384Bench.eq(scalar1, inputs_.r), U384Bench.hintCursor(call)); + return (U384HintCollector.eq(scalar1, inputs_.r), U384HintCollector.hintCursor(call)); } } @@ -183,22 +185,25 @@ library ECDSA384Bench { */ function _isOnCurve(uint256 call, uint256 p, uint256 a, uint256 b, uint256 x, uint256 y) private returns (bool) { unchecked { - if (U384Bench.eqInteger(x, 0) || U384Bench.eq(x, p) || U384Bench.eqInteger(y, 0) || U384Bench.eq(y, p)) { + if ( + U384HintCollector.eqInteger(x, 0) || U384HintCollector.eq(x, p) || U384HintCollector.eqInteger(y, 0) + || U384HintCollector.eq(y, p) + ) { return false; } - uint256 LHS = U384Bench.modexp(call, y, 2); - uint256 RHS = U384Bench.modexp(call, x, 3); + uint256 LHS = U384HintCollector.modexp(call, y, 2); + uint256 RHS = U384HintCollector.modexp(call, x, 3); - if (!U384Bench.eqInteger(a, 0)) { - RHS = U384Bench.modadd(RHS, U384Bench.modmul(call, x, a), p); // x^3 + a*x + if (!U384HintCollector.eqInteger(a, 0)) { + RHS = U384HintCollector.modadd(RHS, U384HintCollector.modmul(call, x, a), p); // x^3 + a*x } - if (!U384Bench.eqInteger(b, 0)) { - RHS = U384Bench.modadd(RHS, b, p); // x^3 + a*x + b + if (!U384HintCollector.eqInteger(b, 0)) { + RHS = U384HintCollector.modadd(RHS, b, p); // x^3 + a*x + b } - return U384Bench.eq(LHS, RHS); + return U384HintCollector.eq(LHS, RHS); } } @@ -279,24 +284,24 @@ library ECDSA384Bench { return (0, 0); } - if (U384Bench.eqInteger(y1, 0)) { + if (U384HintCollector.eqInteger(y1, 0)) { return (0, 0); } - uint256 m1 = U384Bench.modexp(call, x1, 2); - U384Bench.modmulAssign(call, m1, three); - U384Bench.modaddAssign(m1, a, p); + uint256 m1 = U384HintCollector.modexp(call, x1, 2); + U384HintCollector.modmulAssign(call, m1, three); + U384HintCollector.modaddAssign(m1, a, p); - uint256 m2 = U384Bench.modshl1(y1, p); - U384Bench.moddivAssign(call, m1, m2); + uint256 m2 = U384HintCollector.modshl1(y1, p); + U384HintCollector.moddivAssign(call, m1, m2); - x2 = U384Bench.modexp(call, m1, 2); - U384Bench.modsubAssign(x2, x1, p); - U384Bench.modsubAssign(x2, x1, p); + x2 = U384HintCollector.modexp(call, m1, 2); + U384HintCollector.modsubAssign(x2, x1, p); + U384HintCollector.modsubAssign(x2, x1, p); - y2 = U384Bench.modsub(x1, x2, p); - U384Bench.modmulAssign(call, y2, m1); - U384Bench.modsubAssign(y2, y1, p); + y2 = U384HintCollector.modsub(x1, x2, p); + U384HintCollector.modmulAssign(call, y2, m1); + U384HintCollector.modsubAssign(y2, y1, p); } } @@ -312,62 +317,62 @@ library ECDSA384Bench { return (0, 0); } - if (U384Bench.eqInteger(y1, 0)) { + if (U384HintCollector.eqInteger(y1, 0)) { return (0, 0); } - uint256 m1 = U384Bench.modexp(call, x1, 2); - U384Bench.modmulAssign(call, m1, three); - U384Bench.modaddAssign(m1, a, p); + uint256 m1 = U384HintCollector.modexp(call, x1, 2); + U384HintCollector.modmulAssign(call, m1, three); + U384HintCollector.modaddAssign(m1, a, p); - uint256 m2 = U384Bench.modshl1(y1, p); - U384Bench.moddivAssign(call, m1, m2); + uint256 m2 = U384HintCollector.modshl1(y1, p); + U384HintCollector.moddivAssign(call, m1, m2); - x2 = U384Bench.modexp(call, m1, 2); - U384Bench.modsubAssign(x2, x1, p); - U384Bench.modsubAssign(x2, x1, p); + x2 = U384HintCollector.modexp(call, m1, 2); + U384HintCollector.modsubAssign(x2, x1, p); + U384HintCollector.modsubAssign(x2, x1, p); - y2 = U384Bench.modsub(x1, x2, p); - U384Bench.modmulAssign(call, y2, m1); - U384Bench.modsubAssign(y2, y1, p); + y2 = U384HintCollector.modsub(x1, x2, p); + U384HintCollector.modmulAssign(call, y2, m1); + U384HintCollector.modsubAssign(y2, y1, p); - if (U384Bench.eqInteger(y2, 0)) { + if (U384HintCollector.eqInteger(y2, 0)) { return (0, 0); } - U384Bench.modexpAssignTo(call, m1, x2, 2); - U384Bench.modmulAssign(call, m1, three); - U384Bench.modaddAssign(m1, a, p); + U384HintCollector.modexpAssignTo(call, m1, x2, 2); + U384HintCollector.modmulAssign(call, m1, three); + U384HintCollector.modaddAssign(m1, a, p); - U384Bench.modshl1AssignTo(m2, y2, p); - U384Bench.moddivAssign(call, m1, m2); + U384HintCollector.modshl1AssignTo(m2, y2, p); + U384HintCollector.moddivAssign(call, m1, m2); - U384Bench.modexpAssignTo(call, x1, m1, 2); - U384Bench.modsubAssign(x1, x2, p); - U384Bench.modsubAssign(x1, x2, p); + U384HintCollector.modexpAssignTo(call, x1, m1, 2); + U384HintCollector.modsubAssign(x1, x2, p); + U384HintCollector.modsubAssign(x1, x2, p); - U384Bench.modsubAssignTo(y1, x2, x1, p); - U384Bench.modmulAssign(call, y1, m1); - U384Bench.modsubAssign(y1, y2, p); + U384HintCollector.modsubAssignTo(y1, x2, x1, p); + U384HintCollector.modmulAssign(call, y1, m1); + U384HintCollector.modsubAssign(y1, y2, p); - if (U384Bench.eqInteger(y1, 0)) { + if (U384HintCollector.eqInteger(y1, 0)) { return (0, 0); } - U384Bench.modexpAssignTo(call, m1, x1, 2); - U384Bench.modmulAssign(call, m1, three); - U384Bench.modaddAssign(m1, a, p); + U384HintCollector.modexpAssignTo(call, m1, x1, 2); + U384HintCollector.modmulAssign(call, m1, three); + U384HintCollector.modaddAssign(m1, a, p); - U384Bench.modshl1AssignTo(m2, y1, p); - U384Bench.moddivAssign(call, m1, m2); + U384HintCollector.modshl1AssignTo(m2, y1, p); + U384HintCollector.moddivAssign(call, m1, m2); - U384Bench.modexpAssignTo(call, x2, m1, 2); - U384Bench.modsubAssign(x2, x1, p); - U384Bench.modsubAssign(x2, x1, p); + U384HintCollector.modexpAssignTo(call, x2, m1, 2); + U384HintCollector.modsubAssign(x2, x1, p); + U384HintCollector.modsubAssign(x2, x1, p); - U384Bench.modsubAssignTo(y2, x1, x2, p); - U384Bench.modmulAssign(call, y2, m1); - U384Bench.modsubAssign(y2, y1, p); + U384HintCollector.modsubAssignTo(y2, x1, x2, p); + U384HintCollector.modmulAssign(call, y2, m1); + U384HintCollector.modsubAssign(y2, y1, p); } } @@ -393,26 +398,26 @@ library ECDSA384Bench { return x1 == 0 ? (x2.copy(), y2.copy()) : (x1.copy(), y1.copy()); } - if (U384Bench.eq(x1, x2)) { - if (U384Bench.eq(y1, y2)) { + if (U384HintCollector.eq(x1, x2)) { + if (U384HintCollector.eq(y1, y2)) { return _twiceAffine(call, p, three, a, x1, y1); } return (0, 0); } - uint256 m1 = U384Bench.modsub(y1, y2, p); - uint256 m2 = U384Bench.modsub(x1, x2, p); + uint256 m1 = U384HintCollector.modsub(y1, y2, p); + uint256 m2 = U384HintCollector.modsub(x1, x2, p); - U384Bench.moddivAssign(call, m1, m2); + U384HintCollector.moddivAssign(call, m1, m2); - x3 = U384Bench.modexp(call, m1, 2); - U384Bench.modsubAssign(x3, x1, p); - U384Bench.modsubAssign(x3, x2, p); + x3 = U384HintCollector.modexp(call, m1, 2); + U384HintCollector.modsubAssign(x3, x1, p); + U384HintCollector.modsubAssign(x3, x2, p); - y3 = U384Bench.modsub(x1, x3, p); - U384Bench.modmulAssign(call, y3, m1); - U384Bench.modsubAssign(y3, y1, p); + y3 = U384HintCollector.modsub(x1, x3, p); + U384HintCollector.modmulAssign(call, y3, m1); + U384HintCollector.modsubAssign(y3, y1, p); } } @@ -460,7 +465,7 @@ library ECDSA384Bench { * * Should not be used outside of this file. */ -library U384Bench { +library U384HintCollector { uint256 private constant SHORT_ALLOCATION = 64; uint256 private constant MUL_OFFSET = 288; diff --git a/test/helpers/HintedNitroTestHelpers.sol b/test/helpers/HintedNitroTestHelpers.sol new file mode 100644 index 0000000..682255f --- /dev/null +++ b/test/helpers/HintedNitroTestHelpers.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Asn1Decode, Asn1Ptr, LibAsn1Ptr} from "../../src/Asn1Decode.sol"; +import {ECDSA384Curve} from "../../src/ECDSA384Curve.sol"; +import {Sha2Ext} from "../../src/Sha2Ext.sol"; +import {ECDSA384HintCollectorLib} from "./ECDSA384HintCollector.sol"; + +contract P384HintCollector { + using Asn1Decode for bytes; + using LibAsn1Ptr for Asn1Ptr; + + function collectVerifyHints(bytes memory hash, bytes memory signature, bytes memory pubKey) + public + returns (bytes memory hints) + { + (hints,) = collectVerifyProfile(hash, signature, pubKey); + } + + function collectVerifyProfile(bytes memory hash, bytes memory signature, bytes memory pubKey) + public + returns (bytes memory hints, uint256 hintedOtherCalls) + { + assembly { + tstore(0, 0) + tstore(1, 0) + tstore(2, 0) + tstore(7, 0) + tstore(8, 1) + } + + bool ok = ECDSA384HintCollectorLib.verify(_hintParams(), hash, signature, pubKey); + require(ok, "collect verify failed"); + + uint256 count; + uint256 inv; + uint256 other; + assembly { + count := tload(2) + inv := tload(0) + other := tload(1) + tstore(8, 0) + } + require(count == inv, "inverse collection mismatch"); + hintedOtherCalls = inv + other; + + hints = new bytes(count * 48); + for (uint256 i = 0; i < count; ++i) { + uint256 hi; + uint256 lo; + assembly { + let slot_ := add(1000, mul(i, 2)) + hi := tload(slot_) + lo := tload(add(slot_, 1)) + let dst_ := add(add(hints, 0x20), mul(i, 48)) + mstore(dst_, shl(128, hi)) + mstore(add(dst_, 0x10), lo) + } + } + } + + function collectCertSignatureHints(bytes memory certificate, bytes memory parentPubKey) + external + returns (bytes memory) + { + (bytes memory hints,) = collectCertSignatureProfile(certificate, parentPubKey); + return hints; + } + + function collectCertSignatureProfile(bytes memory certificate, bytes memory parentPubKey) + public + returns (bytes memory, uint256) + { + Asn1Ptr root = certificate.root(); + Asn1Ptr tbsCertPtr = certificate.firstChildOf(root); + bytes memory hash = Sha2Ext.sha384(certificate, tbsCertPtr.header(), tbsCertPtr.totalLength()); + + Asn1Ptr sigAlgoPtr = certificate.nextSiblingOf(tbsCertPtr); + bytes memory sigPacked = _certSignature(certificate, sigAlgoPtr); + + return collectVerifyProfile(hash, sigPacked, parentPubKey); + } + + function _certSignature(bytes memory certificate, Asn1Ptr sigAlgoPtr) + internal + pure + returns (bytes memory sigPacked) + { + Asn1Ptr sigPtr = certificate.nextSiblingOf(sigAlgoPtr); + Asn1Ptr sigBPtr = certificate.bitstring(sigPtr); + Asn1Ptr sigRoot = certificate.rootOf(sigBPtr); + Asn1Ptr sigRPtr = certificate.firstChildOf(sigRoot); + Asn1Ptr sigSPtr = certificate.nextSiblingOf(sigRPtr); + (uint128 rhi, uint256 rlo) = certificate.uint384At(sigRPtr); + (uint128 shi, uint256 slo) = certificate.uint384At(sigSPtr); + sigPacked = abi.encodePacked(rhi, rlo, shi, slo); + } + + function _hintParams() internal pure returns (ECDSA384HintCollectorLib.Parameters memory) { + return ECDSA384HintCollectorLib.Parameters({ + a: ECDSA384Curve.CURVE_A, + b: ECDSA384Curve.CURVE_B, + gx: ECDSA384Curve.CURVE_GX, + gy: ECDSA384Curve.CURVE_GY, + p: ECDSA384Curve.CURVE_P, + n: ECDSA384Curve.CURVE_N, + lowSmax: ECDSA384Curve.CURVE_LOW_S_MAX + }); + } +} diff --git a/test/bench/RealAttestationBench.t.sol b/test/hinted/HintedNitroAttestation.t.sol similarity index 69% rename from test/bench/RealAttestationBench.t.sol rename to test/hinted/HintedNitroAttestation.t.sol index 293abc6..7fa2eff 100644 --- a/test/bench/RealAttestationBench.t.sol +++ b/test/hinted/HintedNitroAttestation.t.sol @@ -4,28 +4,24 @@ pragma solidity ^0.8.26; import {Test, console} from "forge-std/Test.sol"; import {NitroValidator} from "../../src/NitroValidator.sol"; import {CertManager} from "../../src/CertManager.sol"; -import {CertManagerHinted} from "../../src/CertManagerHinted.sol"; +import {CertManagerDemo} from "../../src/demo/CertManagerDemo.sol"; import {ICertManager} from "../../src/ICertManager.sol"; import {CborDecode} from "../../src/CborDecode.sol"; -import {NitroValidatorHinted} from "../../src/NitroValidatorHinted.sol"; import {P384Verifier} from "../../src/P384Verifier.sol"; import {Sha2Ext} from "../../src/Sha2Ext.sol"; -import {P384HintCollectorBench} from "./HintedNitroBench.sol"; +import {P384HintCollector} from "../helpers/HintedNitroTestHelpers.sol"; contract NitroValidatorParseHarness is NitroValidator { - constructor(CertManager certManager) NitroValidator(certManager) {} + constructor(CertManager certManager, P384Verifier p384Verifier) NitroValidator(certManager, p384Verifier) {} function parseAttestation(bytes memory attestationTbs) external pure returns (Ptrs memory) { return _parseAttestation(attestationTbs); } } -contract RealAttestationBenchTest is Test { +contract HintedNitroAttestationTest is Test { using CborDecode for bytes; - uint256 constant P384_VERIFY_2565 = 7_938_921; - uint256 constant P384_VERIFY_7883 = 50_646_861; - uint256 constant HINTED_P384_VERIFY_7883_WITH_CALLDATA = 6_040_809; uint256 constant HINTED_MODEXP_FLOOR_DELTA = 300; // EIP-7883 floor 500 - EIP-2565 floor 200 uint256 constant TX_CAP = 16_777_216; uint256 constant EIP170_RUNTIME_LIMIT = 24_576; @@ -33,10 +29,8 @@ contract RealAttestationBenchTest is Test { CertManager certManager; NitroValidator validator; NitroValidatorParseHarness parser; - CertManagerHinted hintedCertManager; - NitroValidatorHinted hintedValidator; P384Verifier p384Verifier; - P384HintCollectorBench hintCollector; + P384HintCollector hintCollector; struct SequenceSummary { ICertManager.VerifiedCert leaf; @@ -53,149 +47,14 @@ contract RealAttestationBenchTest is Test { function setUp() public { vm.warp(1767472867); // 2026-01-03T20:41:07Z, matching the attestation timestamp. - certManager = new CertManager(); - validator = new NitroValidator(certManager); - parser = new NitroValidatorParseHarness(certManager); p384Verifier = new P384Verifier(); - hintedCertManager = new CertManagerHinted(p384Verifier); - hintedValidator = new NitroValidatorHinted(hintedCertManager, p384Verifier); - hintCollector = new P384HintCollectorBench(); + certManager = new CertManager(p384Verifier); + validator = new NitroValidator(certManager, p384Verifier); + parser = new NitroValidatorParseHarness(certManager, p384Verifier); + hintCollector = new P384HintCollector(); } - function test_RealAttestationBaseline() public { - bytes memory attestation = _decodeBase64(_realAttestationB64()); - attestation = _repairMissingPublicKeyBytes(attestation); - - uint256 g0 = gasleft(); - (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); - uint256 decodeGas = g0 - gasleft(); - - g0 = gasleft(); - validator.validateAttestation(attestationTbs, signature); - uint256 coldValidateGas = g0 - gasleft(); - - g0 = gasleft(); - validator.validateAttestation(attestationTbs, signature); - uint256 cachedValidateGas = g0 - gasleft(); - uint256 cachedPostFusakaUnoptimized = cachedValidateGas + (P384_VERIFY_7883 - P384_VERIFY_2565); - uint256 cachedPostFusakaHinted = cachedValidateGas - P384_VERIFY_2565 + HINTED_P384_VERIFY_7883_WITH_CALLDATA; - - console.log("==== REAL ATTESTATION BASELINE ===="); - console.log("attestation bytes :", attestation.length); - console.log("attestationTbs bytes :", attestationTbs.length); - console.log("signature bytes :", signature.length); - console.log("decodeAttestationTbs gas :", decodeGas); - console.log("validateAttestation gas (cold) :", coldValidateGas); - console.log("validateAttestation gas (cached) :", cachedValidateGas); - console.log("cached post-Fusaka unoptimized :", cachedPostFusakaUnoptimized); - console.log("cached post-Fusaka hinted+calldata:", cachedPostFusakaHinted); - console.log("cached hinted fits tx cap? :", cachedPostFusakaHinted <= TX_CAP ? 1 : 0); - } - - function test_RealAttestationPerCertSplitProjection() public { - bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); - (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); - NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); - - bytes32 parentHash; - console.log("==== REAL ATTESTATION PER-CERT SPLIT ===="); - for (uint256 i = 0; i < ptrs.cabundle.length; ++i) { - bytes memory caCert = attestationTbs.slice(ptrs.cabundle[i]); - uint256 certG0 = gasleft(); - parentHash = certManager.verifyCACert(caCert, parentHash); - uint256 certGas = certG0 - gasleft(); - uint256 projected = i == 0 ? certGas : _projectOneHintedP384(certGas); - console.log("cabundle index :", i); - console.log(" current gas :", certGas); - console.log(" projected hinted post-Fusaka :", projected); - console.log(" fits tx cap? :", projected <= TX_CAP ? 1 : 0); - assertLe(projected, TX_CAP); - } - - bytes memory clientCert = attestationTbs.slice(ptrs.cert); - uint256 clientG0 = gasleft(); - certManager.verifyClientCert(clientCert, parentHash); - uint256 clientGas = clientG0 - gasleft(); - uint256 clientProjected = _projectOneHintedP384(clientGas); - console.log("client cert"); - console.log(" current gas :", clientGas); - console.log(" projected hinted post-Fusaka :", clientProjected); - console.log(" fits tx cap? :", clientProjected <= TX_CAP ? 1 : 0); - assertLe(clientProjected, TX_CAP); - } - - function test_ProductionShapedHintedPerCertSplit() public { - bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); - (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); - NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); - - bytes32 parentHash; - bytes memory parentPubKey; - console.log("==== PRODUCTION-SHAPED HINTED PER-CERT SPLIT ===="); - for (uint256 i = 0; i < ptrs.cabundle.length; ++i) { - bytes memory caCert = attestationTbs.slice(ptrs.cabundle[i]); - bytes memory hints; - uint256 hintedOtherCalls; - if (i != 0) { - parentPubKey = hintedCertManager.loadVerified(parentHash).pubKey; - (hints, hintedOtherCalls) = hintCollector.collectCertSignatureProfile(caCert, parentPubKey); - } - - uint256 certG0 = gasleft(); - parentHash = hintedCertManager.verifyCACertWithHints(caCert, parentHash, hints); - uint256 certGas = certG0 - gasleft(); - uint256 projected = _projectHintedMeasured(certGas, hints.length, hintedOtherCalls); - console.log("cabundle index :", i); - console.log(" current hinted gas :", certGas); - console.log(" inverse hint bytes :", hints.length); - console.log(" hinted MODEXP floor calls :", hintedOtherCalls); - console.log(" projected hinted post-Fusaka :", projected); - console.log(" fits tx cap? :", projected <= TX_CAP ? 1 : 0); - assertLe(projected, TX_CAP); - } - - bytes memory clientCert = attestationTbs.slice(ptrs.cert); - parentPubKey = hintedCertManager.loadVerified(parentHash).pubKey; - (bytes memory clientHints, uint256 clientHintedOtherCalls) = - hintCollector.collectCertSignatureProfile(clientCert, parentPubKey); - - uint256 clientG0 = gasleft(); - hintedCertManager.verifyClientCertWithHints(clientCert, parentHash, clientHints); - uint256 clientGas = clientG0 - gasleft(); - uint256 clientProjected = _projectHintedMeasured(clientGas, clientHints.length, clientHintedOtherCalls); - console.log("client cert"); - console.log(" current hinted gas :", clientGas); - console.log(" inverse hint bytes :", clientHints.length); - console.log(" hinted MODEXP floor calls :", clientHintedOtherCalls); - console.log(" projected hinted post-Fusaka :", clientProjected); - console.log(" fits tx cap? :", clientProjected <= TX_CAP ? 1 : 0); - assertLe(clientProjected, TX_CAP); - } - - function test_ProductionShapedHintedCachedAttestation() public { - bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); - (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); - ICertManager.VerifiedCert memory leaf = _cacheCertBundleWithHints(attestationTbs); - - bytes memory hash = Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length); - (bytes memory attestationHints, uint256 hintedOtherCalls) = - hintCollector.collectVerifyProfile(hash, signature, leaf.pubKey); - - uint256 g0 = gasleft(); - hintedValidator.validateAttestationWithHints(attestationTbs, signature, attestationHints); - uint256 hintedGas = g0 - gasleft(); - uint256 projected = _projectHintedMeasured(hintedGas, attestationHints.length, hintedOtherCalls); - - console.log("==== PRODUCTION-SHAPED HINTED CACHED ATTESTATION ===="); - console.log("current hinted cached gas :", hintedGas); - console.log("inverse hint bytes :", attestationHints.length); - console.log("hinted MODEXP floor calls :", hintedOtherCalls); - console.log("projected hinted post-Fusaka :", projected); - console.log("fits tx cap? :", projected <= TX_CAP ? 1 : 0); - assertLe(projected, TX_CAP); - } - - function test_ProductionShapedHintedAttestationRejectsSurplusHint() public { + function test_HintedAttestationRejectsSurplusHint() public { bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); ICertManager.VerifiedCert memory leaf = _cacheCertBundleWithHints(attestationTbs); @@ -205,10 +64,10 @@ contract RealAttestationBenchTest is Test { abi.encodePacked(hintCollector.collectVerifyHints(hash, signature, leaf.pubKey), bytes1(0x00)); vm.expectRevert("unused inverse hints"); - hintedValidator.validateAttestationWithHints(attestationTbs, signature, attestationHints); + validator.validateAttestationWithHints(attestationTbs, signature, attestationHints); } - function test_008_ProductionCandidateRejectsMutatedCertHint() public { + function test_HintedCACertRejectsMutatedHint() public { bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); @@ -217,10 +76,10 @@ contract RealAttestationBenchTest is Test { hints[10] = bytes1(uint8(hints[10]) ^ 1); vm.expectRevert("bad inverse hint"); - hintedCertManager.verifyCACertWithHints(caCert, parentHash, hints); + certManager.verifyCACertWithHints(caCert, parentHash, hints); } - function test_008_ProductionCandidateRejectsTruncatedCertHint() public { + function test_HintedCACertRejectsTruncatedHint() public { bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); @@ -231,56 +90,56 @@ contract RealAttestationBenchTest is Test { } vm.expectRevert("inverse hint underflow"); - hintedCertManager.verifyCACertWithHints(caCert, parentHash, hints); + certManager.verifyCACertWithHints(caCert, parentHash, hints); } - function test_008_ProductionCandidateRejectsWrongParentHash() public { + function test_HintedCACertRejectsWrongParentHash() public { bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); bytes memory caCert = attestationTbs.slice(ptrs.cabundle[1]); vm.expectRevert("parent cert unverified"); - hintedCertManager.verifyCACertWithHints(caCert, bytes32(0), ""); + certManager.verifyCACertWithHints(caCert, bytes32(0), ""); } - function test_008_ProductionCandidateRejectsExpiredCachedCert() public { + function test_HintedCACertRejectsExpiredCachedCert() public { bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); (bytes memory caCert, bytes32 parentHash, bytes memory parentPubKey) = _firstNonRootCA(attestationTbs, ptrs); bytes memory hints = hintCollector.collectCertSignatureHints(caCert, parentPubKey); - hintedCertManager.verifyCACertWithHints(caCert, parentHash, hints); + certManager.verifyCACertWithHints(caCert, parentHash, hints); vm.warp(1768953600); // 2026-01-21T00:00:00Z, after this fixture's first non-root CA expiry. vm.expectRevert("cert expired"); - hintedCertManager.verifyCACertWithHints(caCert, parentHash, ""); + certManager.verifyCACertWithHints(caCert, parentHash, ""); } - function test_008_ProductionCandidateRejectsCachedRoleMismatch() public { + function test_HintedCACertRejectsCachedRoleMismatch() public { bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); (bytes memory caCert, bytes32 parentHash, bytes memory parentPubKey) = _firstNonRootCA(attestationTbs, ptrs); bytes memory hints = hintCollector.collectCertSignatureHints(caCert, parentPubKey); - hintedCertManager.verifyCACertWithHints(caCert, parentHash, hints); + certManager.verifyCACertWithHints(caCert, parentHash, hints); vm.expectRevert("cert is not a CA"); - hintedCertManager.verifyClientCertWithHints(caCert, parentHash, ""); + certManager.verifyClientCertWithHints(caCert, parentHash, ""); } - function test_008_ProductionCandidateValidateRequiresWarmCache() public { + function test_HintedValidationRequiresWarmCache() public { bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); - CertManagerHinted freshCertManager = new CertManagerHinted(p384Verifier); - NitroValidatorHinted freshValidator = new NitroValidatorHinted(freshCertManager, p384Verifier); + CertManager freshCertManager = new CertManager(p384Verifier); + NitroValidator freshValidator = new NitroValidator(freshCertManager, p384Verifier); vm.expectRevert("inverse hint underflow"); freshValidator.validateAttestationWithHints(attestationTbs, signature, ""); } - function test_008_ProductionCandidateRejectsInvalidFinalSignature() public { + function test_HintedValidationRejectsInvalidFinalSignature() public { bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); ICertManager.VerifiedCert memory leaf = _cacheCertBundleWithHints(attestationTbs); @@ -289,31 +148,34 @@ contract RealAttestationBenchTest is Test { signature[0] = bytes1(uint8(signature[0]) ^ 1); vm.expectRevert(); - hintedValidator.validateAttestationWithHints(attestationTbs, signature, attestationHints); + validator.validateAttestationWithHints(attestationTbs, signature, attestationHints); } - function test_009_DeployableHintedContractsFitEIP170() public view { - console.log("==== 009 DEPLOYABLE CONTRACT SIZES ===="); + function test_DeployableContractsFitEIP170() public view { + console.log("==== DEPLOYABLE CONTRACT SIZES ===="); console.log("P384Verifier runtime bytes :", address(p384Verifier).code.length); - console.log("CertManagerHinted runtime bytes :", address(hintedCertManager).code.length); - console.log("NitroValidatorHinted runtime bytes:", address(hintedValidator).code.length); + console.log("CertManager runtime bytes :", address(certManager).code.length); + console.log("NitroValidator runtime bytes :", address(validator).code.length); assertLe(address(p384Verifier).code.length, EIP170_RUNTIME_LIMIT); - assertLe(address(hintedCertManager).code.length, EIP170_RUNTIME_LIMIT); - assertLe(address(hintedValidator).code.length, EIP170_RUNTIME_LIMIT); + assertLe(address(certManager).code.length, EIP170_RUNTIME_LIMIT); + assertLe(address(validator).code.length, EIP170_RUNTIME_LIMIT); } - function test_009_DeployableCertManagerDisablesUnhintedEntrypoints() public { + function test_DeployableCertManagerDisablesUnhintedEntrypoints() public { vm.expectRevert("use hinted cert verification"); - hintedCertManager.verifyCACert("", bytes32(0)); + certManager.verifyCACert("", bytes32(0)); vm.expectRevert("use hinted cert verification"); - hintedCertManager.verifyClientCert("", bytes32(0)); + certManager.verifyClientCert("", bytes32(0)); + + vm.expectRevert("use hinted attestation verification"); + validator.validateAttestation("", ""); } - function test_010_OffchainWitnessGeneratorMatchesSolidityCollector() public { + function test_OffchainWitnessGeneratorMatchesSolidityCollector() public { if (!vm.envOr("NITRO_RUN_FFI", false)) { - console.log("==== 010 OFFCHAIN WITNESS GENERATOR ===="); - console.log("skipped; rerun with NITRO_RUN_FFI=true forge test --ffi --match-test test_010"); + console.log("==== OFFCHAIN WITNESS GENERATOR ===="); + console.log("skipped; rerun with NITRO_RUN_FFI=true forge test --ffi --match-test test_Offchain"); return; } @@ -323,7 +185,7 @@ contract RealAttestationBenchTest is Test { bytes memory rootCert = attestationTbs.slice(ptrs.cabundle[0]); bytes32 parentHash = keccak256(rootCert); - bytes memory parentPubKey = hintedCertManager.loadVerified(parentHash).pubKey; + bytes memory parentPubKey = certManager.loadVerified(parentHash).pubKey; uint256 signaturesChecked; for (uint256 i = 1; i < ptrs.cabundle.length; ++i) { @@ -332,8 +194,8 @@ contract RealAttestationBenchTest is Test { bytes memory offchainHints = _ffiCertSignatureHints(caCert, parentPubKey); assertEq(offchainHints, expectedHints, "offchain CA cert hints mismatch"); - parentHash = hintedCertManager.verifyCACertWithHints(caCert, parentHash, offchainHints); - parentPubKey = hintedCertManager.loadVerified(parentHash).pubKey; + parentHash = certManager.verifyCACertWithHints(caCert, parentHash, offchainHints); + parentPubKey = certManager.loadVerified(parentHash).pubKey; signaturesChecked += 1; } @@ -342,22 +204,80 @@ contract RealAttestationBenchTest is Test { bytes memory offchainClientHints = _ffiCertSignatureHints(clientCert, parentPubKey); assertEq(offchainClientHints, expectedClientHints, "offchain client cert hints mismatch"); ICertManager.VerifiedCert memory leaf = - hintedCertManager.verifyClientCertWithHints(clientCert, parentHash, offchainClientHints); + certManager.verifyClientCertWithHints(clientCert, parentHash, offchainClientHints); signaturesChecked += 1; _assertOffchainAttestationHints(attestation, attestationTbs, signature, leaf.pubKey); signaturesChecked += 1; - console.log("==== 010 OFFCHAIN WITNESS GENERATOR ===="); + console.log("==== OFFCHAIN WITNESS GENERATOR ===="); console.log("signatures checked :", signaturesChecked); } - function test_007_FullColdAndWarmHintedSequence() public { + function test_OffchainWitnessGeneratorRejectsMalformedInputs() public { + if (!vm.envOr("NITRO_RUN_FFI", false)) { + console.log("==== OFFCHAIN WITNESS NEGATIVES ===="); + console.log("skipped; rerun with NITRO_RUN_FFI=true forge test --ffi --match-test test_Offchain"); + return; + } + + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); + ICertManager.VerifiedCert memory leaf = _cacheCertBundleWithHints(attestationTbs); + bytes memory hash = Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length); + + _assertFfiVerifyRejects(hash, _mutateAt(signature, 0), leaf.pubKey); + _assertFfiVerifyRejects(hash, signature, _mutateAt(leaf.pubKey, 0)); + _assertFfiCertRejects(hex"00", leaf.pubKey); + _assertFfiAttestationRejects(hex"00", leaf.pubKey); + + console.log("==== OFFCHAIN WITNESS NEGATIVES ===="); + console.log("malformed CLI cases checked :", uint256(4)); + } + + function test_DemoExpiryGraceAllowsOldFixtureAtBaseSepoliaTime() public { + vm.warp(1780458582); // 2026-06-03T03:49:42Z, matching the Base Sepolia demo. + + CertManagerDemo demoCertManager = new CertManagerDemo(p384Verifier, 365 days); + NitroValidator demoValidator = new NitroValidator(demoCertManager, p384Verifier); + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); - console.log("==== 007 FULL COLD + WARM HINTED SEQUENCE ===="); + bytes memory rootCert = attestationTbs.slice(ptrs.cabundle[0]); + bytes32 parentHash = keccak256(rootCert); + ICertManager.VerifiedCert memory parent = demoCertManager.loadVerified(parentHash); + assertTrue(parent.pubKey.length > 0, "root must be pinned"); + + for (uint256 i = 1; i < ptrs.cabundle.length; ++i) { + bytes memory caCert = attestationTbs.slice(ptrs.cabundle[i]); + bytes memory hints = hintCollector.collectCertSignatureHints(caCert, parent.pubKey); + parentHash = demoCertManager.verifyCACertWithHints(caCert, parentHash, hints); + parent = demoCertManager.loadVerified(parentHash); + } + + bytes memory clientCert = attestationTbs.slice(ptrs.cert); + bytes memory clientHints = hintCollector.collectCertSignatureHints(clientCert, parent.pubKey); + ICertManager.VerifiedCert memory leaf = + demoCertManager.verifyClientCertWithHints(clientCert, parentHash, clientHints); + + bytes memory attestationHints = hintCollector.collectVerifyHints( + Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length), signature, leaf.pubKey + ); + demoValidator.validateAttestationWithHints(attestationTbs, signature, attestationHints); + + console.log("==== DEMO EXPIRY GRACE ===="); + console.log("base sepolia timestamp :", block.timestamp); + console.log("demo expiry grace seconds :", uint256(365 days)); + } + + function test_FullColdAndWarmHintedSequence() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + + console.log("==== FULL COLD + WARM HINTED SEQUENCE ===="); console.log("cabundle certs :", ptrs.cabundle.length); console.log("minimum cold tx count :", ptrs.cabundle.length + 1); console.log("warm-cache tx count :", uint256(1)); @@ -432,10 +352,6 @@ contract RealAttestationBenchTest is Test { revert("bad base64 char"); } - function _projectOneHintedP384(uint256 currentGas) internal pure returns (uint256) { - return currentGas - P384_VERIFY_2565 + HINTED_P384_VERIFY_7883_WITH_CALLDATA; - } - function _projectHintedMeasured(uint256 currentHintedGas, uint256 hintBytes, uint256 hintedOtherCalls) internal pure @@ -456,7 +372,7 @@ contract RealAttestationBenchTest is Test { caCert = attestationTbs.slice(ptrs.cabundle[1]); bytes memory rootCert = attestationTbs.slice(ptrs.cabundle[0]); parentHash = keccak256(rootCert); - parentPubKey = hintedCertManager.loadVerified(parentHash).pubKey; + parentPubKey = certManager.loadVerified(parentHash).pubKey; } function _runColdCertCacheSequence(bytes memory attestationTbs, NitroValidator.Ptrs memory ptrs) @@ -465,7 +381,7 @@ contract RealAttestationBenchTest is Test { { bytes memory rootCert = attestationTbs.slice(ptrs.cabundle[0]); bytes32 parentHash = keccak256(rootCert); - ICertManager.VerifiedCert memory parent = hintedCertManager.loadVerified(parentHash); + ICertManager.VerifiedCert memory parent = certManager.loadVerified(parentHash); assertTrue(parent.pubKey.length > 0, "root must already be cached"); assertTrue(parent.ca, "root must be cached as CA"); console.log("root cert hash pinned :"); @@ -478,9 +394,9 @@ contract RealAttestationBenchTest is Test { hintCollector.collectCertSignatureProfile(caCert, parent.pubKey); g0 = gasleft(); - parentHash = hintedCertManager.verifyCACertWithHints(caCert, parentHash, hints); + parentHash = certManager.verifyCACertWithHints(caCert, parentHash, hints); uint256 currentGas = g0 - gasleft(); - parent = hintedCertManager.loadVerified(parentHash); + parent = certManager.loadVerified(parentHash); assertTrue(parent.pubKey.length > 0, "CA cert must be cached"); assertTrue(parent.ca, "CA cert must be cached as CA"); @@ -495,11 +411,11 @@ contract RealAttestationBenchTest is Test { hintCollector.collectCertSignatureProfile(clientCert, parent.pubKey); g0 = gasleft(); - summary.leaf = hintedCertManager.verifyClientCertWithHints(clientCert, parentHash, clientHints); + summary.leaf = certManager.verifyClientCertWithHints(clientCert, parentHash, clientHints); uint256 clientCurrentGas = g0 - gasleft(); bytes32 leafHash = keccak256(clientCert); - ICertManager.VerifiedCert memory cachedLeaf = hintedCertManager.loadVerified(leafHash); + ICertManager.VerifiedCert memory cachedLeaf = certManager.loadVerified(leafHash); assertTrue(cachedLeaf.pubKey.length > 0, "client cert must be cached"); assertFalse(cachedLeaf.ca, "client cert must be cached as client"); @@ -527,7 +443,7 @@ contract RealAttestationBenchTest is Test { bytes memory attestationHints ) internal returns (TxGas memory txGas) { uint256 g0 = gasleft(); - hintedValidator.validateAttestationWithHints(attestationTbs, signature, attestationHints); + validator.validateAttestationWithHints(attestationTbs, signature, attestationHints); txGas.currentGas = g0 - gasleft(); txGas.projectedGas = _projectHintedMeasured(txGas.currentGas, hintBytes, hintedOtherCalls); _logSequenceTx(txIndex, label, txGas.currentGas, hintBytes, hintedOtherCalls, txGas.projectedGas); @@ -574,22 +490,22 @@ contract RealAttestationBenchTest is Test { bytes memory caCert = attestationTbs.slice(ptrs.cabundle[i]); bytes memory hints; if (i != 0) { - parentPubKey = hintedCertManager.loadVerified(parentHash).pubKey; + parentPubKey = certManager.loadVerified(parentHash).pubKey; hints = hintCollector.collectCertSignatureHints(caCert, parentPubKey); } - parentHash = hintedCertManager.verifyCACertWithHints(caCert, parentHash, hints); + parentHash = certManager.verifyCACertWithHints(caCert, parentHash, hints); } bytes memory clientCert = attestationTbs.slice(ptrs.cert); - parentPubKey = hintedCertManager.loadVerified(parentHash).pubKey; + parentPubKey = certManager.loadVerified(parentHash).pubKey; bytes memory clientHints = hintCollector.collectCertSignatureHints(clientCert, parentPubKey); - leaf = hintedCertManager.verifyClientCertWithHints(clientCert, parentHash, clientHints); + leaf = certManager.verifyClientCertWithHints(clientCert, parentHash, clientHints); } function _ffiCertSignatureHints(bytes memory cert, bytes memory parentPubKey) internal returns (bytes memory) { string[] memory command = new string[](7); command[0] = "node"; - command[1] = string.concat(vm.projectRoot(), "/bench/p384_hints.js"); + command[1] = string.concat(vm.projectRoot(), "/tools/p384_hints.js"); command[2] = "cert"; command[3] = "--cert"; command[4] = vm.toString(cert); @@ -604,7 +520,7 @@ contract RealAttestationBenchTest is Test { { string[] memory command = new string[](9); command[0] = "node"; - command[1] = string.concat(vm.projectRoot(), "/bench/p384_hints.js"); + command[1] = string.concat(vm.projectRoot(), "/tools/p384_hints.js"); command[2] = "verify"; command[3] = "--hash"; command[4] = vm.toString(hash); @@ -629,14 +545,14 @@ contract RealAttestationBenchTest is Test { bytes memory offchainCoseHints = _ffiAttestationHints(attestation, pubKey); assertEq(offchainCoseHints, expectedHints, "offchain COSE attestation hints mismatch"); - hintedValidator.validateAttestationWithHints(attestationTbs, signature, offchainHints); + validator.validateAttestationWithHints(attestationTbs, signature, offchainHints); console.log("final attestation hint bytes :", offchainHints.length); } function _ffiAttestationHints(bytes memory attestation, bytes memory pubKey) internal returns (bytes memory) { string[] memory command = new string[](7); command[0] = "node"; - command[1] = string.concat(vm.projectRoot(), "/bench/p384_hints.js"); + command[1] = string.concat(vm.projectRoot(), "/tools/p384_hints.js"); command[2] = "attestation"; command[3] = "--attestation"; command[4] = vm.toString(attestation); @@ -645,6 +561,44 @@ contract RealAttestationBenchTest is Test { return vm.ffi(command); } + function _assertFfiVerifyRejects(bytes memory hash, bytes memory signature, bytes memory pubKey) internal { + vm.expectRevert(); + this.ffiVerifyHintsForTest(hash, signature, pubKey); + } + + function _assertFfiCertRejects(bytes memory cert, bytes memory pubKey) internal { + vm.expectRevert(); + this.ffiCertSignatureHintsForTest(cert, pubKey); + } + + function _assertFfiAttestationRejects(bytes memory attestation, bytes memory pubKey) internal { + vm.expectRevert(); + this.ffiAttestationHintsForTest(attestation, pubKey); + } + + function ffiVerifyHintsForTest(bytes memory hash, bytes memory signature, bytes memory pubKey) + external + returns (bytes memory) + { + return _ffiVerifyHints(hash, signature, pubKey); + } + + function ffiCertSignatureHintsForTest(bytes memory cert, bytes memory pubKey) external returns (bytes memory) { + return _ffiCertSignatureHints(cert, pubKey); + } + + function ffiAttestationHintsForTest(bytes memory attestation, bytes memory pubKey) external returns (bytes memory) { + return _ffiAttestationHints(attestation, pubKey); + } + + function _mutateAt(bytes memory input, uint256 index) internal pure returns (bytes memory output) { + output = new bytes(input.length); + for (uint256 i = 0; i < input.length; ++i) { + output[i] = input[i]; + } + output[index] = bytes1(uint8(output[index]) ^ 1); + } + function _repairMissingPublicKeyBytes(bytes memory attestation) internal pure returns (bytes memory repaired) { // The pasted Base64 sample is missing "ic_" in the CBOR key // "public_key", but the key length and outer COSE payload length still diff --git a/tools/hinted_attestation_calls.js b/tools/hinted_attestation_calls.js new file mode 100644 index 0000000..c5c5405 --- /dev/null +++ b/tools/hinted_attestation_calls.js @@ -0,0 +1,410 @@ +#!/usr/bin/env node +"use strict"; + +const { + collectAttestationHintBytes, + collectCertSignatureHintBytes, + parseAttestationPayload, + parseAttestationSignature, + parseCertPublicKey, + readBytes, +} = require("./p384_hints"); +const { + realFixture, + repairMissingPublicKeyBytes, +} = require("./nitro_attestation_input"); + +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const RATE_BYTES = 136; +const MASK_64 = (1n << 64n) - 1n; + +if (require.main === module) { + main(); +} + +function main() { + try { + const { command, options } = parseArgs(process.argv.slice(2)); + let attestation; + + if (command === "fixture") { + attestation = repairMissingPublicKeyBytes(realFixture()); + } else if (command === "prepare") { + attestation = readBytes(options, "attestation"); + if (options.repair === "true") { + attestation = repairMissingPublicKeyBytes(attestation); + } + } else { + usage(); + process.exit(2); + } + + const plan = prepareHintedAttestationCalls(attestation, { + certManager: options["cert-manager"] || null, + validator: options.validator || null, + }); + + process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`); + } catch (err) { + process.stderr.write(`${err.stack || err.message}\n`); + process.exit(1); + } +} + +function usage() { + process.stderr.write(`Usage: + node tools/hinted_attestation_calls.js fixture [--cert-manager
] [--validator
] + node tools/hinted_attestation_calls.js prepare --attestation [--repair true] [--cert-manager
] [--validator
] + +Outputs a JSON transaction plan for the hinted Nitro flow. Each item includes +the target contract, function signature, ABI arguments, packed inverse hints, +and ready-to-submit calldata. +`); +} + +function parseArgs(args) { + const command = args[0] || ""; + const options = {}; + + for (let i = 1; i < args.length; i += 2) { + const key = args[i]; + const value = args[i + 1]; + if (!key || !key.startsWith("--") || value === undefined) { + usage(); + process.exit(2); + } + options[key.slice(2)] = value; + } + + return { command, options }; +} + +function prepareHintedAttestationCalls(attestation, addresses = {}) { + const certManager = normalizeOptionalAddress(addresses.certManager); + const validator = normalizeOptionalAddress(addresses.validator); + const parsed = parseAttestationSignature(attestation); + const payload = parseAttestationPayload(attestation); + + if (payload.cabundle.length < 2) { + throw new Error("attestation cabundle must include root plus at least one non-root CA"); + } + + const rootCert = payload.cabundle[0]; + const rootHash = keccak256Hex(rootCert); + let parentHash = rootHash; + let parentPubKey = parseCertPublicKey(rootCert); + const cold = []; + + for (let i = 1; i < payload.cabundle.length; ++i) { + const cert = payload.cabundle[i]; + const hints = collectCertSignatureHintBytes(cert, parentPubKey); + const certHash = keccak256Hex(cert); + cold.push(certTx({ + tx: cold.length + 1, + label: caLabel(i), + kind: "cache_ca", + to: certManager, + functionName: "verifyCACertWithHints", + signature: "verifyCACertWithHints(bytes,bytes32,bytes)", + cert, + certHash, + parentCertHash: parentHash, + hints, + })); + parentHash = certHash; + parentPubKey = parseCertPublicKey(cert); + } + + const leafHints = collectCertSignatureHintBytes(payload.certificate, parentPubKey); + const leafHash = keccak256Hex(payload.certificate); + cold.push(certTx({ + tx: cold.length + 1, + label: "client / leaf cert", + kind: "cache_leaf", + to: certManager, + functionName: "verifyClientCertWithHints", + signature: "verifyClientCertWithHints(bytes,bytes32,bytes)", + cert: payload.certificate, + certHash: leafHash, + parentCertHash: parentHash, + hints: leafHints, + })); + + const leafPubKey = parseCertPublicKey(payload.certificate); + const attestationHints = collectAttestationHintBytes(attestation, leafPubKey); + const finalTx = attestationTx({ + tx: cold.length + 1, + to: validator, + attestationTbs: parsed.attestationTbs, + signature: parsed.signature, + hints: attestationHints, + }); + cold.push(finalTx); + + return { + schema: "nitro-validator.hinted-attestation-calls.v1", + contracts: { + certManager, + validator, + }, + root: { + label: "pinned AWS Nitro root CA", + certHash: rootHash, + certBytes: hex(rootCert), + transaction: null, + }, + leaf: { + certHash: leafHash, + pubKey: hex(leafPubKey), + }, + cold, + warm: [ + { + ...finalTx, + tx: 1, + label: "validate Nitro attestation document (warm cache)", + }, + ], + }; +} + +function certTx({ tx, label, kind, to, functionName, signature, cert, certHash, parentCertHash, hints }) { + return { + tx, + kind, + label, + to, + function: signature, + certHash, + parentCertHash, + hintBytes: hints.length, + hintCount: hints.length / 48, + args: { + cert: hex(cert), + parentCertHash, + signatureHints: hex(hints), + }, + calldata: encodeCall(signature, [ + { type: "bytes", value: cert }, + { type: "bytes32", value: parentCertHash }, + { type: "bytes", value: hints }, + ]), + notes: `${functionName} caches this certificate if the parent is already cached and unexpired.`, + }; +} + +function attestationTx({ tx, to, attestationTbs, signature, hints }) { + return { + tx, + kind: "validate_attestation", + label: "validate Nitro attestation document", + to, + function: "validateAttestationWithHints(bytes,bytes,bytes)", + hintBytes: hints.length, + hintCount: hints.length / 48, + args: { + attestationTbs: hex(attestationTbs), + signature: hex(signature), + attestationHints: hex(hints), + }, + calldata: encodeCall("validateAttestationWithHints(bytes,bytes,bytes)", [ + { type: "bytes", value: attestationTbs }, + { type: "bytes", value: signature }, + { type: "bytes", value: hints }, + ]), + notes: "Requires the cabundle and leaf certificate hashes embedded in attestationTbs to already be cached.", + }; +} + +function caLabel(index) { + if (index === 1) return "regional CA"; + if (index === 2) return "zonal CA"; + if (index === 3) return "issuer / instance CA"; + return `non-root CA ${index}`; +} + +function normalizeOptionalAddress(address) { + if (!address) { + return null; + } + if (!/^0x[0-9a-fA-F]{40}$/.test(address)) { + throw new Error(`invalid address: ${address}`); + } + if (address.toLowerCase() === ZERO_ADDRESS) { + return ZERO_ADDRESS; + } + return `0x${address.slice(2).toLowerCase()}`; +} + +function encodeCall(signature, params) { + const selector = keccak256(Buffer.from(signature, "ascii")).subarray(0, 4); + return hex(Buffer.concat([selector, encodeParams(params)])); +} + +function encodeParams(params) { + const head = []; + const tail = []; + let tailOffset = BigInt(params.length * 32); + + for (const param of params) { + if (param.type === "bytes") { + const encoded = encodeBytes(param.value); + head.push(encodeUint256(tailOffset)); + tail.push(encoded); + tailOffset += BigInt(encoded.length); + } else if (param.type === "bytes32") { + head.push(decodeFixedHex(param.value, 32)); + } else { + throw new Error(`unsupported ABI type ${param.type}`); + } + } + + return Buffer.concat([...head, ...tail]); +} + +function encodeBytes(value) { + const bytes = Buffer.from(value); + const padding = (32 - (bytes.length % 32)) % 32; + return Buffer.concat([encodeUint256(BigInt(bytes.length)), bytes, Buffer.alloc(padding)]); +} + +function encodeUint256(value) { + return decodeFixedHex(value.toString(16).padStart(64, "0"), 32); +} + +function decodeFixedHex(value, length) { + const raw = value.startsWith("0x") || value.startsWith("0X") ? value.slice(2) : value; + if (raw.length !== length * 2 || /[^0-9a-f]/i.test(raw)) { + throw new Error(`expected ${length}-byte hex value`); + } + return Buffer.from(raw, "hex"); +} + +function keccak256Hex(bytes) { + return hex(keccak256(bytes)); +} + +function keccak256(bytes) { + const state = Array(25).fill(0n); + let offset = 0; + const input = Buffer.from(bytes); + + while (offset + RATE_BYTES <= input.length) { + absorbBlock(state, input.subarray(offset, offset + RATE_BYTES)); + keccakF1600(state); + offset += RATE_BYTES; + } + + const finalBlock = Buffer.alloc(RATE_BYTES); + input.copy(finalBlock, 0, offset); + finalBlock[input.length - offset] = 0x01; + finalBlock[RATE_BYTES - 1] |= 0x80; + absorbBlock(state, finalBlock); + keccakF1600(state); + + return squeeze(state, 32); +} + +function absorbBlock(state, block) { + for (let i = 0; i < RATE_BYTES / 8; ++i) { + state[i] ^= readLaneLE(block, i * 8); + } +} + +function squeeze(state, length) { + const out = Buffer.alloc(length); + let written = 0; + for (let i = 0; written < length; ++i) { + const lane = writeLaneLE(state[i]); + const take = Math.min(8, length - written); + lane.copy(out, written, 0, take); + written += take; + } + return out; +} + +function keccakF1600(state) { + const rounds = [ + 0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n, + 0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n, + 0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an, + 0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n, + 0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an, + 0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n, + ]; + const rho = [ + [0, 36, 3, 41, 18], + [1, 44, 10, 45, 2], + [62, 6, 43, 15, 61], + [28, 55, 25, 21, 56], + [27, 20, 39, 8, 14], + ]; + + for (const rc of rounds) { + const c = Array(5); + const d = Array(5); + const b = Array(25); + + for (let x = 0; x < 5; ++x) { + c[x] = state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20]; + } + for (let x = 0; x < 5; ++x) { + d[x] = c[(x + 4) % 5] ^ rotl64(c[(x + 1) % 5], 1); + } + for (let x = 0; x < 5; ++x) { + for (let y = 0; y < 5; ++y) { + state[x + 5 * y] = (state[x + 5 * y] ^ d[x]) & MASK_64; + } + } + + for (let x = 0; x < 5; ++x) { + for (let y = 0; y < 5; ++y) { + b[y + 5 * ((2 * x + 3 * y) % 5)] = rotl64(state[x + 5 * y], rho[x][y]); + } + } + + for (let x = 0; x < 5; ++x) { + for (let y = 0; y < 5; ++y) { + state[x + 5 * y] = (b[x + 5 * y] ^ ((~b[((x + 1) % 5) + 5 * y]) & b[((x + 2) % 5) + 5 * y])) & MASK_64; + } + } + + state[0] = (state[0] ^ rc) & MASK_64; + } +} + +function rotl64(value, shift) { + const s = BigInt(shift); + if (s === 0n) { + return value & MASK_64; + } + return ((value << s) | (value >> (64n - s))) & MASK_64; +} + +function readLaneLE(buffer, offset) { + let value = 0n; + for (let i = 7; i >= 0; --i) { + value = (value << 8n) | BigInt(buffer[offset + i]); + } + return value; +} + +function writeLaneLE(value) { + const buffer = Buffer.alloc(8); + let lane = value; + for (let i = 0; i < 8; ++i) { + buffer[i] = Number(lane & 0xffn); + lane >>= 8n; + } + return buffer; +} + +function hex(bytes) { + return `0x${Buffer.from(bytes).toString("hex")}`; +} + +module.exports = { + encodeCall, + keccak256, + prepareHintedAttestationCalls, +}; diff --git a/tools/nitro_attestation_input.js b/tools/nitro_attestation_input.js new file mode 100644 index 0000000..c3b34d7 --- /dev/null +++ b/tools/nitro_attestation_input.js @@ -0,0 +1,123 @@ +#!/usr/bin/env node +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +if (require.main === module) { + main(); +} + +function main() { + try { + const { command, options } = parseArgs(process.argv.slice(2)); + let attestation; + + if (command === "fixture") { + attestation = realFixture(); + } else if (command === "read") { + attestation = decodeBytes(requireOption(options, "input")); + } else { + usage(); + process.exit(2); + } + + if (options.repair === "true" || command === "fixture") { + attestation = repairMissingPublicKeyBytes(attestation); + } + + process.stdout.write(`0x${attestation.toString("hex")}`); + } catch (err) { + process.stderr.write(`${err.stack || err.message}\n`); + process.exit(1); + } +} + +function usage() { + process.stderr.write(`Usage: + node tools/nitro_attestation_input.js fixture + node tools/nitro_attestation_input.js read --input [--repair true] + +The fixture mode extracts the January 2026 real attestation from +test/hinted/HintedNitroAttestation.t.sol and applies the documented public_key +repair used by the happy-path tests. +`); +} + +function parseArgs(args) { + const command = args[0] || ""; + const options = {}; + + for (let i = 1; i < args.length; i += 2) { + const key = args[i]; + const value = args[i + 1]; + if (!key || !key.startsWith("--") || value === undefined) { + usage(); + process.exit(2); + } + options[key.slice(2)] = value; + } + + return { command, options }; +} + +function requireOption(options, name) { + if (options[name] === undefined) { + throw new Error(`missing --${name}`); + } + return options[name]; +} + +function decodeBytes(value) { + let raw = value; + if (raw.startsWith("@")) { + raw = fs.readFileSync(raw.slice(1), "utf8").trim(); + } + + if (raw.startsWith("0x") || raw.startsWith("0X")) { + const hex = raw.slice(2); + if (hex.length % 2 !== 0 || /[^0-9a-f]/i.test(hex)) { + throw new Error("invalid hex input"); + } + return Buffer.from(hex, "hex"); + } + + if (/^[0-9a-f]+$/i.test(raw) && raw.length % 2 === 0) { + return Buffer.from(raw, "hex"); + } + + return Buffer.from(raw, "base64"); +} + +function realFixture() { + const testPath = path.join(process.cwd(), "test", "hinted", "HintedNitroAttestation.t.sol"); + const source = fs.readFileSync(testPath, "utf8"); + const match = source.match(/function _realAttestationB64\(\)[\s\S]*?return "([^"]+)";/); + if (!match) { + throw new Error("could not find _realAttestationB64 fixture"); + } + return Buffer.from(match[1], "base64"); +} + +function repairMissingPublicKeyBytes(attestation) { + const insertAt = 4338; + const expected = Buffer.from("7075626c6b6579", "hex"); + if ( + attestation.length <= insertAt + 2 || + !attestation.subarray(insertAt - 4, insertAt + 3).equals(expected) + ) { + throw new Error("unexpected fixture public_key corruption"); + } + + return Buffer.concat([ + attestation.subarray(0, insertAt), + Buffer.from("69635f", "hex"), + attestation.subarray(insertAt), + ]); +} + +module.exports = { + decodeBytes, + realFixture, + repairMissingPublicKeyBytes, +}; diff --git a/bench/p384_hints.js b/tools/p384_hints.js similarity index 76% rename from bench/p384_hints.js rename to tools/p384_hints.js index 4eb7b4b..2945d9b 100644 --- a/bench/p384_hints.js +++ b/tools/p384_hints.js @@ -13,7 +13,9 @@ const GX = hexToBigInt("aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e0 const GY = hexToBigInt("3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f"); const MASK_256 = (1n << 256n) - 1n; -main(); +if (require.main === module) { + main(); +} function main() { try { @@ -50,9 +52,9 @@ function main() { function usage() { process.stderr.write(`Usage: - node bench/p384_hints.js verify --hash --signature --pubkey - node bench/p384_hints.js cert --cert --pubkey - node bench/p384_hints.js attestation --attestation --pubkey + node tools/p384_hints.js verify --hash --signature --pubkey + node tools/p384_hints.js cert --cert --pubkey + node tools/p384_hints.js attestation --attestation --pubkey Inputs may be 0x-prefixed hex, base64, or @path. Output is a 0x-prefixed packed stream of 48-byte big-endian inverse hints. @@ -145,6 +147,20 @@ function collectVerifyHints(hashBytes, signatureBytes, pubKeyBytes) { return ctx.hints; } +function collectVerifyHintBytes(hashBytes, signatureBytes, pubKeyBytes) { + return Buffer.concat(collectVerifyHints(hashBytes, signatureBytes, pubKeyBytes)); +} + +function collectCertSignatureHintBytes(cert, parentPubKey) { + const { hash, signature } = parseCertSignature(cert); + return collectVerifyHintBytes(hash, signature, parentPubKey); +} + +function collectAttestationHintBytes(attestation, leafPubKey) { + const { hash, signature } = parseAttestationSignature(attestation); + return collectVerifyHintBytes(hash, signature, leafPubKey); +} + function isOnCurve(x, y) { if (x === 0n || x === P || y === 0n || y === P) { return false; @@ -428,6 +444,38 @@ function parseCertSignature(cert) { return { hash, signature: Buffer.concat([r, s]) }; } +function parseCertPublicKey(cert) { + const root = readAsn1(cert, 0); + requireTag(root, 0x30, "certificate"); + + const tbs = readAsn1(cert, root.contentStart); + requireTag(tbs, 0x30, "TBS certificate"); + + let ptr = readAsn1(cert, tbs.contentStart); + if (ptr.tag === 0xa0) { + ptr = readAsn1(cert, ptr.end); // serial number + } + + const serial = ptr; + const signatureAlgorithm = readAsn1(cert, serial.end); + const issuer = readAsn1(cert, signatureAlgorithm.end); + const validity = readAsn1(cert, issuer.end); + const subject = readAsn1(cert, validity.end); + const subjectPublicKeyInfo = readAsn1(cert, subject.end); + + const algorithm = readAsn1(cert, subjectPublicKeyInfo.contentStart); + const subjectPublicKey = readAsn1(cert, algorithm.end); + requireTag(subjectPublicKey, 0x03, "subject public key bit string"); + if (cert[subjectPublicKey.contentStart] !== 0x00) { + throw new Error("unsupported nonzero public key unused-bits count"); + } + if (subjectPublicKey.contentEnd - subjectPublicKey.contentStart < 97) { + throw new Error("subject public key is too short"); + } + + return cert.subarray(subjectPublicKey.contentEnd - 96, subjectPublicKey.contentEnd); +} + function parseAsn1Integer(bytes) { let value = bytes; while (value.length > 0 && value[0] === 0x00) { @@ -514,9 +562,56 @@ function parseAttestationSignature(attestation) { return { hash: crypto.createHash("sha384").update(attestationTbs).digest(), signature: attestation.subarray(signaturePtr.contentStart, signaturePtr.end), + attestationTbs, + payload: attestation.subarray(payloadPtr.contentStart, payloadPtr.end), }; } +function parseAttestationPayload(attestation) { + const { payload } = parseAttestationSignature(attestation); + const root = readCborItem(payload, 0); + requireCborMajor(root, 5, "attestation payload"); + + const result = {}; + let offset = root.contentStart; + let itemCount = 0n; + while (root.indefinite ? payload[offset] !== 0xff : itemCount < root.value) { + const key = readCborItem(payload, offset); + requireCborMajor(key, 3, "attestation payload key"); + const keyName = payload.subarray(key.contentStart, key.end).toString("utf8"); + const value = readCborItem(payload, key.end); + + if (keyName === "certificate") { + requireCborMajor(value, 2, "certificate"); + result.certificate = payload.subarray(value.contentStart, value.end); + } else if (keyName === "cabundle") { + requireCborMajor(value, 4, "cabundle"); + result.cabundle = []; + let itemOffset = value.contentStart; + let cabundleCount = 0n; + while (value.indefinite ? payload[itemOffset] !== 0xff : cabundleCount < value.value) { + const item = readCborItem(payload, itemOffset); + requireCborMajor(item, 2, "cabundle certificate"); + result.cabundle.push(payload.subarray(item.contentStart, item.end)); + itemOffset = item.end; + cabundleCount++; + } + } + + offset = value.end; + itemCount++; + } + + if (!result.certificate) { + throw new Error("attestation payload missing certificate"); + } + if (!result.cabundle || result.cabundle.length === 0) { + throw new Error("attestation payload missing cabundle"); + } + + return result; +} + function readCborItem(bytes, start) { const initial = bytes[start]; if (initial === undefined) { @@ -524,23 +619,51 @@ function readCborItem(bytes, start) { } const major = initial >> 5; const ai = initial & 0x1f; - const { value, headerLength } = readCborValue(bytes, start + 1, ai); + const { value, headerLength, indefinite } = readCborValue(bytes, start + 1, ai); const contentStart = start + headerLength; let end; if (major === 2 || major === 3) { + if (indefinite) { + throw new Error("unsupported indefinite CBOR byte/text string"); + } end = contentStart + Number(value); } else if (major === 4) { end = contentStart; - for (let i = 0n; i < value; ++i) { - end = readCborItem(bytes, end).end; + if (indefinite) { + while (bytes[end] !== 0xff) { + if (end >= bytes.length) { + throw new Error("indefinite CBOR array missing break"); + } + end = readCborItem(bytes, end).end; + } + end++; + } else { + for (let i = 0n; i < value; ++i) { + end = readCborItem(bytes, end).end; + } } } else if (major === 5) { end = contentStart; - for (let i = 0n; i < value * 2n; ++i) { - end = readCborItem(bytes, end).end; + if (indefinite) { + while (bytes[end] !== 0xff) { + if (end >= bytes.length) { + throw new Error("indefinite CBOR map missing break"); + } + const key = readCborItem(bytes, end); + const mapValue = readCborItem(bytes, key.end); + end = mapValue.end; + } + end++; + } else { + for (let i = 0n; i < value * 2n; ++i) { + end = readCborItem(bytes, end).end; + } } } else if (major === 0 || major === 1 || major === 6 || major === 7) { + if (indefinite) { + throw new Error(`unsupported indefinite CBOR major type ${major}`); + } end = contentStart; } else { throw new Error(`unsupported CBOR major type ${major}`); @@ -550,7 +673,7 @@ function readCborItem(bytes, start) { throw new Error("CBOR item length out of bounds"); } - return { major, ai, value, start, contentStart, end }; + return { major, ai, value, indefinite, start, contentStart, end }; } function readCborValue(bytes, offset, ai) { @@ -569,6 +692,9 @@ function readCborValue(bytes, offset, ai) { if (ai === 27) { return { value: readBigUintBE(bytes, offset, 8), headerLength: 9 }; } + if (ai === 31) { + return { value: 0n, headerLength: 1, indefinite: true }; + } throw new Error(`unsupported CBOR additional information ${ai}`); } @@ -622,3 +748,14 @@ function bigIntToFixedBuffer(value, length) { function hexToBigInt(hex) { return BigInt(`0x${hex}`); } + +module.exports = { + collectAttestationHintBytes, + collectCertSignatureHintBytes, + collectVerifyHintBytes, + parseAttestationPayload, + parseAttestationSignature, + parseCertPublicKey, + parseCertSignature, + readBytes, +}; From ceee719dc60ebb8b37fb31c4a048912441b00bd1 Mon Sep 17 00:00:00 2001 From: Elena Nadolinski Date: Tue, 9 Jun 2026 10:14:06 -0700 Subject: [PATCH 03/10] Make hinted P-384 branch self-contained and review-ready Vendor the hinted ECDSA384 crypto in-tree and drop the personal-fork solidity-lib submodule so the repo builds standalone and the audited code is exactly what deploys: - src/vendor/{ECDSA384,MemoryUtils}.sol copied from dl-solarity@b947571, with provenance README + the upstream hinted diff (ECDSA384.hinted.patch) - repoint imports to relative paths, drop @solarity remapping & submodule - exclude src/vendor from forge fmt to keep it byte-identical to upstream Tighten production hygiene for the human security review: - rewrite README for the hinted flow (deployment order, cold/warm phases, off-chain hint generation, verify-not-trust safety model) - add CI job running the FFI off-chain/Solidity hint parity tests - add negative tests: expired-cert-on-first-verification, notAfter validity boundary, out-of-range scalar rejection (r==0, r>=n, s==0, s>lowSmax) - move CertManagerDemo out of src/ into test/helpers/ - document the warm-cache precondition (NatSpec) on the hinted entrypoints Generated with Claude Code Co-Authored-By: Claude --- .github/workflows/test.yml | 23 + .gitmodules | 4 - README.md | 106 +- foundry.toml | 5 + lib/solidity-lib | 1 - remappings.txt | 1 - script/BaseSepoliaDemo.s.sol | 2 +- src/CertManager.sol | 10 + src/ECDSA384Curve.sol | 2 +- src/NitroValidator.sol | 17 + src/P384Verifier.sol | 2 +- src/vendor/ECDSA384.hinted.patch | 226 ++++ src/vendor/ECDSA384.sol | 1138 +++++++++++++++++ src/vendor/MemoryUtils.sol | 97 ++ src/vendor/README.md | 44 + .../demo => test/helpers}/CertManagerDemo.sol | 4 +- test/helpers/ECDSA384HintCollector.sol | 2 +- test/hinted/HintedNitroAttestation.t.sol | 71 +- 18 files changed, 1722 insertions(+), 33 deletions(-) delete mode 160000 lib/solidity-lib create mode 100644 src/vendor/ECDSA384.hinted.patch create mode 100644 src/vendor/ECDSA384.sol create mode 100644 src/vendor/MemoryUtils.sol create mode 100644 src/vendor/README.md rename {src/demo => test/helpers}/CertManagerDemo.sol (87%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 509dde8..a5ac046 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,3 +43,26 @@ jobs: run: | forge test -vvv id: test + + ffi-parity: + name: Off-chain witness parity (FFI) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + # Asserts the off-chain hint generator (tools/p384_hints.js) produces byte-identical + # hints to the on-chain Solidity reference; guards against the two drifting apart. + # Node.js is preinstalled on ubuntu-latest and the tools use only Node built-ins. + - name: Run FFI parity tests + env: + NITRO_RUN_FFI: "true" + run: | + forge test --ffi -vvv --match-test test_OffchainWitness + id: ffi diff --git a/.gitmodules b/.gitmodules index 1319979..888d42d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,3 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/solidity-lib"] - path = lib/solidity-lib - url = https://github.com/leanthebean/solidity-lib - branch = hinted-p384-inversion diff --git a/README.md b/README.md index 23d383e..c1f7f75 100644 --- a/README.md +++ b/README.md @@ -3,35 +3,102 @@ This repo provides solidity contracts for the verification of attestations generated by AWS Nitro Enclaves, as outlined in [this doc](https://github.com/aws/aws-nitro-enclaves-nsm-api/blob/4b851f3006c6fa98f23dcffb2cba03b39de9b8af/docs/attestation_process.md#3-attestation-document-validation). -Note that it costs around 63m gas to validate an attestation with no prior verified certs. -You can break this up into smaller transactions by verifying each cert in the chain separately. -You can call `CertManager.verifyCACert` for each cert in the attestation `cabundle`. - This library does not currently support certificate revocation, which is disabled in AWS's attestation verification documentation [here](https://github.com/aws/aws-nitro-enclaves-nsm-api/blob/4b851f3006c6fa98f23dcffb2cba03b39de9b8af/docs/attestation_process.md#32-syntactical-validation). +## Hinted P-384 verification + +Nitro attestations are signed with ECDSA over the NIST P-384 curve. Verifying a P-384 signature +on-chain requires modular inversion, which is computed via the `MODEXP` precompile — and the Fusaka +upgrade (EIP-7883, live on Base) raises `MODEXP` pricing enough that the old fully on-chain flow no +longer fits in a block. + +This library therefore uses **hinted verification**: the modular inverses are computed *off-chain* +and supplied in calldata as "hints". The contract does **not** trust them — before using each hint +`inv` for a value `b` modulo `m`, it checks `b · inv ≡ 1 (mod m)` and reverts (`"bad inverse hint"`) +otherwise. Because the moduli are prime the inverse is unique, so a wrong hint can only cause a +revert, never a forged signature. The acceptance rule is identical to a standard on-chain ECDSA-384 +verification — hints are purely a gas optimization. + +The legacy non-hinted entrypoints (`verifyCACert`, `verifyClientCert`, `validateAttestation`) are +retained only as reverting stubs; use the `*WithHints` functions below. + +For the full design, security argument, and measured gas, see +[docs/hinted-p384-nitro-attestation.md](docs/hinted-p384-nitro-attestation.md). + ## Usage -1. Deploy the `CertManager` separately. -2. Validate Nitro attestation in your contract: +### Deployment + +Deploy in this order (the verifier references are immutable): + +1. `P384Verifier` +2. `CertManager(p384Verifier)` — pins the AWS Nitro root CA in its constructor. +3. `NitroValidator(certManager, p384Verifier)` + +### Verification flow + +Verification has two phases. Certificate chains are reused across many attestations from the same +enclave, so the chain is verified and **cached once**, after which each attestation only pays for its +own document signature. + +1. **Cold phase — verify & cache the certificate chain** (once per chain). For each CA cert in the + `cabundle`, then the leaf cert, compute the inverse hints off-chain and submit them: + - `certManager.verifyCACertWithHints(caCert, parentCertHash, hints)` (use `0` as the parent hash + for the cert signed by the pinned root) + - `certManager.verifyClientCertWithHints(leafCert, parentCertHash, hints)` +2. **Validation — verify the document signature.** Once the chain is cached: + - `validator.validateAttestationWithHints(attestationTbs, signature, attestationHints)` + - **Precondition:** the whole `cabundle` + leaf must already be cached from phase 1. This call + re-walks the chain with empty hints (relying on the cache), so an uncached chain reverts with + `"inverse hint underflow"`. + +Splitting `attestation` into `attestationTbs` (to-be-signed bytes) and `signature` is cheapest +off-chain, but `validator.decodeAttestationTbs(attestation)` is available on-chain too. + +### Computing hints off-chain + +`tools/p384_hints.js` is a reference generator (Node.js, no dependencies): + +```sh +node tools/p384_hints.js cert --cert <0x DER cert> --pubkey <0x parent pubkey> +node tools/p384_hints.js attestation --attestation <0x COSE> --pubkey <0x leaf pubkey> +``` + +Production callers should reimplement this in their backend language; the contract re-verifies every +hint, so the generator is trusted only for liveness, never for correctness. + +### Example consumer ```solidity pragma solidity ^0.8.0; import {NitroValidator} from "@nitro-validator/NitroValidator.sol"; -import {CborDecode} from "@nitro-validator/CborDecode.sol"; import {CertManager} from "@nitro-validator/CertManager.sol"; +import {CborDecode} from "@nitro-validator/CborDecode.sol"; -contract Validator is NitroValidator { +contract Validator { using CborDecode for bytes; uint256 public constant MAX_AGE = 60 minutes; bytes32 public constant PCR0 = keccak256("some PCR0 value"); - - constructor(CertManager certManager) NitroValidator(certManager) {} - function registerSigner(bytes calldata attestationTbs, bytes calldata signature) external onlyOwner { - Ptrs memory ptrs = validateAttestation(attestationTbs, signature); + NitroValidator public immutable validator; + + constructor(NitroValidator validator_) { + validator = validator_; + } + + // Assumes the attestation's certificate chain has already been cached on the CertManager via + // verifyCACertWithHints / verifyClientCertWithHints (see the cold phase above). + function registerSigner( + bytes calldata attestationTbs, + bytes calldata signature, + bytes calldata attestationHints // computed off-chain + ) external { + NitroValidator.Ptrs memory ptrs = + validator.validateAttestationWithHints(attestationTbs, signature, attestationHints); + bytes32 pcr0 = attestationTbs.keccak(ptrs.pcrs[0]); require(pcr0 == PCR0, "invalid pcr0 in attestation"); require(ptrs.timestamp + MAX_AGE > block.timestamp, "attestation too old"); @@ -41,18 +108,10 @@ contract Validator is NitroValidator { } } ``` -3. Convert an attestation document to an attestationTbs / signature (TBS means to-be-signed). -Note it's cheaper to perform this conversion offchain (e.g. using `cast call`). -```solidity -bytes memory attestation = hex"84.."; -(bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); -validator.validateAttestation(attestationTbs, signature); -``` ## Build ```sh -forge install forge build ``` @@ -61,3 +120,10 @@ forge build ```sh forge test ``` + +The off-chain witness generator is cross-checked for byte-identical parity against the on-chain +Solidity reference under FFI (requires Node.js): + +```sh +NITRO_RUN_FFI=true forge test --ffi --match-test test_OffchainWitness +``` diff --git a/foundry.toml b/foundry.toml index f3944a3..d4e8489 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,4 +6,9 @@ optimizer = true optimizer_runs = 999999 solc_version = "0.8.26" +[fmt] +# Vendored third-party crypto is kept byte-identical to upstream (see src/vendor/README.md), +# so it is excluded from this repo's formatting rules. +ignore = ["src/vendor/*.sol"] + # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/solidity-lib b/lib/solidity-lib deleted file mode 160000 index d4f594b..0000000 --- a/lib/solidity-lib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d4f594bc81da96121a3f7e652cce17f699f6cf00 diff --git a/remappings.txt b/remappings.txt index 1cfeb32..feaba2d 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1 @@ forge-std/=lib/forge-std/src/ -@solarity/=lib/solidity-lib/contracts/ diff --git a/script/BaseSepoliaDemo.s.sol b/script/BaseSepoliaDemo.s.sol index abf6a48..9e55111 100644 --- a/script/BaseSepoliaDemo.s.sol +++ b/script/BaseSepoliaDemo.s.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.26; import {Script, console2} from "forge-std/Script.sol"; import {CborDecode, CborElement, LibCborElement} from "../src/CborDecode.sol"; -import {CertManagerDemo} from "../src/demo/CertManagerDemo.sol"; +import {CertManagerDemo} from "../test/helpers/CertManagerDemo.sol"; import {ICertManager} from "../src/ICertManager.sol"; import {IP384Verifier} from "../src/IP384Verifier.sol"; import {LibBytes} from "../src/LibBytes.sol"; diff --git a/src/CertManager.sol b/src/CertManager.sol index e230e12..a7d2e87 100644 --- a/src/CertManager.sol +++ b/src/CertManager.sol @@ -72,6 +72,12 @@ contract CertManager is ICertManager { revert("use hinted cert verification"); } + /// @notice Verify a CA certificate against its (already-cached) parent and cache the result. + /// @dev Idempotent with a cache short-circuit: if `cert` is already verified and unexpired, the + /// cached record is returned and `signatureHints` is ignored (so "" is valid on a re-call). + /// On a cold cert, `signatureHints` must contain the real off-chain inverse hints for the + /// cert signature; they are re-verified on-chain, so a wrong hint only reverts. Pass the + /// root's hash (or 0 for the pinned root) as `parentCertHash`. function verifyCACertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) external returns (bytes32) @@ -81,6 +87,10 @@ contract CertManager is ICertManager { return certHash; } + /// @notice Verify a leaf (client) certificate against its (already-cached) parent and cache it. + /// @dev Same cache short-circuit and hint semantics as {verifyCACertWithHints}: on a cold cert + /// `signatureHints` must hold the real off-chain inverse hints (re-verified on-chain); on a + /// cached cert they are ignored. function verifyClientCertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) external returns (VerifiedCert memory) diff --git a/src/ECDSA384Curve.sol b/src/ECDSA384Curve.sol index 948792d..f55b4e2 100644 --- a/src/ECDSA384Curve.sol +++ b/src/ECDSA384Curve.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -import {ECDSA384} from "@solarity/libs/crypto/ECDSA384.sol"; +import {ECDSA384} from "./vendor/ECDSA384.sol"; library ECDSA384Curve { // ECDSA384 curve parameters (NIST P-384) diff --git a/src/NitroValidator.sol b/src/NitroValidator.sol index 0b13ad5..bd8c423 100644 --- a/src/NitroValidator.sol +++ b/src/NitroValidator.sol @@ -77,6 +77,18 @@ contract NitroValidator { revert("use hinted attestation verification"); } + /// @notice Validate a Nitro attestation document, supplying off-chain inverse hints for the + /// final document signature. + /// @dev PRECONDITION: the attestation's entire certificate bundle (every CA cert plus the leaf + /// cert) MUST already be verified and cached, via prior calls to + /// `CertManager.verifyCACertWithHints` / `verifyClientCertWithHints` with real hints. + /// This function re-walks the bundle with EMPTY hints (see `verifyCachedCertBundle`), + /// which only succeeds on already-cached certs. If any cert is uncached it reverts with + /// "inverse hint underflow" — even when `attestationSigHints` itself is valid. + /// @param attestationTbs The COSE Sign1 to-be-signed bytes (from `decodeAttestationTbs`). + /// @param signature The 96-byte (r||s) P-384 attestation signature. + /// @param attestationSigHints Off-chain inverse hints for the attestation signature; re-verified + /// on-chain, so a wrong hint only reverts and can never forge a valid signature. function validateAttestationWithHints( bytes memory attestationTbs, bytes memory signature, @@ -119,6 +131,11 @@ contract NitroValidator { return ptrs; } + /// @dev Re-walks the cert bundle (cabundle + leaf) passing EMPTY hint streams, relying on the + /// CertManager cache short-circuit: an already-verified, unexpired cert returns its cached + /// record without re-checking the signature (and so needs no hints). If a cert is NOT + /// cached, signature verification is attempted against an empty hint stream and reverts + /// with "inverse hint underflow". Callers must therefore pre-cache the whole bundle first. function verifyCachedCertBundle(bytes memory certificate, bytes[] memory cabundle) internal returns (ICertManager.VerifiedCert memory) diff --git a/src/P384Verifier.sol b/src/P384Verifier.sol index 9bed6c3..b3d70c9 100644 --- a/src/P384Verifier.sol +++ b/src/P384Verifier.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.15; import {ECDSA384Curve} from "./ECDSA384Curve.sol"; import {IP384Verifier} from "./IP384Verifier.sol"; -import {ECDSA384} from "@solarity/libs/crypto/ECDSA384.sol"; +import {ECDSA384} from "./vendor/ECDSA384.sol"; contract P384Verifier is IP384Verifier { function verifyP384SignatureWithHints( diff --git a/src/vendor/ECDSA384.hinted.patch b/src/vendor/ECDSA384.hinted.patch new file mode 100644 index 0000000..6d9ae1f --- /dev/null +++ b/src/vendor/ECDSA384.hinted.patch @@ -0,0 +1,226 @@ +diff --git a/contracts/libs/crypto/ECDSA384.sol b/contracts/libs/crypto/ECDSA384.sol +index 9e43756..053803b 100644 +--- a/contracts/libs/crypto/ECDSA384.sol ++++ b/contracts/libs/crypto/ECDSA384.sol +@@ -64,6 +64,40 @@ library ECDSA384 { + bytes memory signature_, + bytes memory pubKey_ + ) internal view returns (bool) { ++ (bool ok_, ) = _verify(curveParams_, hashedMessage_, signature_, pubKey_, "", false); ++ return ok_; ++ } ++ ++ function verifyWithHints( ++ Parameters memory curveParams_, ++ bytes memory hashedMessage_, ++ bytes memory signature_, ++ bytes memory pubKey_, ++ bytes memory inverseHints_ ++ ) internal view returns (bool ok_) { ++ uint256 consumed_; ++ (ok_, consumed_) = _verify(curveParams_, hashedMessage_, signature_, pubKey_, inverseHints_, true); ++ require(consumed_ == inverseHints_.length, "unused inverse hints"); ++ } ++ ++ function verifyWithHintsConsumed( ++ Parameters memory curveParams_, ++ bytes memory hashedMessage_, ++ bytes memory signature_, ++ bytes memory pubKey_, ++ bytes memory inverseHints_ ++ ) internal view returns (bool ok_, uint256 consumed_) { ++ return _verify(curveParams_, hashedMessage_, signature_, pubKey_, inverseHints_, true); ++ } ++ ++ function _verify( ++ Parameters memory curveParams_, ++ bytes memory hashedMessage_, ++ bytes memory signature_, ++ bytes memory pubKey_, ++ bytes memory inverseHints_, ++ bool useHints_ ++ ) private view returns (bool ok_, uint256 consumed_) { + unchecked { + _Inputs memory inputs_; + +@@ -80,7 +114,7 @@ library ECDSA384 { + lowSmax: curveParams_.lowSmax.init() + }); + +- uint256 call = U384.initCall(params_.p); ++ uint256 call = useHints_ ? U384.initCallWithHints(params_.p, inverseHints_) : U384.initCall(params_.p); + + /// accept s only from the lower part of the curve + if ( +@@ -89,11 +123,11 @@ library ECDSA384 { + U384.eqInteger(inputs_.s, 0) || + U384.cmp(inputs_.s, params_.lowSmax) > 0 + ) { +- return false; ++ return (false, U384.hintCursor(call)); + } + + if (!_isOnCurve(call, params_.p, params_.a, params_.b, inputs_.x, inputs_.y)) { +- return false; ++ return (false, U384.hintCursor(call)); + } + + /// allow compatibility with non-384-bit hash functions. +@@ -144,7 +178,7 @@ library ECDSA384 { + + U384.modAssign(call, scalar1, params_.n); + +- return U384.eq(scalar1, inputs_.r); ++ return (U384.eq(scalar1, inputs_.r), U384.hintCursor(call)); + } + } + +@@ -486,11 +520,14 @@ library ECDSA384 { + library U384 { + uint256 private constant SHORT_ALLOCATION = 64; + +- uint256 private constant CALL_ALLOCATION = 4 * 288; +- + uint256 private constant MUL_OFFSET = 288; + uint256 private constant EXP_OFFSET = 2 * 288; + uint256 private constant INV_OFFSET = 3 * 288; ++ uint256 private constant HINT_DATA_OFFSET = 0x480; ++ uint256 private constant HINT_LENGTH_OFFSET = 0x4A0; ++ uint256 private constant HINT_CURSOR_OFFSET = 0x4C0; ++ uint256 private constant HINT_ENABLED_OFFSET = 0x4E0; ++ uint256 private constant CALL_ALLOCATION = 0x500; + + function init(uint256 from_) internal pure returns (uint256 handler_) { + unchecked { +@@ -575,10 +612,32 @@ library U384 { + mstore(add(0x40, call_), 0x40) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) ++ ++ mstore(add(handler_, HINT_DATA_OFFSET), 0) ++ mstore(add(handler_, HINT_LENGTH_OFFSET), 0) ++ mstore(add(handler_, HINT_CURSOR_OFFSET), 0) ++ mstore(add(handler_, HINT_ENABLED_OFFSET), 0) + } + } + } + ++ function initCallWithHints(uint256 m_, bytes memory inverseHints_) internal pure returns (uint256 handler_) { ++ handler_ = initCall(m_); ++ ++ assembly { ++ mstore(add(handler_, HINT_DATA_OFFSET), add(inverseHints_, 0x20)) ++ mstore(add(handler_, HINT_LENGTH_OFFSET), mload(inverseHints_)) ++ mstore(add(handler_, HINT_CURSOR_OFFSET), 0) ++ mstore(add(handler_, HINT_ENABLED_OFFSET), 1) ++ } ++ } ++ ++ function hintCursor(uint256 call_) internal pure returns (uint256 cursor_) { ++ assembly { ++ cursor_ := mload(add(call_, HINT_CURSOR_OFFSET)) ++ } ++ } ++ + function copy(uint256 handler_) internal pure returns (uint256 handlerCopy_) { + unchecked { + handlerCopy_ = _allocate(SHORT_ALLOCATION); +@@ -808,16 +867,28 @@ library U384 { + /// @dev Stores modinv into `b_` and moddiv into `a_`. + function moddivAssign(uint256 call_, uint256 a_, uint256 b_) internal view { + unchecked { +- assembly { +- call_ := add(call_, INV_OFFSET) ++ uint256 baseCall_ = call_; ++ if (_hintsEnabled(call_)) { ++ uint256 inv_ = _nextInverseHint(call_); ++ uint256 check_ = modmul(call_, b_, inv_); ++ require(eqInteger(check_, 1), "bad inverse hint"); ++ ++ assembly { ++ mstore(b_, mload(inv_)) ++ mstore(add(b_, 0x20), mload(add(inv_, 0x20))) ++ } ++ } else { ++ assembly { ++ call_ := add(call_, INV_OFFSET) + +- mstore(add(0x60, call_), mload(b_)) +- mstore(add(0x80, call_), mload(add(b_, 0x20))) ++ mstore(add(0x60, call_), mload(b_)) ++ mstore(add(0x80, call_), mload(add(b_, 0x20))) + +- pop(staticcall(gas(), 0x5, call_, 0x0120, b_, 0x40)) ++ pop(staticcall(gas(), 0x5, call_, 0x0120, b_, 0x40)) ++ } + } + +- modmulAssign(call_ - INV_OFFSET, a_, b_); ++ modmulAssign(baseCall_, a_, b_); + } + } + +@@ -847,6 +918,13 @@ library U384 { + + function modinv(uint256 call_, uint256 b_, uint256 m_) internal view returns (uint256 r_) { + unchecked { ++ if (_hintsEnabled(call_)) { ++ r_ = _nextInverseHint(call_); ++ uint256 check_ = _modmulWithMod(call_, b_, r_, m_); ++ require(eqInteger(check_, 1), "bad inverse hint"); ++ return r_; ++ } ++ + r_ = _allocate(SHORT_ALLOCATION); + + _sub(m_, init(2), call_ + 0xA0); +@@ -865,6 +943,49 @@ library U384 { + } + } + ++ function _hintsEnabled(uint256 call_) private pure returns (bool enabled_) { ++ assembly { ++ enabled_ := mload(add(call_, HINT_ENABLED_OFFSET)) ++ } ++ } ++ ++ function _nextInverseHint(uint256 call_) private pure returns (uint256 r_) { ++ uint256 cursor_; ++ uint256 length_; ++ assembly { ++ cursor_ := mload(add(call_, HINT_CURSOR_OFFSET)) ++ length_ := mload(add(call_, HINT_LENGTH_OFFSET)) ++ } ++ require(cursor_ + 48 <= length_, "inverse hint underflow"); ++ ++ r_ = _allocate(SHORT_ALLOCATION); ++ ++ assembly { ++ let src_ := add(mload(add(call_, HINT_DATA_OFFSET)), cursor_) ++ mstore(r_, shr(128, mload(src_))) ++ mstore(add(r_, 0x20), mload(add(src_, 0x10))) ++ mstore(add(call_, HINT_CURSOR_OFFSET), add(cursor_, 48)) ++ } ++ } ++ ++ function _modmulWithMod(uint256 call_, uint256 a_, uint256 b_, uint256 m_) private view returns (uint256 r_) { ++ unchecked { ++ r_ = _allocate(SHORT_ALLOCATION); ++ _mul(a_, b_, call_ + 0x60); ++ ++ assembly { ++ mstore(call_, 0x60) ++ mstore(add(0x20, call_), 0x20) ++ mstore(add(0x40, call_), 0x40) ++ mstore(add(0xC0, call_), 0x01) ++ mstore(add(0xE0, call_), mload(m_)) ++ mstore(add(0x0100, call_), mload(add(m_, 0x20))) ++ ++ pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) ++ } ++ } ++ } ++ + function _shl1(uint256 a_, uint256 r_) internal pure { + assembly { + let a1_ := mload(add(a_, 0x20)) diff --git a/src/vendor/ECDSA384.sol b/src/vendor/ECDSA384.sol new file mode 100644 index 0000000..ebcb014 --- /dev/null +++ b/src/vendor/ECDSA384.sol @@ -0,0 +1,1138 @@ +// SPDX-License-Identifier: MIT +// +// Vendored from Solarity solidity-lib: https://github.com/dl-solarity/solidity-lib +// Base (upstream, unmodified): commit b947757194de6436062c2d68118c0352be84ac4be +// Local modification: "Add hinted P384 inverse verification" (verifyWithHints / +// verifyWithHintsConsumed and the U384 hint-consumption paths). The exact upstream +// diff is committed alongside this file as ECDSA384.hinted.patch for review. +// Copyright (c) 2023 Solarity. Originally licensed MIT (see header above). +pragma solidity ^0.8.4; + +import {MemoryUtils} from "./MemoryUtils.sol"; + +/** + * @notice Cryptography module + * + * This library provides functionality for ECDSA verification over any 384-bit curve. Currently, + * this is the most efficient implementation out there, consuming ~8.025 million gas per call. + * + * The approach is Strauss-Shamir double scalar multiplication with 6 bits of precompute + affine coordinates. + * For reference, naive implementation uses ~400 billion gas, which is 50000 times more expensive. + * + * We also tried using projective coordinates, however, the gas consumption rose to ~9 million gas. + */ +library ECDSA384 { + using MemoryUtils for *; + using U384 for *; + + /** + * @notice 384-bit curve parameters. + */ + struct Parameters { + bytes a; + bytes b; + bytes gx; + bytes gy; + bytes p; + bytes n; + bytes lowSmax; + } + + struct _Parameters { + uint256 a; + uint256 b; + uint256 gx; + uint256 gy; + uint256 p; + uint256 n; + uint256 lowSmax; + } + + struct _Inputs { + uint256 r; + uint256 s; + uint256 x; + uint256 y; + } + + /** + * @notice The function to verify the ECDSA signature + * @param curveParams_ the 384-bit curve parameters. `lowSmax` is `n / 2`. + * @param hashedMessage_ the already hashed message to be verified. + * @param signature_ the ECDSA signature. Equals to `bytes(r) + bytes(s)`. + * @param pubKey_ the full public key of a signer. Equals to `bytes(x) + bytes(y)`. + * + * Note that signatures only from the lower part of the curve are accepted. + * If your `s > n / 2`, change it to `s = n - s`. + */ + function verify( + Parameters memory curveParams_, + bytes memory hashedMessage_, + bytes memory signature_, + bytes memory pubKey_ + ) internal view returns (bool) { + (bool ok_, ) = _verify(curveParams_, hashedMessage_, signature_, pubKey_, "", false); + return ok_; + } + + function verifyWithHints( + Parameters memory curveParams_, + bytes memory hashedMessage_, + bytes memory signature_, + bytes memory pubKey_, + bytes memory inverseHints_ + ) internal view returns (bool ok_) { + uint256 consumed_; + (ok_, consumed_) = _verify(curveParams_, hashedMessage_, signature_, pubKey_, inverseHints_, true); + require(consumed_ == inverseHints_.length, "unused inverse hints"); + } + + function verifyWithHintsConsumed( + Parameters memory curveParams_, + bytes memory hashedMessage_, + bytes memory signature_, + bytes memory pubKey_, + bytes memory inverseHints_ + ) internal view returns (bool ok_, uint256 consumed_) { + return _verify(curveParams_, hashedMessage_, signature_, pubKey_, inverseHints_, true); + } + + function _verify( + Parameters memory curveParams_, + bytes memory hashedMessage_, + bytes memory signature_, + bytes memory pubKey_, + bytes memory inverseHints_, + bool useHints_ + ) private view returns (bool ok_, uint256 consumed_) { + unchecked { + _Inputs memory inputs_; + + (inputs_.r, inputs_.s) = U384.init2(signature_); + (inputs_.x, inputs_.y) = U384.init2(pubKey_); + + _Parameters memory params_ = _Parameters({ + a: curveParams_.a.init(), + b: curveParams_.b.init(), + gx: curveParams_.gx.init(), + gy: curveParams_.gy.init(), + p: curveParams_.p.init(), + n: curveParams_.n.init(), + lowSmax: curveParams_.lowSmax.init() + }); + + uint256 call = useHints_ ? U384.initCallWithHints(params_.p, inverseHints_) : U384.initCall(params_.p); + + /// accept s only from the lower part of the curve + if ( + U384.eqInteger(inputs_.r, 0) || + U384.cmp(inputs_.r, params_.n) >= 0 || + U384.eqInteger(inputs_.s, 0) || + U384.cmp(inputs_.s, params_.lowSmax) > 0 + ) { + return (false, U384.hintCursor(call)); + } + + if (!_isOnCurve(call, params_.p, params_.a, params_.b, inputs_.x, inputs_.y)) { + return (false, U384.hintCursor(call)); + } + + /// allow compatibility with non-384-bit hash functions. + { + uint256 hashedMessageLength_ = hashedMessage_.length; + + if (hashedMessageLength_ < 48) { + bytes memory tmp_ = new bytes(48); + + MemoryUtils.unsafeCopy( + hashedMessage_.getDataPointer(), + tmp_.getDataPointer() + 48 - hashedMessageLength_, + hashedMessageLength_ + ); + + hashedMessage_ = tmp_; + } + } + + uint256 scalar1 = U384.moddiv(call, hashedMessage_.init(), inputs_.s, params_.n); + uint256 scalar2 = U384.moddiv(call, inputs_.r, inputs_.s, params_.n); + + { + uint256 three = U384.init(3); + + /// We use 6-bit masks where the first 3 bits refer to `scalar1` and the last 3 bits refer to `scalar2`. + uint256[2][64] memory points_ = _precomputePointsTable( + call, + params_.p, + three, + params_.a, + params_.gx, + params_.gy, + inputs_.x, + inputs_.y + ); + + (scalar1, ) = _doubleScalarMultiplication( + call, + params_.p, + three, + params_.a, + points_, + scalar1, + scalar2 + ); + } + + U384.modAssign(call, scalar1, params_.n); + + return (U384.eq(scalar1, inputs_.r), U384.hintCursor(call)); + } + } + + /** + * @dev Check if a point in affine coordinates is on the curve. + */ + function _isOnCurve( + uint256 call, + uint256 p, + uint256 a, + uint256 b, + uint256 x, + uint256 y + ) private view returns (bool) { + unchecked { + if (U384.eqInteger(x, 0) || U384.eq(x, p) || U384.eqInteger(y, 0) || U384.eq(y, p)) { + return false; + } + + uint256 LHS = U384.modexp(call, y, 2); + uint256 RHS = U384.modexp(call, x, 3); + + if (!U384.eqInteger(a, 0)) { + RHS = U384.modadd(RHS, U384.modmul(call, x, a), p); // x^3 + a*x + } + + if (!U384.eqInteger(b, 0)) { + RHS = U384.modadd(RHS, b, p); // x^3 + a*x + b + } + + return U384.eq(LHS, RHS); + } + } + + /** + * @dev Compute the Strauss-Shamir double scalar multiplication scalar1*G + scalar2*H. + */ + function _doubleScalarMultiplication( + uint256 call, + uint256 p, + uint256 three, + uint256 a, + uint256[2][64] memory points, + uint256 scalar1, + uint256 scalar2 + ) private view returns (uint256 x, uint256 y) { + unchecked { + uint256 mask_; + uint256 scalar1Bits_; + uint256 scalar2Bits_; + + assembly { + scalar1Bits_ := mload(scalar1) + scalar2Bits_ := mload(scalar2) + } + + (x, y) = _twiceAffine(call, p, three, a, x, y); + + mask_ = ((scalar1Bits_ >> 183) << 3) | (scalar2Bits_ >> 183); + + if (mask_ != 0) { + (x, y) = _addAffine(call, p, three, a, points[mask_][0], points[mask_][1], x, y); + } + + for (uint256 word = 4; word <= 184; word += 3) { + (x, y) = _twice3Affine(call, p, three, a, x, y); + + mask_ = + (((scalar1Bits_ >> (184 - word)) & 0x07) << 3) | + ((scalar2Bits_ >> (184 - word)) & 0x07); + + if (mask_ != 0) { + (x, y) = _addAffine( + call, + p, + three, + a, + points[mask_][0], + points[mask_][1], + x, + y + ); + } + } + + assembly { + scalar1Bits_ := mload(add(scalar1, 0x20)) + scalar2Bits_ := mload(add(scalar2, 0x20)) + } + + (x, y) = _twiceAffine(call, p, three, a, x, y); + + mask_ = ((scalar1Bits_ >> 255) << 3) | (scalar2Bits_ >> 255); + + if (mask_ != 0) { + (x, y) = _addAffine(call, p, three, a, points[mask_][0], points[mask_][1], x, y); + } + + for (uint256 word = 4; word <= 256; word += 3) { + (x, y) = _twice3Affine(call, p, three, a, x, y); + + mask_ = + (((scalar1Bits_ >> (256 - word)) & 0x07) << 3) | + ((scalar2Bits_ >> (256 - word)) & 0x07); + + if (mask_ != 0) { + (x, y) = _addAffine( + call, + p, + three, + a, + points[mask_][0], + points[mask_][1], + x, + y + ); + } + } + } + } + + /** + * @dev Double an elliptic curve point in affine coordinates. + */ + function _twiceAffine( + uint256 call, + uint256 p, + uint256 three, + uint256 a, + uint256 x1, + uint256 y1 + ) private view returns (uint256 x2, uint256 y2) { + unchecked { + if (x1 == 0) { + return (0, 0); + } + + if (U384.eqInteger(y1, 0)) { + return (0, 0); + } + + uint256 m1 = U384.modexp(call, x1, 2); + U384.modmulAssign(call, m1, three); + U384.modaddAssign(m1, a, p); + + uint256 m2 = U384.modshl1(y1, p); + U384.moddivAssign(call, m1, m2); + + x2 = U384.modexp(call, m1, 2); + U384.modsubAssign(x2, x1, p); + U384.modsubAssign(x2, x1, p); + + y2 = U384.modsub(x1, x2, p); + U384.modmulAssign(call, y2, m1); + U384.modsubAssign(y2, y1, p); + } + } + + /** + * @dev Doubles an elliptic curve point 3 times in affine coordinates. + */ + function _twice3Affine( + uint256 call, + uint256 p, + uint256 three, + uint256 a, + uint256 x1, + uint256 y1 + ) private view returns (uint256 x2, uint256 y2) { + unchecked { + if (x1 == 0) { + return (0, 0); + } + + if (U384.eqInteger(y1, 0)) { + return (0, 0); + } + + uint256 m1 = U384.modexp(call, x1, 2); + U384.modmulAssign(call, m1, three); + U384.modaddAssign(m1, a, p); + + uint256 m2 = U384.modshl1(y1, p); + U384.moddivAssign(call, m1, m2); + + x2 = U384.modexp(call, m1, 2); + U384.modsubAssign(x2, x1, p); + U384.modsubAssign(x2, x1, p); + + y2 = U384.modsub(x1, x2, p); + U384.modmulAssign(call, y2, m1); + U384.modsubAssign(y2, y1, p); + + if (U384.eqInteger(y2, 0)) { + return (0, 0); + } + + U384.modexpAssignTo(call, m1, x2, 2); + U384.modmulAssign(call, m1, three); + U384.modaddAssign(m1, a, p); + + U384.modshl1AssignTo(m2, y2, p); + U384.moddivAssign(call, m1, m2); + + U384.modexpAssignTo(call, x1, m1, 2); + U384.modsubAssign(x1, x2, p); + U384.modsubAssign(x1, x2, p); + + U384.modsubAssignTo(y1, x2, x1, p); + U384.modmulAssign(call, y1, m1); + U384.modsubAssign(y1, y2, p); + + if (U384.eqInteger(y1, 0)) { + return (0, 0); + } + + U384.modexpAssignTo(call, m1, x1, 2); + U384.modmulAssign(call, m1, three); + U384.modaddAssign(m1, a, p); + + U384.modshl1AssignTo(m2, y1, p); + U384.moddivAssign(call, m1, m2); + + U384.modexpAssignTo(call, x2, m1, 2); + U384.modsubAssign(x2, x1, p); + U384.modsubAssign(x2, x1, p); + + U384.modsubAssignTo(y2, x1, x2, p); + U384.modmulAssign(call, y2, m1); + U384.modsubAssign(y2, y1, p); + } + } + + /** + * @dev Add two elliptic curve points in affine coordinates. + */ + function _addAffine( + uint256 call, + uint256 p, + uint256 three, + uint256 a, + uint256 x1, + uint256 y1, + uint256 x2, + uint256 y2 + ) private view returns (uint256 x3, uint256 y3) { + unchecked { + if (x1 == 0 || x2 == 0) { + if (x1 == 0 && x2 == 0) { + return (0, 0); + } + + return x1 == 0 ? (x2.copy(), y2.copy()) : (x1.copy(), y1.copy()); + } + + if (U384.eq(x1, x2)) { + if (U384.eq(y1, y2)) { + return _twiceAffine(call, p, three, a, x1, y1); + } + + return (0, 0); + } + + uint256 m1 = U384.modsub(y1, y2, p); + uint256 m2 = U384.modsub(x1, x2, p); + + U384.moddivAssign(call, m1, m2); + + x3 = U384.modexp(call, m1, 2); + U384.modsubAssign(x3, x1, p); + U384.modsubAssign(x3, x2, p); + + y3 = U384.modsub(x1, x3, p); + U384.modmulAssign(call, y3, m1); + U384.modsubAssign(y3, y1, p); + } + } + + function _precomputePointsTable( + uint256 call, + uint256 p, + uint256 three, + uint256 a, + uint256 gx, + uint256 gy, + uint256 hx, + uint256 hy + ) private view returns (uint256[2][64] memory points_) { + unchecked { + (points_[0x01][0], points_[0x01][1]) = (hx.copy(), hy.copy()); + (points_[0x08][0], points_[0x08][1]) = (gx.copy(), gy.copy()); + + for (uint256 i = 0; i < 8; ++i) { + for (uint256 j = 0; j < 8; ++j) { + if (i + j < 2) { + continue; + } + + uint256 maskTo = (i << 3) | j; + + if (i != 0) { + uint256 maskFrom = ((i - 1) << 3) | j; + + (points_[maskTo][0], points_[maskTo][1]) = _addAffine( + call, + p, + three, + a, + points_[maskFrom][0], + points_[maskFrom][1], + gx, + gy + ); + } else { + uint256 maskFrom = (i << 3) | (j - 1); + + (points_[maskTo][0], points_[maskTo][1]) = _addAffine( + call, + p, + three, + a, + points_[maskFrom][0], + points_[maskFrom][1], + hx, + hy + ); + } + } + } + } + } +} + +/** + * @notice Low-level utility library that implements unsigned 384-bit arithmetics. + * + * Should not be used outside of this file. + */ +library U384 { + uint256 private constant SHORT_ALLOCATION = 64; + + uint256 private constant MUL_OFFSET = 288; + uint256 private constant EXP_OFFSET = 2 * 288; + uint256 private constant INV_OFFSET = 3 * 288; + uint256 private constant HINT_DATA_OFFSET = 0x480; + uint256 private constant HINT_LENGTH_OFFSET = 0x4A0; + uint256 private constant HINT_CURSOR_OFFSET = 0x4C0; + uint256 private constant HINT_ENABLED_OFFSET = 0x4E0; + uint256 private constant CALL_ALLOCATION = 0x500; + + function init(uint256 from_) internal pure returns (uint256 handler_) { + unchecked { + handler_ = _allocate(SHORT_ALLOCATION); + + assembly { + mstore(handler_, 0x00) + mstore(add(0x20, handler_), from_) + } + + return handler_; + } + } + + function init(bytes memory from_) internal pure returns (uint256 handler_) { + unchecked { + require(from_.length == 48, "U384: not 384"); + + handler_ = _allocate(SHORT_ALLOCATION); + + assembly { + mstore(handler_, 0x00) + mstore(add(handler_, 0x10), mload(add(from_, 0x20))) + mstore(add(handler_, 0x20), mload(add(from_, 0x30))) + } + + return handler_; + } + } + + function init2( + bytes memory from2_ + ) internal pure returns (uint256 handler1_, uint256 handler2_) { + unchecked { + require(from2_.length == 96, "U384: not 768"); + + handler1_ = _allocate(SHORT_ALLOCATION); + handler2_ = _allocate(SHORT_ALLOCATION); + + assembly { + mstore(handler1_, 0x00) + mstore(add(handler1_, 0x10), mload(add(from2_, 0x20))) + mstore(add(handler1_, 0x20), mload(add(from2_, 0x30))) + + mstore(handler2_, 0x00) + mstore(add(handler2_, 0x10), mload(add(from2_, 0x50))) + mstore(add(handler2_, 0x20), mload(add(from2_, 0x60))) + } + + return (handler1_, handler2_); + } + } + + function initCall(uint256 m_) internal pure returns (uint256 handler_) { + unchecked { + handler_ = _allocate(CALL_ALLOCATION); + + _sub(m_, init(2), handler_ + INV_OFFSET + 0xA0); + + assembly { + let call_ := add(handler_, MUL_OFFSET) + + mstore(call_, 0x60) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0xC0, call_), 0x01) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + call_ := add(handler_, EXP_OFFSET) + + mstore(call_, 0x40) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0xC0, call_), mload(m_)) + mstore(add(0xE0, call_), mload(add(m_, 0x20))) + + call_ := add(handler_, INV_OFFSET) + + mstore(call_, 0x40) + mstore(add(0x20, call_), 0x40) + mstore(add(0x40, call_), 0x40) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + mstore(add(handler_, HINT_DATA_OFFSET), 0) + mstore(add(handler_, HINT_LENGTH_OFFSET), 0) + mstore(add(handler_, HINT_CURSOR_OFFSET), 0) + mstore(add(handler_, HINT_ENABLED_OFFSET), 0) + } + } + } + + function initCallWithHints(uint256 m_, bytes memory inverseHints_) internal pure returns (uint256 handler_) { + handler_ = initCall(m_); + + assembly { + mstore(add(handler_, HINT_DATA_OFFSET), add(inverseHints_, 0x20)) + mstore(add(handler_, HINT_LENGTH_OFFSET), mload(inverseHints_)) + mstore(add(handler_, HINT_CURSOR_OFFSET), 0) + mstore(add(handler_, HINT_ENABLED_OFFSET), 1) + } + } + + function hintCursor(uint256 call_) internal pure returns (uint256 cursor_) { + assembly { + cursor_ := mload(add(call_, HINT_CURSOR_OFFSET)) + } + } + + function copy(uint256 handler_) internal pure returns (uint256 handlerCopy_) { + unchecked { + handlerCopy_ = _allocate(SHORT_ALLOCATION); + + assembly { + mstore(handlerCopy_, mload(handler_)) + mstore(add(handlerCopy_, 0x20), mload(add(handler_, 0x20))) + } + + return handlerCopy_; + } + } + + function eq(uint256 a_, uint256 b_) internal pure returns (bool eq_) { + assembly { + eq_ := and(eq(mload(a_), mload(b_)), eq(mload(add(a_, 0x20)), mload(add(b_, 0x20)))) + } + } + + function eqInteger(uint256 a_, uint256 bInteger_) internal pure returns (bool eq_) { + assembly { + eq_ := and(eq(mload(a_), 0), eq(mload(add(a_, 0x20)), bInteger_)) + } + } + + function cmp(uint256 a_, uint256 b_) internal pure returns (int256 cmp_) { + unchecked { + uint256 aWord_; + uint256 bWord_; + + assembly { + aWord_ := mload(a_) + bWord_ := mload(b_) + } + + if (aWord_ > bWord_) { + return 1; + } + + if (aWord_ < bWord_) { + return -1; + } + + assembly { + aWord_ := mload(add(a_, 0x20)) + bWord_ := mload(add(b_, 0x20)) + } + + if (aWord_ > bWord_) { + return 1; + } + + if (aWord_ < bWord_) { + return -1; + } + } + } + + function modAssign(uint256 call_, uint256 a_, uint256 m_) internal view { + assembly { + mstore(call_, 0x40) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0x60, call_), mload(a_)) + mstore(add(0x80, call_), mload(add(a_, 0x20))) + mstore(add(0xA0, call_), 0x01) + mstore(add(0xC0, call_), mload(m_)) + mstore(add(0xE0, call_), mload(add(m_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0100, a_, 0x40)) + } + } + + function modexp( + uint256 call_, + uint256 b_, + uint256 eInteger_ + ) internal view returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + assembly { + call_ := add(call_, EXP_OFFSET) + + mstore(add(0x60, call_), mload(b_)) + mstore(add(0x80, call_), mload(add(b_, 0x20))) + mstore(add(0xA0, call_), eInteger_) + + pop(staticcall(gas(), 0x5, call_, 0x0100, r_, 0x40)) + } + + return r_; + } + } + + function modexpAssignTo( + uint256 call_, + uint256 to_, + uint256 b_, + uint256 eInteger_ + ) internal view { + assembly { + call_ := add(call_, EXP_OFFSET) + + mstore(add(0x60, call_), mload(b_)) + mstore(add(0x80, call_), mload(add(b_, 0x20))) + mstore(add(0xA0, call_), eInteger_) + + pop(staticcall(gas(), 0x5, call_, 0x0100, to_, 0x40)) + } + } + + function modadd(uint256 a_, uint256 b_, uint256 m_) internal pure returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + _add(a_, b_, r_); + + if (cmp(r_, m_) >= 0) { + _subFrom(r_, m_); + } + + return r_; + } + } + + function modaddAssign(uint256 a_, uint256 b_, uint256 m_) internal pure { + unchecked { + _addTo(a_, b_); + + if (cmp(a_, m_) >= 0) { + return _subFrom(a_, m_); + } + } + } + + function modmul(uint256 call_, uint256 a_, uint256 b_) internal view returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + _mul(a_, b_, call_ + MUL_OFFSET + 0x60); + + assembly { + call_ := add(call_, MUL_OFFSET) + + pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) + } + + return r_; + } + } + + function modmulAssign(uint256 call_, uint256 a_, uint256 b_) internal view { + unchecked { + _mul(a_, b_, call_ + MUL_OFFSET + 0x60); + + assembly { + call_ := add(call_, MUL_OFFSET) + + pop(staticcall(gas(), 0x5, call_, 0x0120, a_, 0x40)) + } + } + } + + function modsub(uint256 a_, uint256 b_, uint256 m_) internal pure returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + if (cmp(a_, b_) >= 0) { + _sub(a_, b_, r_); + return r_; + } + + _add(a_, m_, r_); + _subFrom(r_, b_); + } + } + + function modsubAssign(uint256 a_, uint256 b_, uint256 m_) internal pure { + unchecked { + if (cmp(a_, b_) >= 0) { + _subFrom(a_, b_); + return; + } + + _addTo(a_, m_); + _subFrom(a_, b_); + } + } + + function modsubAssignTo(uint256 to_, uint256 a_, uint256 b_, uint256 m_) internal pure { + unchecked { + if (cmp(a_, b_) >= 0) { + _sub(a_, b_, to_); + return; + } + + _add(a_, m_, to_); + _subFrom(to_, b_); + } + } + + function modshl1(uint256 a_, uint256 m_) internal pure returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + + _shl1(a_, r_); + + if (cmp(r_, m_) >= 0) { + _subFrom(r_, m_); + } + + return r_; + } + } + + function modshl1AssignTo(uint256 to_, uint256 a_, uint256 m_) internal pure { + unchecked { + _shl1(a_, to_); + + if (cmp(to_, m_) >= 0) { + _subFrom(to_, m_); + } + } + } + + /// @dev Stores modinv into `b_` and moddiv into `a_`. + function moddivAssign(uint256 call_, uint256 a_, uint256 b_) internal view { + unchecked { + uint256 baseCall_ = call_; + if (_hintsEnabled(call_)) { + uint256 inv_ = _nextInverseHint(call_); + uint256 check_ = modmul(call_, b_, inv_); + require(eqInteger(check_, 1), "bad inverse hint"); + + assembly { + mstore(b_, mload(inv_)) + mstore(add(b_, 0x20), mload(add(inv_, 0x20))) + } + } else { + assembly { + call_ := add(call_, INV_OFFSET) + + mstore(add(0x60, call_), mload(b_)) + mstore(add(0x80, call_), mload(add(b_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0120, b_, 0x40)) + } + } + + modmulAssign(baseCall_, a_, b_); + } + } + + function moddiv( + uint256 call_, + uint256 a_, + uint256 b_, + uint256 m_ + ) internal view returns (uint256 r_) { + unchecked { + r_ = modinv(call_, b_, m_); + + _mul(a_, r_, call_ + 0x60); + + assembly { + mstore(call_, 0x60) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0xC0, call_), 0x01) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) + } + } + } + + function modinv(uint256 call_, uint256 b_, uint256 m_) internal view returns (uint256 r_) { + unchecked { + if (_hintsEnabled(call_)) { + r_ = _nextInverseHint(call_); + uint256 check_ = _modmulWithMod(call_, b_, r_, m_); + require(eqInteger(check_, 1), "bad inverse hint"); + return r_; + } + + r_ = _allocate(SHORT_ALLOCATION); + + _sub(m_, init(2), call_ + 0xA0); + + assembly { + mstore(call_, 0x40) + mstore(add(0x20, call_), 0x40) + mstore(add(0x40, call_), 0x40) + mstore(add(0x60, call_), mload(b_)) + mstore(add(0x80, call_), mload(add(b_, 0x20))) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) + } + } + } + + function _hintsEnabled(uint256 call_) private pure returns (bool enabled_) { + assembly { + enabled_ := mload(add(call_, HINT_ENABLED_OFFSET)) + } + } + + function _nextInverseHint(uint256 call_) private pure returns (uint256 r_) { + uint256 cursor_; + uint256 length_; + assembly { + cursor_ := mload(add(call_, HINT_CURSOR_OFFSET)) + length_ := mload(add(call_, HINT_LENGTH_OFFSET)) + } + require(cursor_ + 48 <= length_, "inverse hint underflow"); + + r_ = _allocate(SHORT_ALLOCATION); + + assembly { + let src_ := add(mload(add(call_, HINT_DATA_OFFSET)), cursor_) + mstore(r_, shr(128, mload(src_))) + mstore(add(r_, 0x20), mload(add(src_, 0x10))) + mstore(add(call_, HINT_CURSOR_OFFSET), add(cursor_, 48)) + } + } + + function _modmulWithMod(uint256 call_, uint256 a_, uint256 b_, uint256 m_) private view returns (uint256 r_) { + unchecked { + r_ = _allocate(SHORT_ALLOCATION); + _mul(a_, b_, call_ + 0x60); + + assembly { + mstore(call_, 0x60) + mstore(add(0x20, call_), 0x20) + mstore(add(0x40, call_), 0x40) + mstore(add(0xC0, call_), 0x01) + mstore(add(0xE0, call_), mload(m_)) + mstore(add(0x0100, call_), mload(add(m_, 0x20))) + + pop(staticcall(gas(), 0x5, call_, 0x0120, r_, 0x40)) + } + } + } + + function _shl1(uint256 a_, uint256 r_) internal pure { + assembly { + let a1_ := mload(add(a_, 0x20)) + + mstore(r_, or(shl(1, mload(a_)), shr(255, a1_))) + mstore(add(r_, 0x20), shl(1, a1_)) + } + } + + function _add(uint256 a_, uint256 b_, uint256 r_) private pure { + assembly { + let aWord_ := mload(add(a_, 0x20)) + let sum_ := add(aWord_, mload(add(b_, 0x20))) + + mstore(add(r_, 0x20), sum_) + + sum_ := gt(aWord_, sum_) + sum_ := add(sum_, add(mload(a_), mload(b_))) + + mstore(r_, sum_) + } + } + + function _sub(uint256 a_, uint256 b_, uint256 r_) private pure { + assembly { + let aWord_ := mload(add(a_, 0x20)) + let diff_ := sub(aWord_, mload(add(b_, 0x20))) + + mstore(add(r_, 0x20), diff_) + + diff_ := gt(diff_, aWord_) + diff_ := sub(sub(mload(a_), mload(b_)), diff_) + + mstore(r_, diff_) + } + } + + function _subFrom(uint256 a_, uint256 b_) private pure { + assembly { + let aWord_ := mload(add(a_, 0x20)) + let diff_ := sub(aWord_, mload(add(b_, 0x20))) + + mstore(add(a_, 0x20), diff_) + + diff_ := gt(diff_, aWord_) + diff_ := sub(sub(mload(a_), mload(b_)), diff_) + + mstore(a_, diff_) + } + } + + function _addTo(uint256 a_, uint256 b_) private pure { + assembly { + let aWord_ := mload(add(a_, 0x20)) + let sum_ := add(aWord_, mload(add(b_, 0x20))) + + mstore(add(a_, 0x20), sum_) + + sum_ := gt(aWord_, sum_) + sum_ := add(sum_, add(mload(a_), mload(b_))) + + mstore(a_, sum_) + } + } + + function _mul(uint256 a_, uint256 b_, uint256 r_) private pure { + assembly { + let a0_ := mload(a_) + let a1_ := shr(128, mload(add(a_, 0x20))) + let a2_ := and(mload(add(a_, 0x20)), 0xffffffffffffffffffffffffffffffff) + + let b0_ := mload(b_) + let b1_ := shr(128, mload(add(b_, 0x20))) + let b2_ := and(mload(add(b_, 0x20)), 0xffffffffffffffffffffffffffffffff) + + // r5 + let current_ := mul(a2_, b2_) + let r0_ := and(current_, 0xffffffffffffffffffffffffffffffff) + + // r4 + current_ := shr(128, current_) + + let temp_ := mul(a1_, b2_) + current_ := add(current_, temp_) + let curry_ := lt(current_, temp_) + + temp_ := mul(a2_, b1_) + current_ := add(current_, temp_) + curry_ := add(curry_, lt(current_, temp_)) + + mstore(add(r_, 0x40), add(shl(128, current_), r0_)) + + // r3 + current_ := add(shl(128, curry_), shr(128, current_)) + curry_ := 0 + + temp_ := mul(a0_, b2_) + current_ := add(current_, temp_) + curry_ := lt(current_, temp_) + + temp_ := mul(a1_, b1_) + current_ := add(current_, temp_) + curry_ := add(curry_, lt(current_, temp_)) + + temp_ := mul(a2_, b0_) + current_ := add(current_, temp_) + curry_ := add(curry_, lt(current_, temp_)) + + r0_ := and(current_, 0xffffffffffffffffffffffffffffffff) + + // r2 + current_ := add(shl(128, curry_), shr(128, current_)) + curry_ := 0 + + temp_ := mul(a0_, b1_) + current_ := add(current_, temp_) + curry_ := lt(current_, temp_) + + temp_ := mul(a1_, b0_) + current_ := add(current_, temp_) + curry_ := add(curry_, lt(current_, temp_)) + + mstore(add(r_, 0x20), add(shl(128, current_), r0_)) + + // r1 + current_ := add(shl(128, curry_), shr(128, current_)) + current_ := add(current_, mul(a0_, b0_)) + + mstore(r_, current_) + } + } + + function _allocate(uint256 bytes_) private pure returns (uint256 handler_) { + unchecked { + assembly { + handler_ := mload(0x40) + mstore(0x40, add(handler_, bytes_)) + } + + return handler_; + } + } +} diff --git a/src/vendor/MemoryUtils.sol b/src/vendor/MemoryUtils.sol new file mode 100644 index 0000000..39a899c --- /dev/null +++ b/src/vendor/MemoryUtils.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +// +// Vendored unmodified from Solarity solidity-lib: +// https://github.com/dl-solarity/solidity-lib (commit b947757194de6436062c2d68118c0352be84ac4be) +// Copyright (c) 2023 Solarity. Originally licensed MIT (see header above). +pragma solidity ^0.8.4; + +/** + * @title MemoryUtils + * @notice A library that provides utility functions for memory manipulation in Solidity. + */ +library MemoryUtils { + /** + * @notice Copies the contents of the source bytes to the destination bytes. strings can be casted + * to bytes in order to use this function. + * + * @param source_ The source bytes to copy from. + * @return destination_ The newly allocated bytes. + */ + function copy(bytes memory source_) internal view returns (bytes memory destination_) { + destination_ = new bytes(source_.length); + + unsafeCopy(getDataPointer(source_), getDataPointer(destination_), source_.length); + } + + /** + * @notice Copies the contents of the source bytes32 array to the destination bytes32 array. + * uint256[], address[] array can be casted to bytes32[] via `TypeCaster` library. + * + * @param source_ The source bytes32 array to copy from. + * @return destination_ The newly allocated bytes32 array. + */ + function copy(bytes32[] memory source_) internal view returns (bytes32[] memory destination_) { + destination_ = new bytes32[](source_.length); + + unsafeCopy(getDataPointer(source_), getDataPointer(destination_), source_.length * 32); + } + + /** + * @notice Copies memory from one location to another efficiently via identity precompile. + * @param sourcePointer_ The offset in the memory from which to copy. + * @param destinationPointer_ The offset in the memory where the result will be copied. + * @param size_ The size of the memory to copy. + * + * @dev This function does not account for free memory pointer and should be used with caution. + * + * This signature of calling identity precompile is: + * staticcall(gas(), address(0x04), argsOffset, argsSize, retOffset, retSize) + */ + function unsafeCopy( + uint256 sourcePointer_, + uint256 destinationPointer_, + uint256 size_ + ) internal view { + assembly { + pop(staticcall(gas(), 4, sourcePointer_, size_, destinationPointer_, size_)) + } + } + + /** + * @notice Returns the memory pointer to the given bytes starting position including the length. + */ + function getPointer(bytes memory data_) internal pure returns (uint256 pointer_) { + assembly { + pointer_ := data_ + } + } + + /** + * @notice Returns the memory pointer to the given bytes starting position including the length. + * Cast uint256[] and address[] to bytes32[] via `TypeCaster` library. + */ + function getPointer(bytes32[] memory data_) internal pure returns (uint256 pointer_) { + assembly { + pointer_ := data_ + } + } + + /** + * @notice Returns the memory pointer to the given bytes data starting position skipping the length. + */ + function getDataPointer(bytes memory data_) internal pure returns (uint256 pointer_) { + assembly { + pointer_ := add(data_, 32) + } + } + + /** + * @notice Returns the memory pointer to the given bytes data starting position skipping the length. + * Cast uint256[] and address[] to bytes32[] via `TypeCaster` library. + */ + function getDataPointer(bytes32[] memory data_) internal pure returns (uint256 pointer_) { + assembly { + pointer_ := add(data_, 32) + } + } +} diff --git a/src/vendor/README.md b/src/vendor/README.md new file mode 100644 index 0000000..0f0d39f --- /dev/null +++ b/src/vendor/README.md @@ -0,0 +1,44 @@ +# Vendored dependencies + +These files are vendored (copied in-tree) from the Solarity `solidity-lib` so that +nitro-validator is self-contained and the cryptographic code audited here is exactly +the code that is deployed — with no dependency on an external submodule or fork. + +| File | Source path in solidity-lib | Modified? | +|------|------------------------------|-----------| +| `ECDSA384.sol` | `contracts/libs/crypto/ECDSA384.sol` (contains both `ECDSA384` and `U384`) | **Yes** — see below | +| `MemoryUtils.sol` | `contracts/libs/utils/MemoryUtils.sol` | No (verbatim) | + +## Source & license + +- Upstream: https://github.com/dl-solarity/solidity-lib +- Base commit (unmodified upstream): `b947757194de6436062c2d68118c0352be84ac4be` +- License: MIT, Copyright (c) 2023 Solarity (SPDX headers retained on each file). + +The only imports either file makes are between these two vendored files; nothing else +from `solidity-lib` is used by this repo. + +## The one local modification (audit focus) + +`ECDSA384.sol` carries a single functional change on top of the base commit: +**"Add hinted P384 inverse verification"** — adding `verifyWithHints` / +`verifyWithHintsConsumed` and the hint-consumption paths in `U384` +(`initCallWithHints`, `_nextInverseHint`, the hinted branches of `moddivAssign` / +`modinv`). + +The exact upstream diff is committed next to the file as +[`ECDSA384.hinted.patch`](./ECDSA384.hinted.patch) so reviewers can see precisely +the delta from audited upstream code. + +**Safety summary:** every off-chain-supplied inverse `inv` is verified on-chain with +`require(b·inv ≡ 1 (mod m), "bad inverse hint")` before use, plus a bounds check +(`"inverse hint underflow"`) and an exact-consumption check (`"unused inverse hints"`). +Because the moduli (`n`, `p`) are prime, the inverse is unique, so a malicious hint can +only cause a revert — never a false accept. The acceptance rule is identical to upstream +`ECDSA384.verify`. See `docs/hinted-p384-nitro-attestation.md` for the full argument. + +## Re-syncing with upstream + +To pull a newer upstream `ECDSA384.sol`/`MemoryUtils.sol`, re-copy from the desired +commit, re-apply `ECDSA384.hinted.patch` (or re-derive the hinted change), update the +base commit hash above, and re-run the test suite (including the FFI parity tests). diff --git a/src/demo/CertManagerDemo.sol b/test/helpers/CertManagerDemo.sol similarity index 87% rename from src/demo/CertManagerDemo.sol rename to test/helpers/CertManagerDemo.sol index 15fb756..40ef0ca 100644 --- a/src/demo/CertManagerDemo.sol +++ b/test/helpers/CertManagerDemo.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; -import {CertManager} from "../CertManager.sol"; -import {IP384Verifier} from "../IP384Verifier.sol"; +import {CertManager} from "../../src/CertManager.sol"; +import {IP384Verifier} from "../../src/IP384Verifier.sol"; /// @notice Demo-only certificate manager with configurable certificate expiry grace. /// @dev This exists only to replay expired Nitro fixtures on testnets. Do not use in production or audit target deployments. diff --git a/test/helpers/ECDSA384HintCollector.sol b/test/helpers/ECDSA384HintCollector.sol index bd93cb8..002c072 100644 --- a/test/helpers/ECDSA384HintCollector.sol +++ b/test/helpers/ECDSA384HintCollector.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; -import {MemoryUtils} from "@solarity/libs/utils/MemoryUtils.sol"; +import {MemoryUtils} from "../../src/vendor/MemoryUtils.sol"; /** * @notice Cryptography module diff --git a/test/hinted/HintedNitroAttestation.t.sol b/test/hinted/HintedNitroAttestation.t.sol index 7fa2eff..84c9efc 100644 --- a/test/hinted/HintedNitroAttestation.t.sol +++ b/test/hinted/HintedNitroAttestation.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.26; import {Test, console} from "forge-std/Test.sol"; import {NitroValidator} from "../../src/NitroValidator.sol"; import {CertManager} from "../../src/CertManager.sol"; -import {CertManagerDemo} from "../../src/demo/CertManagerDemo.sol"; +import {CertManagerDemo} from "../helpers/CertManagerDemo.sol"; import {ICertManager} from "../../src/ICertManager.sol"; import {CborDecode} from "../../src/CborDecode.sol"; import {P384Verifier} from "../../src/P384Verifier.sol"; @@ -172,6 +172,75 @@ contract HintedNitroAttestationTest is Test { validator.validateAttestation("", ""); } + // Expiry is checked in _verifyValidity during cold parsing, before the signature is verified, + // so an expired cert is rejected on its FIRST (uncached) verification even with empty hints. + // Distinct from test_HintedCACertRejectsExpiredCachedCert, which exercises the cached path + // ("cert expired"). + function test_HintedCACertRejectsExpiredCertOnFirstVerification() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + (bytes memory caCert, bytes32 parentHash,) = _firstNonRootCA(attestationTbs, ptrs); + + vm.warp(1768953600); // 2026-01-21T00:00:00Z, past this fixture's first non-root CA expiry. + + // caCert has never been verified, so this is the cold path; the root parent stays valid. + vm.expectRevert("certificate not valid anymore"); + certManager.verifyCACertWithHints(caCert, parentHash, ""); + } + + // _certificateExpired is `notAfter < block.timestamp`, so a cert is still valid at the exact + // notAfter second and expired one second later. Verify both sides of that boundary on a cold + // (uncached) cert via fresh CertManager instances. + function test_CertValidityBoundaryAtNotAfter() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + (bytes memory caCert, bytes32 parentHash, bytes memory parentPubKey) = _firstNonRootCA(attestationTbs, ptrs); + + // Learn caCert's notAfter by caching it on the shared manager at the (valid) setUp time. + bytes memory hints = hintCollector.collectCertSignatureHints(caCert, parentPubKey); + bytes32 certHash = certManager.verifyCACertWithHints(caCert, parentHash, hints); + uint64 notAfter = certManager.loadVerified(certHash).notAfter; + + // Exactly at notAfter: cold verification on a fresh manager succeeds (cert still valid). + CertManager atBoundary = new CertManager(p384Verifier); + vm.warp(notAfter); + atBoundary.verifyCACertWithHints(caCert, parentHash, hints); + assertGt(atBoundary.loadVerified(certHash).pubKey.length, 0, "cert valid at notAfter"); + + // One second later: cold verification on a fresh manager is rejected as expired. + CertManager pastBoundary = new CertManager(p384Verifier); + vm.warp(uint256(notAfter) + 1); + vm.expectRevert("certificate not valid anymore"); + pastBoundary.verifyCACertWithHints(caCert, parentHash, hints); + } + + // ECDSA384 rejects out-of-range scalars (r==0, r>=n, s==0, s>lowSmax) before consuming any + // hints, so the verifier returns false (no hints needed). Guards against malleable/degenerate + // signatures independent of the cert-chain plumbing. + function test_P384VerifierRejectsOutOfRangeScalars() public view { + bytes memory hash = new bytes(48); // contents irrelevant: rejected before hashing/curve math + bytes memory pubKey = new bytes(96); // not reached: bounds are checked before _isOnCurve + + bytes memory zero48 = new bytes(48); + bytes memory one48 = new bytes(48); + one48[47] = 0x01; + bytes memory max48 = new bytes(48); // > n and > lowSmax + for (uint256 i = 0; i < 48; i++) { + max48[i] = 0xff; + } + + // r == 0 + assertFalse(p384Verifier.verifyP384SignatureWithHints(hash, abi.encodePacked(zero48, one48), pubKey, "")); + // r >= n + assertFalse(p384Verifier.verifyP384SignatureWithHints(hash, abi.encodePacked(max48, one48), pubKey, "")); + // s == 0 + assertFalse(p384Verifier.verifyP384SignatureWithHints(hash, abi.encodePacked(one48, zero48), pubKey, "")); + // s > lowSmax + assertFalse(p384Verifier.verifyP384SignatureWithHints(hash, abi.encodePacked(one48, max48), pubKey, "")); + } + function test_OffchainWitnessGeneratorMatchesSolidityCollector() public { if (!vm.envOr("NITRO_RUN_FFI", false)) { console.log("==== OFFCHAIN WITNESS GENERATOR ===="); From 1206e8476f1bcb6fca2e250715e129a9ddf585dd Mon Sep 17 00:00:00 2001 From: Elena Nadolinski Date: Tue, 9 Jun 2026 12:03:39 -0700 Subject: [PATCH 04/10] Document behavioral gaps, add parser tests, version as v2.0.0-rc.1 Behavioral-gap documentation (no logic change): - ECDSA384Curve: explain why lowSmax = n-1 (AWS doesn't emit low-S) and the resulting signature malleability; warn against using the signature as a key - NitroValidator/P384Verifier NatSpec: integrator responsibilities for freshness/replay, malleability, and PCR/moduleID policy - README + design doc: "Security considerations" / "Integrator responsibilities" - Mark the reverting legacy stubs @deprecated (CertManager, NitroValidator) and reorder ICertManager to lead with the hinted entrypoints Test coverage for the parsing layers (the cert/attestation attack surface): - test/Asn1Decode.t.sol: malformed DER (multi-byte tag, oversized length, out-of-range length, wrong types, bad timestamps, non-0-padded bitstring) + fuzz - test/CborDecode.t.sol: unexpected/unsupported/indefinite types, null handling, truncated length, declared-length-exceeds-buffer + fuzz - test/LibBytes.t.sol: slice/keccak/readUintN bounds, overflow, round-trip fuzz (86 tests total, up from 42) Release hygiene: - add CHANGELOG.md describing the v2.0.0-rc.1 hinted P-384 release - README: installation + @nitro-validator remapping guidance - docs: fix stale leanthebean/solidity-lib fork reference to point at src/vendor Generated with Claude Code Co-Authored-By: Claude --- CHANGELOG.md | 47 ++++++++ README.md | 33 +++++ docs/hinted-p384-nitro-attestation.md | 27 ++++- src/CertManager.sol | 3 + src/ECDSA384Curve.sol | 8 +- src/ICertManager.sol | 12 +- src/NitroValidator.sol | 13 ++ src/P384Verifier.sol | 7 ++ test/Asn1Decode.t.sol | 167 ++++++++++++++++++++++++++ test/CborDecode.t.sol | 102 ++++++++++++++++ test/LibBytes.t.sol | 122 +++++++++++++++++++ 11 files changed, 534 insertions(+), 7 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 test/Asn1Decode.t.sol create mode 100644 test/CborDecode.t.sol create mode 100644 test/LibBytes.t.sol diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0065ccb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,47 @@ +# Changelog + +All notable changes to this project are documented here. The format is based on +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project aims to follow +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [2.0.0-rc.1] - 2026-06-09 + +First release candidate of the **hinted P-384** rework. This is a major, breaking change motivated +by the Fusaka upgrade (EIP-7883), which raises `MODEXP` pricing enough that the previous fully +on-chain attestation verification no longer fits in a block on Base. Verification now moves the +modular inversions off-chain as calldata "hints" that are re-verified on-chain (`b · inv ≡ 1 mod m`), +so a wrong hint can only revert, never forge — the accept rule is unchanged. + +This is a release candidate: it is intended for the human security audit and partner evaluation, not +yet a general-availability release. + +### Changed (breaking) +- Verification is now hinted. Use `CertManager.verifyCACertWithHints` / + `verifyClientCertWithHints` and `NitroValidator.validateAttestationWithHints`. +- Constructors now take an `IP384Verifier`: deploy `P384Verifier` → `CertManager(p384Verifier)` → + `NitroValidator(certManager, p384Verifier)`. +- `validateAttestationWithHints` requires the certificate bundle to be verified/cached first; an + uncached bundle reverts with `"inverse hint underflow"`. + +### Removed +- The fully on-chain (non-hinted) verification path. `verifyCACert`, `verifyClientCert`, and + `validateAttestation` are retained only as reverting stubs (marked deprecated) for ABI continuity. + +### Added +- `IP384Verifier` / `P384Verifier` (swappable hinted P-384 verifier) and `ECDSA384Curve` params. +- Off-chain hint generator and tooling under `tools/` (Node.js, no dependencies), cross-checked for + byte-identical parity with the on-chain reference via FFI tests. +- `docs/hinted-p384-nitro-attestation.md` design/security/gas spec. +- CI job running the FFI hint-parity tests. +- Negative tests: expired cert (cold & cached), validity boundary, out-of-range scalar rejection; + and malformed-input / fuzz tests for the DER, CBOR, and byte-slicing parsers. + +### Internal / hygiene +- Vendored the P-384 verifier (`src/vendor/ECDSA384.sol`, `MemoryUtils.sol`) from + `dl-solarity/solidity-lib`, removing the personal-fork submodule; provenance and the exact + upstream diff are recorded in `src/vendor/`. +- Documented integrator responsibilities (freshness/replay, signature malleability, enclave policy) + in NatSpec, the README, and the design doc. +- Moved the demo `CertManagerDemo` out of `src/` into `test/helpers/`. + +[2.0.0-rc.1]: https://github.com/base/nitro-validator/releases/tag/v2.0.0-rc.1 diff --git a/README.md b/README.md index c1f7f75..792d811 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,39 @@ contract Validator { } ``` +## Installation + +As a Foundry dependency: + +```sh +forge install base/nitro-validator +``` + +Then map the `@nitro-validator/` prefix used in the examples above to the package's `src/` in your +`remappings.txt`: + +``` +@nitro-validator/=lib/nitro-validator/src/ +``` + +The library vendors its only third-party dependency (the P-384 verifier) under `src/vendor/`, so no +additional submodules are required beyond `forge-std`. + +## Security considerations + +Verification proves an attestation is genuine; some properties are intentionally left to the +integrator (see [docs](docs/hinted-p384-nitro-attestation.md#integrator-responsibilities-what-the-contract-does-not-enforce)): + +- **Freshness / replay** — the contract does not compare the attestation `timestamp` to + `block.timestamp` or match the `nonce` to a challenge. Enforce freshness yourself if you need it. +- **Signature malleability** — low-S is not enforced (AWS does not emit low-S), so the malleable + twin `(r, n-s)` also verifies. Never use the signature as a uniqueness key; dedupe on canonical + attestation fields instead. +- **Enclave policy** — checking `pcrs` / `moduleID` against the enclave image(s) you trust is your + responsibility. +- **Revocation** — not supported (consistent with AWS's documented validation process, linked + above). + ## Build ```sh diff --git a/docs/hinted-p384-nitro-attestation.md b/docs/hinted-p384-nitro-attestation.md index e77f8b1..7df6bb4 100644 --- a/docs/hinted-p384-nitro-attestation.md +++ b/docs/hinted-p384-nitro-attestation.md @@ -442,6 +442,26 @@ can never silently diverge. must stay in sync with the verifier's execution order. Its DER/CBOR parsing should be reviewed for robustness. +### Integrator responsibilities (what the contract does NOT enforce) + +Verification proves an attestation is genuine and well-formed. The following are +deliberately left to the caller and must be handled in the consuming contract: + +- **Freshness / anti-replay.** `validateAttestationWithHints` only checks that + `timestamp` is non-zero and that `nonce` is within a size bound; it never compares + `timestamp` to `block.timestamp` nor matches `nonce` to a challenge. A valid + attestation can be replayed until its short-lived leaf certificate expires. If you + need freshness, compare `ptrs.timestamp` to `block.timestamp` and/or verify + `ptrs.nonce` against a value you issued. +- **Signature malleability.** Low-S is intentionally not enforced (AWS does not + guarantee low-S; see `CURVE_LOW_S_MAX` in `ECDSA384Curve.sol`), so for a valid + signature `(r, s)` the twin `(r, n−s)` also verifies. This cannot forge an + attestation AWS never produced, but you must NOT use the raw signature (or + `attestationTbs + signature`, or its hash) as a unique key — dedupe on canonical + attestation fields (e.g. `moduleID + timestamp + nonce`). +- **Enclave-image / PCR policy.** The contract returns the parsed `pcrs` and + `moduleID`; deciding which enclave images you trust is application policy. + ## 11. On-chain demo A Base Sepolia run of the §6 cold sequence needs: an RPC URL, a funded broadcaster @@ -464,9 +484,10 @@ submitting ordinary transactions. ## Appendix: the code change -The hinted verifier is the upstream `ECDSA384` verifier from the -`dl-solarity/solidity-lib` dependency, patched in place on the -`leanthebean/solidity-lib` fork with **one operation — modular inversion — made +The hinted verifier is the upstream `ECDSA384` verifier from +`dl-solarity/solidity-lib`, vendored into this repo at `src/vendor/ECDSA384.sol` (see +`src/vendor/README.md` for provenance and the exact upstream diff in +`src/vendor/ECDSA384.hinted.patch`) with **one operation — modular inversion — made hint-aware in two places**. Everything else (the Strauss–Shamir ladder, precompute table, on-curve check, scalar bounds, final `x_R == r`) is unchanged. When hints are disabled the code follows the original path, so the unhinted path is the equivalence diff --git a/src/CertManager.sol b/src/CertManager.sol index a7d2e87..cc2edf3 100644 --- a/src/CertManager.sol +++ b/src/CertManager.sol @@ -64,10 +64,13 @@ contract CertManager is ICertManager { ); } + /// @notice DEPRECATED — always reverts. The fully on-chain (non-hinted) path is too expensive + /// post-Fusaka and has been removed. Use {verifyCACertWithHints}. function verifyCACert(bytes memory, bytes32) external pure returns (bytes32) { revert("use hinted cert verification"); } + /// @notice DEPRECATED — always reverts. Use {verifyClientCertWithHints}. function verifyClientCert(bytes memory, bytes32) external pure returns (VerifiedCert memory) { revert("use hinted cert verification"); } diff --git a/src/ECDSA384Curve.sol b/src/ECDSA384Curve.sol index f55b4e2..8e19b03 100644 --- a/src/ECDSA384Curve.sol +++ b/src/ECDSA384Curve.sol @@ -17,7 +17,13 @@ library ECDSA384Curve { hex"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff"; bytes public constant CURVE_N = hex"ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973"; - // use n-1 for lowSmax, which allows s-values above n/2 + // Upper bound for the accepted `s` value. Set to n-1 (NOT n/2) on purpose: AWS Nitro does not + // guarantee low-S signatures, so enforcing low-S would reject legitimate attestations. The + // consequence is that ECDSA signature malleability is NOT prevented at this layer — for a valid + // signature (r, s) the twin (r, n-s) also verifies. This cannot forge a signature AWS never + // produced, but integrators MUST NOT treat the raw signature (or attestationTbs+signature, or + // its hash) as a unique identifier; dedupe on a canonical attestation field (e.g. moduleID + + // timestamp + nonce) instead. bytes public constant CURVE_LOW_S_MAX = hex"ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52972"; diff --git a/src/ICertManager.sol b/src/ICertManager.sol index bfef14e..e072abe 100644 --- a/src/ICertManager.sol +++ b/src/ICertManager.sol @@ -10,9 +10,7 @@ interface ICertManager { bytes pubKey; } - function verifyCACert(bytes memory cert, bytes32 parentCertHash) external returns (bytes32); - - function verifyClientCert(bytes memory cert, bytes32 parentCertHash) external returns (VerifiedCert memory); + // --- Active (hinted) entrypoints --- function verifyCACertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) external @@ -23,4 +21,12 @@ interface ICertManager { returns (VerifiedCert memory); function loadVerified(bytes32 certHash) external view returns (VerifiedCert memory); + + // --- DEPRECATED: these always revert; use the *WithHints variants above. --- + + /// @dev DEPRECATED — always reverts ("use hinted cert verification"). + function verifyCACert(bytes memory cert, bytes32 parentCertHash) external returns (bytes32); + + /// @dev DEPRECATED — always reverts ("use hinted cert verification"). + function verifyClientCert(bytes memory cert, bytes32 parentCertHash) external returns (VerifiedCert memory); } diff --git a/src/NitroValidator.sol b/src/NitroValidator.sol index bd8c423..f4ddf0c 100644 --- a/src/NitroValidator.sol +++ b/src/NitroValidator.sol @@ -73,6 +73,8 @@ contract NitroValidator { signature = attestation.slice(signaturePtr.start(), signaturePtr.length()); } + /// @notice DEPRECATED — always reverts. The fully on-chain (non-hinted) path is too expensive + /// post-Fusaka and has been removed. Use {validateAttestationWithHints}. function validateAttestation(bytes memory, bytes memory) public pure returns (Ptrs memory) { revert("use hinted attestation verification"); } @@ -85,6 +87,17 @@ contract NitroValidator { /// This function re-walks the bundle with EMPTY hints (see `verifyCachedCertBundle`), /// which only succeeds on already-cached certs. If any cert is uncached it reverts with /// "inverse hint underflow" — even when `attestationSigHints` itself is valid. + /// @dev INTEGRATOR RESPONSIBILITIES — this function proves the attestation is genuine and + /// well-formed, but deliberately does NOT enforce: + /// - Freshness / anti-replay: `ptrs.timestamp` is only checked to be non-zero and `nonce` + /// is only length-bounded. A valid attestation can be replayed until its leaf cert + /// expires. Callers that need freshness must compare `ptrs.timestamp` to `block.timestamp` + /// and/or match `ptrs.nonce` against a challenge they issued. + /// - Signature non-malleability: low-S is not enforced (see {ECDSA384Curve.CURVE_LOW_S_MAX}), + /// so do not use `signature` (or its hash) as a uniqueness key — dedupe on attestation + /// fields instead. + /// - PCR / moduleID policy: the caller must check `ptrs.pcrs` / `ptrs.moduleID` against the + /// enclave image(s) they trust. /// @param attestationTbs The COSE Sign1 to-be-signed bytes (from `decodeAttestationTbs`). /// @param signature The 96-byte (r||s) P-384 attestation signature. /// @param attestationSigHints Off-chain inverse hints for the attestation signature; re-verified diff --git a/src/P384Verifier.sol b/src/P384Verifier.sol index b3d70c9..d815303 100644 --- a/src/P384Verifier.sol +++ b/src/P384Verifier.sol @@ -6,6 +6,13 @@ import {IP384Verifier} from "./IP384Verifier.sol"; import {ECDSA384} from "./vendor/ECDSA384.sol"; contract P384Verifier is IP384Verifier { + /// @notice Verify a P-384 ECDSA signature using off-chain-computed modular-inverse hints. + /// @dev Each hint is re-checked on-chain (`b · inv ≡ 1 mod m`), so a wrong hint can only cause a + /// revert, never a false accept; the accept rule is identical to a hint-free verification. + /// Scalars are required in range (r, s ∈ [1, n-1]) and the public key must be on-curve, but + /// low-S is NOT enforced (see {ECDSA384Curve.CURVE_LOW_S_MAX}): signatures are malleable, so + /// callers must not use the signature as a uniqueness key. + /// @return True iff the signature is valid for `hash` under `pubKey`. function verifyP384SignatureWithHints( bytes memory hash, bytes memory signature, diff --git a/test/Asn1Decode.t.sol b/test/Asn1Decode.t.sol new file mode 100644 index 0000000..6f049cf --- /dev/null +++ b/test/Asn1Decode.t.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.15; + +import {Test} from "forge-std/Test.sol"; +import {Asn1Decode, Asn1Ptr, LibAsn1Ptr} from "../src/Asn1Decode.sol"; + +contract Asn1Harness { + using Asn1Decode for bytes; + using LibAsn1Ptr for Asn1Ptr; + + function rootLength(bytes memory der) external pure returns (uint256) { + return der.root().length(); + } + + function rootContent(bytes memory der) external pure returns (uint256) { + return der.root().content(); + } + + function uintAtRoot(bytes memory der) external pure returns (uint256) { + return der.uintAt(der.root()); + } + + function timestampAtRoot(bytes memory der) external pure returns (uint256) { + return der.timestampAt(der.root()); + } + + function bitstringContent(bytes memory der) external pure returns (uint256) { + return der.bitstring(der.root()).content(); + } + + function firstChildHeader(bytes memory der) external pure returns (uint256) { + return der.firstChildOf(der.root()).header(); + } +} + +contract Asn1DecodeTest is Test { + Asn1Harness h; + + function setUp() public { + h = new Asn1Harness(); + } + + // --- readNodeLength / tag handling --- + + function test_root_multiByteTag_reverts() public { + vm.expectRevert("ASN.1 tags longer than 1-byte are not supported"); + h.rootLength(hex"1f00"); // low tag bits 0x1f == high-tag-number form + } + + function test_root_emptyInput_reverts() public { + vm.expectRevert(); // der[0] out-of-bounds + h.rootLength(hex""); + } + + // length encoded over >2 bytes and exceeding 2**64-1 must be rejected + function test_root_oversizedLength_reverts() public { + vm.expectRevert(); // require(length <= 2**64-1) has no message + h.rootLength(hex"0289ffffffffffffffffff"); // INTEGER, 9 length bytes all 0xff + } + + // --- uintAt --- + + function test_uintAt_value() public view { + assertEq(h.uintAtRoot(hex"0203012345"), 0x012345); // INTEGER 0x012345 + } + + function test_uintAt_notInteger_reverts() public { + vm.expectRevert("Not type INTEGER"); + h.uintAtRoot(hex"0401ff"); // OCTET STRING, not INTEGER + } + + function test_uintAt_negative_reverts() public { + vm.expectRevert("Not positive"); + h.uintAtRoot(hex"020180"); // high bit set + } + + // declared length runs past the buffer -> readBytesN bound trips + function test_uintAt_lengthPastBuffer_reverts() public { + vm.expectRevert(); // require(idx + len <= self.length) has no message + h.uintAtRoot(hex"02050000"); // claims 5 content bytes, only 2 present + } + + // --- timestampAt --- + + function test_timestamp_utcEpoch() public view { + // UTCTime "700101000000Z" -> 1970-01-01T00:00:00Z + assertEq(h.timestampAtRoot(_utcTime("700101000000Z")), 0); + } + + function test_timestamp_generalizedKnownValue() public view { + // GeneralizedTime "20240101000000Z" -> 2024-01-01T00:00:00Z + assertEq(h.timestampAtRoot(_generalizedTime("20240101000000Z")), 1704067200); + } + + function test_timestamp_wrongType_reverts() public { + bytes memory der = abi.encodePacked(hex"160d", bytes("700101000000Z")); // type 0x16 + vm.expectRevert("Invalid TIMESTAMP"); + h.timestampAtRoot(der); + } + + function test_timestamp_wrongLength_reverts() public { + bytes memory der = abi.encodePacked(hex"170c", bytes("70010100000Z")); // UTCTime, length 12 + vm.expectRevert("Invalid TIMESTAMP"); + h.timestampAtRoot(der); + } + + function test_timestamp_missingZ_reverts() public { + vm.expectRevert("TIMESTAMP must be UTC"); + h.timestampAtRoot(_utcTime("700101000000X")); + } + + function test_timestamp_nonDigit_reverts() public { + vm.expectRevert("Invalid character in TIMESTAMP"); + h.timestampAtRoot(_utcTime("7A0101000000Z")); + } + + // --- bitstring --- + + function test_bitstring_content() public view { + // BIT STRING, 0x00 pad byte then 0x41 -> content pointer advances past the pad byte + assertEq(h.bitstringContent(hex"03020041"), 3); + } + + function test_bitstring_notBitString_reverts() public { + vm.expectRevert("Not type BIT STRING"); + h.bitstringContent(hex"0401ff"); + } + + function test_bitstring_nonZeroPadded_reverts() public { + vm.expectRevert("Non-0-padded BIT STRING"); + h.bitstringContent(hex"03020100"); // pad byte is 0x01, not 0x00 + } + + // --- firstChildOf --- + + function test_firstChildOf_notConstructed_reverts() public { + vm.expectRevert("Not a constructed type"); + h.firstChildHeader(hex"0401ff"); // OCTET STRING is primitive, not constructed + } + + // --- fuzz --- + + function testFuzz_readNodeLength_shortForm(uint8 lenSeed) public view { + uint256 len = bound(lenSeed, 0, 127); // short-form length is a single < 0x80 byte + bytes memory der = abi.encodePacked(bytes1(0x04), bytes1(uint8(len)), new bytes(len)); + assertEq(h.rootLength(der), len); + assertEq(h.rootContent(der), 2); + } + + function testFuzz_uintAt_positive(uint64 v) public view { + // INTEGER with an explicit 0x00 sign byte so the value is always positive + bytes memory der = abi.encodePacked(bytes1(0x02), bytes1(0x09), bytes1(0x00), bytes8(v)); + assertEq(h.uintAtRoot(der), v); + } + + function _utcTime(string memory s) internal pure returns (bytes memory) { + bytes memory b = bytes(s); + require(b.length == 13, "test: UTCTime must be 13 chars"); + return abi.encodePacked(bytes1(0x17), bytes1(uint8(13)), b); + } + + function _generalizedTime(string memory s) internal pure returns (bytes memory) { + bytes memory b = bytes(s); + require(b.length == 15, "test: GeneralizedTime must be 15 chars"); + return abi.encodePacked(bytes1(0x18), bytes1(uint8(15)), b); + } +} diff --git a/test/CborDecode.t.sol b/test/CborDecode.t.sol new file mode 100644 index 0000000..f3c6be5 --- /dev/null +++ b/test/CborDecode.t.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.15; + +import {Test} from "forge-std/Test.sol"; +import {CborDecode, CborElement, LibCborElement} from "../src/CborDecode.sol"; + +contract CborHarness { + using CborDecode for bytes; + using LibCborElement for CborElement; + + function byteStringLength(bytes memory c) external pure returns (uint256) { + return c.byteStringAt(0).length(); + } + + function byteStringStart(bytes memory c) external pure returns (uint256) { + return c.byteStringAt(0).start(); + } + + function byteStringSlice(bytes memory c) external pure returns (bytes memory) { + return c.slice(c.byteStringAt(0)); + } + + function mapValue(bytes memory c) external pure returns (uint64) { + return c.mapAt(0).value(); + } + + function isNullAt0(bytes memory c) external pure returns (bool) { + return c.elementAt(0, 0x40, false).isNull(); + } +} + +contract CborDecodeTest is Test { + CborHarness h; + + function setUp() public { + h = new CborHarness(); + } + + function test_byteString_shortForm() public view { + assertEq(h.byteStringLength(hex"43aabbcc"), 3); + assertEq(h.byteStringStart(hex"43aabbcc"), 1); + assertEq(h.byteStringSlice(hex"43aabbcc"), hex"aabbcc"); + } + + function test_byteString_ai24Length() public view { + bytes memory c = abi.encodePacked(hex"5818", new bytes(24)); // 0x40|24, length byte 0x18 = 24 + assertEq(h.byteStringLength(c), 24); + assertEq(h.byteStringStart(c), 2); + } + + function test_unexpectedType_reverts() public { + vm.expectRevert("unexpected type"); + h.byteStringLength(hex"a0"); // map header where a byte string is expected + } + + function test_unsupportedAdditionalInfo_reverts() public { + vm.expectRevert("unsupported type"); + h.mapValue(hex"bc"); // 0xa0|28, additional info 28 is reserved + } + + function test_indefiniteLengthForByteString_reverts() public { + vm.expectRevert("indefinite-length only for maps/arrays"); + h.byteStringLength(hex"5f"); // 0x40|31, indefinite length not allowed for byte strings + } + + function test_nullForRequired_reverts() public { + vm.expectRevert("null value for required element"); + h.byteStringLength(hex"f6"); // null where a value is required + } + + function test_nullWhenAllowed() public view { + assertTrue(h.isNullAt0(hex"f6")); // null permitted -> recognized as null + } + + function test_truncatedLengthByte_reverts() public { + vm.expectRevert(); // cbor[ix+1] out-of-bounds + h.byteStringLength(hex"58"); // ai=24 promises a length byte that is missing + } + + // The element header parses, but a declared length beyond the buffer trips on slice. + function test_declaredLengthExceedsBuffer_reverts() public { + vm.expectRevert("index out of bounds"); + h.byteStringSlice(hex"4a"); // 0x40|10: claims 10 content bytes, none present + } + + function testFuzz_byteString_shortForm(bytes memory payload) public view { + uint256 len = bound(payload.length, 0, 23); // single-byte short-form length + bytes memory content = new bytes(len); + for (uint256 i = 0; i < len; i++) { + content[i] = payload[i]; + } + bytes memory c = abi.encodePacked(bytes1(uint8(0x40 | len)), content); + assertEq(h.byteStringLength(c), len); + assertEq(h.byteStringStart(c), 1); + assertEq(h.byteStringSlice(c), content); + } + + function testFuzz_map_ai24Count(uint8 count) public view { + bytes memory c = abi.encodePacked(hex"b8", bytes1(count)); // 0xa0|24, count entries + assertEq(h.mapValue(c), count); + } +} diff --git a/test/LibBytes.t.sol b/test/LibBytes.t.sol new file mode 100644 index 0000000..928b20f --- /dev/null +++ b/test/LibBytes.t.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.15; + +import {Test} from "forge-std/Test.sol"; +import {stdError} from "forge-std/StdError.sol"; +import {LibBytes} from "../src/LibBytes.sol"; + +contract LibBytesHarness { + using LibBytes for bytes; + + function slice(bytes memory b, uint256 offset, uint256 length) external pure returns (bytes memory) { + return b.slice(offset, length); + } + + function keccakRange(bytes memory b, uint256 offset, uint256 length) external pure returns (bytes32) { + return b.keccak(offset, length); + } + + function readUint16(bytes memory b, uint256 i) external pure returns (uint16) { + return b.readUint16(i); + } + + function readUint32(bytes memory b, uint256 i) external pure returns (uint32) { + return b.readUint32(i); + } + + function readUint64(bytes memory b, uint256 i) external pure returns (uint64) { + return b.readUint64(i); + } +} + +contract LibBytesTest is Test { + LibBytesHarness h; + + function setUp() public { + h = new LibBytesHarness(); + } + + function test_slice_basic() public view { + bytes memory b = hex"00112233445566"; + assertEq(h.slice(b, 2, 3), hex"223344"); + assertEq(h.slice(b, 0, 7), b); + assertEq(h.slice(b, 7, 0), hex""); // offset == length, zero-length slice at the very end + } + + function test_slice_outOfBounds_reverts() public { + bytes memory b = hex"0011"; + vm.expectRevert("index out of bounds"); + h.slice(b, 1, 2); + } + + function test_slice_offsetPastEnd_reverts() public { + bytes memory b = hex"0011"; + vm.expectRevert("index out of bounds"); + h.slice(b, 3, 0); + } + + // offset + length overflowing uint256 must revert (checked arithmetic), not wrap and bypass the bound. + function test_slice_lengthOverflow_reverts() public { + bytes memory b = hex"0011"; + vm.expectRevert(stdError.arithmeticError); + h.slice(b, type(uint256).max, 1); + } + + function test_keccak_matchesReference() public view { + bytes memory b = hex"00112233445566"; + assertEq(h.keccakRange(b, 2, 3), keccak256(hex"223344")); + } + + function test_keccak_outOfBounds_reverts() public { + bytes memory b = hex"0011"; + vm.expectRevert("index out of bounds"); + h.keccakRange(b, 1, 2); + } + + function test_readUint_values() public view { + bytes memory b = hex"0102030405060708090a"; + assertEq(h.readUint16(b, 0), 0x0102); + assertEq(h.readUint32(b, 1), 0x02030405); + assertEq(h.readUint64(b, 2), 0x030405060708090a); + } + + function test_readUint16_outOfBounds_reverts() public { + bytes memory b = hex"00"; + vm.expectRevert("index out of bounds"); + h.readUint16(b, 0); + } + + function test_readUint32_outOfBounds_reverts() public { + bytes memory b = hex"000102"; + vm.expectRevert("index out of bounds"); + h.readUint32(b, 0); + } + + function test_readUint64_outOfBounds_reverts() public { + bytes memory b = hex"00010203040506"; + vm.expectRevert("index out of bounds"); + h.readUint64(b, 0); + } + + function testFuzz_slice_roundTrip(bytes memory data, uint256 offset, uint256 length) public view { + offset = bound(offset, 0, data.length); + length = bound(length, 0, data.length - offset); + bytes memory got = h.slice(data, offset, length); + assertEq(got.length, length); + for (uint256 i = 0; i < length; i++) { + assertEq(got[i], data[offset + i]); + } + } + + function testFuzz_readUint16(uint16 v) public view { + assertEq(h.readUint16(abi.encodePacked(v), 0), v); + } + + function testFuzz_readUint32(uint32 v) public view { + assertEq(h.readUint32(abi.encodePacked(v), 0), v); + } + + function testFuzz_readUint64(uint64 v) public view { + assertEq(h.readUint64(abi.encodePacked(v), 0), v); + } +} From 14b72b65d5c0c93b8716db4e2c2134e4a78ec2ca Mon Sep 17 00:00:00 2001 From: Elena Nadolinski Date: Tue, 9 Jun 2026 21:19:34 -0700 Subject: [PATCH 05/10] docs: sync feature doc with current code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - §3: ECDSA384 is vendored at src/vendor, not a solidity-lib submodule - §8: correct CertManager runtime size (19,394 / margin 5,182) - §9: add the new coverage (cold-expiry, validity boundary, scalar bounds, and DER/CBOR/LibBytes malformed-input + fuzz tests) Generated with Claude Code Co-Authored-By: Claude --- docs/hinted-p384-nitro-attestation.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/hinted-p384-nitro-attestation.md b/docs/hinted-p384-nitro-attestation.md index 7df6bb4..a63c547 100644 --- a/docs/hinted-p384-nitro-attestation.md +++ b/docs/hinted-p384-nitro-attestation.md @@ -94,8 +94,9 @@ Three deployable contracts: - **`P384Verifier`** — all ECDSA-P384 math and hint checking, behind one external call. It is isolated so the parser/cache contracts stay under the EIP-170 code-size - limit. It uses the patched `ECDSA384` library in the `solidity-lib` submodule. - `CertManager` and `NitroValidator` hold **immutable** references to it. + limit. It uses the hint-aware `ECDSA384` library vendored at `src/vendor/ECDSA384.sol` + (see `src/vendor/README.md`). `CertManager` and `NitroValidator` hold **immutable** + references to it. - **`CertManager`** — parses/validates certificates, caches verified ones, and pins the AWS Nitro root. Implements `ICertManager`. - **`NitroValidator`** — parses the CBOR/COSE attestation and drives the @@ -382,7 +383,7 @@ Runtime sizes (`forge build --sizes`); EIP-170 limit is 24,576 bytes: | contract | runtime size | margin | |----------|-------------:|-------:| | `P384Verifier` | 7,805 | 16,771 | -| `CertManager` | 19,372 | 5,204 | +| `CertManager` | 19,394 | 5,182 | | `NitroValidator` | 14,062 | 10,514 | (Test-only helper contracts are not part of the deployable contract set.) @@ -391,9 +392,13 @@ Runtime sizes (`forge build --sizes`); EIP-170 limit is 24,576 bytes: The hinted contracts are exercised against the real fixture and adversarial inputs. Covered failure modes: mutated hint, truncated hint, surplus hint, wrong parent hash, -expired cached cert, CA/client role mismatch, missing warm cache, invalid final -signature, disabled unhinted entrypoints, EIP-170 fit, and off-chain↔on-chain hint -equivalence. +expired cached cert, expired cert on first (cold) verification, the `notAfter` validity +boundary, CA/client role mismatch, missing warm cache, invalid final signature, +out-of-range ECDSA scalars (`r=0`, `r≥n`, `s=0`, `s>lowSmax`), disabled unhinted +entrypoints, EIP-170 fit, and off-chain↔on-chain hint equivalence. The DER, CBOR, and +byte-slicing parsers additionally have direct unit and fuzz tests for malformed and +out-of-bounds input (`test/Asn1Decode.t.sol`, `test/CborDecode.t.sol`, +`test/LibBytes.t.sol`). | invariant | component | how it is tested | |-----------|-----------|------------------| @@ -403,6 +408,9 @@ equivalence. | hinted verifier matches the original accept/reject set | `P384Verifier` | accepts a valid signature; rejects mutated hash / signature / public key | | no unhinted fallback via hinted entrypoints | `CertManager` | the unhinted entrypoints revert | | warm validation requires cached certs | `NitroValidator` | empty-hint final validation reverts when a cert is uncached | +| out-of-range scalars are rejected | `P384Verifier` | `r=0` / `r≥n` / `s=0` / `s>lowSmax` signatures return false | +| certificate validity is enforced at the boundary | `CertManager` | cold-path expiry reverts; valid at `notAfter`, expired at `notAfter+1` | +| parsers reject malformed / out-of-bounds input | `Asn1Decode`, `CborDecode`, `LibBytes` | direct unit + fuzz tests for bad tags, lengths, types, and slices | | off-chain generator matches on-chain order, rejects bad input | off-chain generator | equivalence test + negative-input checks | The off-chain↔on-chain hint equivalence is checked under an FFI test (it shells out to From 86ddb8446c207b155191792081185a749f9ebf23 Mon Sep 17 00:00:00 2001 From: Elena Nadolinski Date: Tue, 9 Jun 2026 21:39:26 -0700 Subject: [PATCH 06/10] docs: add plain-language "Intuition" section to the feature doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A non-cryptographer's walkthrough of hinted P-384 (factoring analogy + mod-7 worked example + why it's safe), placed before the formal sections, with cross-references into §4 so it complements rather than repeats the spec and soundness argument. Generated with Claude Code Co-Authored-By: Claude --- docs/hinted-p384-nitro-attestation.md | 104 ++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/docs/hinted-p384-nitro-attestation.md b/docs/hinted-p384-nitro-attestation.md index a63c547..bfe1a37 100644 --- a/docs/hinted-p384-nitro-attestation.md +++ b/docs/hinted-p384-nitro-attestation.md @@ -22,6 +22,110 @@ precompile, and that is exactly what the Fusaka upgrade reprices. --- +## Intuition: how hinting works in plain terms + +*A non-cryptographer's walkthrough of the idea. §1 onward give the formal construction, +measured gas, and proofs.* + +### Some things are hard to find but easy to check + +Think about factoring. Ask "what are the factors of 589?" and you have to grind. But +hand someone the answer — 19 and 31 — and they confirm it with one multiplication: +19 × 31 = 589 ✓. + +- Finding the answer is hard / expensive. +- Checking a proposed answer is easy / cheap. + +A **hint** is exactly this: instead of making the contract *find* a hard value, the +caller finds it off-chain and hands it over, and the contract only *checks* it. + +### Our hard thing: a modular inverse + +The P-384 verifier constantly divides in modular arithmetic — `a / b mod m`. Division +there means "multiply by the inverse of `b`", where the inverse `inv` is the number with +`b · inv ≡ 1 (mod m)`. + +Take a tiny modulus `m = 7` and find the inverse of `b = 3`: + +- **The hard way (what the contract used to do):** compute `inv = b^(m−2) mod m` + (Fermat's little theorem) = `3^5 mod 7`. `3^5 = 243`, and `243 mod 7 = 5` — several + multiplications to get there. +- **The easy way (checking a proposed answer):** someone hands you `inv = 5`. One + multiply: `3 · 5 = 15`, `15 mod 7 = 1` ✓ — done. + +> Same asymmetry as factoring: *finding* 5 took exponentiation; *checking* 5 took one +> multiply. + +### Why that gap is huge on-chain + +Now scale up: the real modulus is a 384-bit number, not 7. + +- **Finding** the inverse (`b^(m−2)`) means raising to a 384-bit power — hundreds of + big-number multiplications via the EVM's `MODEXP` precompile, which Fusaka made ~10× + more expensive. +- **Checking** a proposed inverse is still one big-number multiply — cheap, and Fusaka + barely touches it. + +So each inverse goes from hundreds of expensive operations to one cheap one. + +### "But can we trust the hint?" — no, and we don't have to + +The caller could lie and send `inv = 4`. The contract checks: + +``` +3 · 4 = 12, 12 mod 7 = 5 ≠ 1 ✗ → revert +``` + +A wrong hint fails the check and the transaction reverts. A malicious caller can waste +their own gas, but can never push a wrong value through. The hint is a *proposal, +validated before use* — nothing is trusted. + +### How it's used in the real verifier + +One P-384 signature verify needs ~570 of these inverses. + +- **Before (no hints):** the contract computed all ~570 on-chain → ~570 expensive + `MODEXP` calls. This is what blew past the gas cap. +- **After (with hints):** the caller computes all ~570 off-chain (free, on their own + machine) and sends them as a list in calldata; the contract pops them one at a time + and checks each with a single multiply. + +The list is just values back-to-back, consumed in order: + +``` +caller sends: [ inv_1 , inv_2 , inv_3 , … , inv_570 ] + +contract: needs 1/b_1 → take inv_1, check b_1 · inv_1 ≡ 1, use it + needs 1/b_2 → take inv_2, check b_2 · inv_2 ≡ 1, use it + needs 1/b_3 → take inv_3, check b_3 · inv_3 ≡ 1, use it + … +``` + +There are no labels — position `i` is for the `i`-th inverse the contract needs, because +it always does its work in the same deterministic order. (§4 gives the exact stream +format — 48-byte big-endian values — and the two guardrails that force the count to be +exact: it reverts if the list runs out early or has leftover values.) + +### Why this is provably safe + +Because `m` is **prime**, every nonzero number has exactly *one* inverse — there is no +second valid answer. So if a hint passes `b · inv ≡ 1 (mod m)`, it must be the one true +inverse, bit-for-bit identical to what the contract would have computed itself. The +hinted verifier therefore accepts exactly the same signatures as the original — cheaper, +with nothing changed about what it accepts or rejects, so no new way to forge anything. +(§4, *Why this is sound*, states this formally.) + +> **One-line recap.** Computing a modular inverse is expensive; checking one is a single +> multiply. So the caller computes the ~570 inverses off-chain and submits them as hints; +> the contract verifies each (`b · inv ≡ 1`) before using it. Wrong hints revert, and +> since a prime modulus has a unique inverse, a passing hint is guaranteed to be the real +> one. +> +> The contract stops being a *solver* and becomes a *checker* — and checking is what +> blockchains are cheap at. + +--- + ## 1. Why this exists A P384 ECDSA verify is dominated by **modular inversions** computed on-chain via the From 04a76fbd134a099edadc921ed138c1c7cb021f9c Mon Sep 17 00:00:00 2001 From: Elena Nadolinski Date: Tue, 9 Jun 2026 21:46:56 -0700 Subject: [PATCH 07/10] docs: emphasize the ~384-bit exponent and add per-signature hint table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the Intuition section: highlight that the on-chain exponent m-2 is itself a ~384-bit number (~10^115) — the reason the MODEXP step is so costly — and add the 48-byte hint definition plus the per-signature hint-bytes / inverse-count table for a cold attestation. Generated with Claude Code Co-Authored-By: Claude --- docs/hinted-p384-nitro-attestation.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/hinted-p384-nitro-attestation.md b/docs/hinted-p384-nitro-attestation.md index bfe1a37..8a29362 100644 --- a/docs/hinted-p384-nitro-attestation.md +++ b/docs/hinted-p384-nitro-attestation.md @@ -49,7 +49,10 @@ Take a tiny modulus `m = 7` and find the inverse of `b = 3`: - **The hard way (what the contract used to do):** compute `inv = b^(m−2) mod m` (Fermat's little theorem) = `3^5 mod 7`. `3^5 = 243`, and `243 mod 7 = 5` — several - multiplications to get there. + multiplications to get there. In this toy example the exponent `m−2` is just `5`, but + for real P-384 the modulus is a **384-bit** number, so the exponent `m−2` is *also* + ~384 bits — a value around 10¹¹⁵. Raising to a power *that* size is what makes the step + so expensive (and it's why the curve is called *p384*). - **The easy way (checking a proposed answer):** someone hands you `inv = 5`. One multiply: `3 · 5 = 15`, `15 mod 7 = 1` ✓ — done. @@ -60,9 +63,9 @@ Take a tiny modulus `m = 7` and find the inverse of `b = 3`: Now scale up: the real modulus is a 384-bit number, not 7. -- **Finding** the inverse (`b^(m−2)`) means raising to a 384-bit power — hundreds of - big-number multiplications via the EVM's `MODEXP` precompile, which Fusaka made ~10× - more expensive. +- **Finding** the inverse (`b^(m−2)`) means raising to that ~384-bit exponent — hundreds + of big-number multiplications via the EVM's `MODEXP` precompile, which Fusaka made ~10× + more expensive. And a single signature verify does this ~570 times. - **Checking** a proposed inverse is still one big-number multiply — cheap, and Fusaka barely touches it. @@ -90,6 +93,20 @@ One P-384 signature verify needs ~570 of these inverses. machine) and sends them as a list in calldata; the contract pops them one at a time and checks each with a single multiply. +Each hint is a **48-byte number** (the inverse), and a verify needs ~570 of them, so the +calldata is roughly 570 × 48 ≈ 27 KB per signature. A cold attestation has five such +signatures: + +| signature | hint bytes | inverses (÷ 48) | +|---|---:|---:| +| CA cert 1 | 27,456 | 572 | +| CA cert 2 | 27,408 | 571 | +| CA cert 3 | 27,408 | 571 | +| client / leaf cert | 27,504 | 573 | +| COSE document | 27,312 | 569 | + +(§6 maps these five signatures to transactions; §7 has their gas.) + The list is just values back-to-back, consumed in order: ``` From 10e9a06f66a56e320e9aeaf4410d34d13fc80634 Mon Sep 17 00:00:00 2001 From: Elena Nadolinski Date: Tue, 9 Jun 2026 22:50:38 -0700 Subject: [PATCH 08/10] docs: condense "Preparing calls off-chain" section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trim §5 to the idea (hints are replayed off-chain, re-checked on-chain, so the generator is liveness-only and language-agnostic) plus a brief note that a reference implementation is bundled. Drops the codebase-specific CLI flags, step-by-step prep lists, JSON schema, and Go/Rust details. Generated with Claude Code Co-Authored-By: Claude --- docs/hinted-p384-nitro-attestation.md | 93 ++++----------------------- 1 file changed, 12 insertions(+), 81 deletions(-) diff --git a/docs/hinted-p384-nitro-attestation.md b/docs/hinted-p384-nitro-attestation.md index 8a29362..2ec9141 100644 --- a/docs/hinted-p384-nitro-attestation.md +++ b/docs/hinted-p384-nitro-attestation.md @@ -307,87 +307,18 @@ pre-verified value* for the same inverse the original computes. ## 5. Preparing calls off-chain -An off-chain hint generator (Node.js `BigInt`, no dependencies) reproduces the -verifier's execution order and emits the packed stream. In this repository it is -`tools/p384_hints.js`: - -```sh -node tools/p384_hints.js verify --hash <0x 48B> --signature <0x r‖s> --pubkey <0x x‖y> -node tools/p384_hints.js cert --cert <0x DER | base64 | @file> --pubkey <0x parent x‖y> -node tools/p384_hints.js attestation --attestation <0x COSE | base64 | @file> --pubkey <0x leaf x‖y> -``` - -`cert` mode SHA-384-hashes the DER TBS certificate and packs the DER signature into -`r‖s`; `attestation` mode reconstructs the COSE `Sig_structure` Nitro signs and -hashes it. The generator only has to get the **order and count** right — every value -is re-checked on-chain (§4), so a generator bug causes a revert, never a false accept. - -The CLI is a reference implementation and demo convenience, not a requirement of the -on-chain design. In production, the caller service should implement the same -deterministic hint generation in its own off-chain stack (for example Go or Rust), -or use `tools/p384_hints.js` as a byte-for-byte reference while porting. The smart -contracts only see ordinary calldata: - -```solidity -verifyCACertWithHints(bytes cert, bytes32 parentCertHash, bytes signatureHints) -verifyClientCertWithHints(bytes cert, bytes32 parentCertHash, bytes signatureHints) -validateAttestationWithHints(bytes attestationTbs, bytes signature, bytes attestationHints) -``` - -So the production service prepares: - -1. the DER certificates from the Nitro `cabundle` and `certificate`; -2. the parent certificate hashes (`keccak256(derCert)`); -3. one packed inverse-hint stream per uncached certificate signature; -4. the COSE `Sig_structure` hash, the document signature, and its hint stream; and -5. the ABI-encoded calls for the cold or warm sequence in §6. - -The service can also call `loadVerified(certHash)` before submitting transactions to -choose the shortest path: full cold chain, cached CA chain plus new leaf, or fully -warm document validation. - -For integration testing and porting, `tools/hinted_attestation_calls.js` builds the -full transaction plan from one Nitro attestation: - -```sh -node tools/hinted_attestation_calls.js prepare \ - --attestation <0x COSE | base64 | @file> \ - --cert-manager <0x CertManager> \ - --validator <0x NitroValidator> -``` - -It outputs JSON with `cold` and `warm` arrays. Each item contains: - -- `to`: target contract address; -- `function`: Solidity function signature; -- `args`: decoded ABI arguments, including the packed hint stream; -- `calldata`: ready-to-submit ABI calldata; -- `hintBytes` / `hintCount`: the witness size. - -The bundled fixture can be prepared with: - -```sh -node tools/hinted_attestation_calls.js fixture \ - --cert-manager <0x CertManager> \ - --validator <0x NitroValidator> -``` - -This preparer is also reference tooling, not a production dependency. A production -Go or Rust service should implement the same deterministic steps in-process: - -1. decode the Nitro COSE_Sign1 envelope and payload; -2. extract the `cabundle` DER certificates and leaf `certificate`; -3. compute `keccak256(derCert)` identities for cache lookups and parent hashes; -4. compute inverse hints for every cert/document signature that will be verified in - the chosen transaction sequence; -5. ABI-pack the hinted contract calls; and -6. submit the calls in dependency order. - -In Go this maps naturally to `abi.Pack` plus a Keccak implementation from the -Ethereum stack; in Rust, to an ABI encoder such as `alloy-sol-types` / `ethers` -and the corresponding Keccak primitive. The Solidity contracts do not know or care -which language produced the bytes — malformed hints or mismatched calldata simply -revert on-chain. +Hints are produced off-chain by replaying the verifier's deterministic execution order +and emitting the packed 48-byte inverse stream. The generator only has to get the +**order and count** right: every value is re-checked on-chain (§4), so it is trusted for +*liveness*, not correctness — a bug can cause a revert, never a false accept. A +production caller can therefore implement it in whatever language its backend uses; the +contracts only ever see ordinary calldata via the `*WithHints` entrypoints. + +A reference implementation is included in this repo: `tools/p384_hints.js` (Node.js, no +dependencies) generates the hint stream for a raw signature, a certificate, or a full +attestation, and a companion script assembles a ready-to-submit transaction plan (the +cold/warm sequences of §6) from one attestation. These are reference and demo tooling, +not a production dependency — use them as a byte-for-byte oracle when porting. ## 6. The attestation verification flow From 4bb60c3ff3a4fe4147027575574c70a489afe171 Mon Sep 17 00:00:00 2001 From: Elena Nadolinski Date: Thu, 11 Jun 2026 12:08:28 -0700 Subject: [PATCH 09/10] Address hinted P-384 review feedback --- .github/workflows/test.yml | 8 +++++ README.md | 12 ++++--- docs/hinted-p384-nitro-attestation.md | 29 +++++++++------- script/BaseSepoliaDemo.s.sol | 1 + src/CertManager.sol | 43 +++++++++++++++++------- src/NitroValidator.sol | 4 +-- test/hinted/HintedNitroAttestation.t.sol | 32 ++++++++++++++++++ 7 files changed, 96 insertions(+), 33 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a5ac046..2d70a28 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,6 +60,14 @@ jobs: # Asserts the off-chain hint generator (tools/p384_hints.js) produces byte-identical # hints to the on-chain Solidity reference; guards against the two drifting apart. # Node.js is preinstalled on ubuntu-latest and the tools use only Node built-ins. + - name: Smoke-test off-chain tools + run: | + node --check tools/nitro_attestation_input.js + node --check tools/hinted_attestation_calls.js + node tools/nitro_attestation_input.js fixture > /dev/null + node tools/hinted_attestation_calls.js fixture > /dev/null + id: tools + - name: Run FFI parity tests env: NITRO_RUN_FFI: "true" diff --git a/README.md b/README.md index 792d811..69d72b0 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,9 @@ own document signature. 1. **Cold phase — verify & cache the certificate chain** (once per chain). For each CA cert in the `cabundle`, then the leaf cert, compute the inverse hints off-chain and submit them: - - `certManager.verifyCACertWithHints(caCert, parentCertHash, hints)` (use `0` as the parent hash - for the cert signed by the pinned root) + - `certManager.verifyCACertWithHints(caCert, parentCertHash, hints)` (use + `ROOT_CA_CERT_HASH` / `keccak256(rootCert)` for the first non-root CA; `0` is only for the + pinned root itself) - `certManager.verifyClientCertWithHints(leafCert, parentCertHash, hints)` 2. **Validation — verify the document signature.** Once the chain is cached: - `validator.validateAttestationWithHints(attestationTbs, signature, attestationHints)` @@ -101,7 +102,7 @@ contract Validator { bytes32 pcr0 = attestationTbs.keccak(ptrs.pcrs[0]); require(pcr0 == PCR0, "invalid pcr0 in attestation"); - require(ptrs.timestamp + MAX_AGE > block.timestamp, "attestation too old"); + require(ptrs.timestamp / 1000 + MAX_AGE > block.timestamp, "attestation too old"); bytes memory publicKey = attestationTbs.slice(ptrs.publicKey); // do something with the public key, user data, etc @@ -132,8 +133,9 @@ additional submodules are required beyond `forge-std`. Verification proves an attestation is genuine; some properties are intentionally left to the integrator (see [docs](docs/hinted-p384-nitro-attestation.md#integrator-responsibilities-what-the-contract-does-not-enforce)): -- **Freshness / replay** — the contract does not compare the attestation `timestamp` to - `block.timestamp` or match the `nonce` to a challenge. Enforce freshness yourself if you need it. +- **Freshness / replay** — the contract does not compare the attestation `timestamp` (milliseconds) + to `block.timestamp` (seconds) or match the `nonce` to a challenge. Enforce freshness yourself if + you need it. - **Signature malleability** — low-S is not enforced (AWS does not emit low-S), so the malleable twin `(r, n-s)` also verifies. Never use the signature as a uniqueness key; dedupe on canonical attestation fields instead. diff --git a/docs/hinted-p384-nitro-attestation.md b/docs/hinted-p384-nitro-attestation.md index 2ec9141..cd827ca 100644 --- a/docs/hinted-p384-nitro-attestation.md +++ b/docs/hinted-p384-nitro-attestation.md @@ -11,7 +11,7 @@ P-384 curve (secp384r1). P-384 verification leans heavily on the EVM's MODEXP precompile, and that is exactly what the Fusaka upgrade reprices. - **Problem:** the Fusaka MODEXP repricing makes a standard on-chain P-384 verifier - far too expensive to land on an OP-Stack L2 such as Base. + far too expensive to land on an L2 such as Base. - **Idea:** the expensive step (modular inversion) is *hard to compute but easy to check*, so the caller supplies it as a verified **hint**. - **Result:** a full cold verification fits in **5 transactions, each ≤ ~13.8M gas** @@ -358,7 +358,8 @@ document signature. Once the leaf and its CA chain are cached and unexpired, a later attestation signed by the same leaf is a **single transaction** (`validateAttestationWithHints`) carrying only the COSE signature hints. The cabundle certs are not re-verified — they are -reloaded by `keccak256(cert)` identity and their cached metadata is re-checked. +reloaded by `keccak256(cert)` identity, checked against their original cached parent, +and their cached metadata is re-checked. Practical reuse cases: @@ -368,9 +369,10 @@ Practical reuse cases: - **CA chain and leaf cached:** validate the document only → 1 transaction. **Cache reuse** is allowed when: the submitted DER hashes to a cached cert; the cert -is unexpired (`notAfter ≥ block.timestamp`); the cached CA/client role matches; parent -checks pass for non-root certs. The cache is global on-chain state — once any caller -verifies a cert, others reuse it until expiry. +is unexpired (`notAfter ≥ block.timestamp`); the cached CA/client role matches; and +`parentCertHash` matches the parent used during cold verification. The cache is global +on-chain state — once any caller verifies a cert, others reuse it until expiry, but +only under the same parent binding. **Warm-only guard.** `validateAttestationWithHints` re-runs the cabundle checks with an *empty* hint stream. Cached certs return before signature verification; a missing cert @@ -484,11 +486,10 @@ can never silently diverge. ## 10. Caveats and notes -- **Calldata cost is separate from the gas above.** The tables in §7 are *execution* - gas. Each signature also carries ~27 KB of hint calldata, which on an OP-Stack L2 - incurs an L1 data-availability fee on top of execution gas — budget for it - separately. It does not affect whether a transaction fits the per-transaction - execution-gas cap. +- **Calldata contributes to transaction gas.** The tables in §7 are receipt gas and + include the intrinsic gas for the ~27 KB hint calldata carried by each signature. + On Base (as an L2), calldata also incurs an L1 data-availability fee on top of EVM + gas — budget for that separately. - **Acceptance rule is inherited unchanged.** The verifier accepts when `x_R mod n == r`, exactly as the upstream library, including its handling of the negligible (~2⁻¹⁹⁰) case where the recovered x-coordinate lies in `[n, p)`. Hinting @@ -509,9 +510,10 @@ deliberately left to the caller and must be handled in the consuming contract: - **Freshness / anti-replay.** `validateAttestationWithHints` only checks that `timestamp` is non-zero and that `nonce` is within a size bound; it never compares - `timestamp` to `block.timestamp` nor matches `nonce` to a challenge. A valid + `timestamp` (milliseconds) to `block.timestamp` (seconds) nor matches `nonce` to a + challenge. A valid attestation can be replayed until its short-lived leaf certificate expires. If you - need freshness, compare `ptrs.timestamp` to `block.timestamp` and/or verify + need freshness, compare `ptrs.timestamp / 1000` to `block.timestamp` and/or verify `ptrs.nonce` against a value you issued. - **Signature malleability.** Low-S is intentionally not enforced (AWS does not guarantee low-S; see `CURVE_LOW_S_MAX` in `ECDSA384Curve.sol`), so for a valid @@ -538,7 +540,8 @@ COSE hints with `tools/p384_hints.js attestation`; submits the final same cached chain and leaf. The script uses `vm.ffi` only because Foundry Solidity scripts cannot run the off-chain parsing and BigInt witness code in-process. In the production flow, the caller service prepares the same hints and ABI calldata before -submitting ordinary transactions. +submitting ordinary transactions. Run the demo with Foundry FFI enabled (`--ffi`); +without it, Foundry disables `vm.ffi` by default. --- diff --git a/script/BaseSepoliaDemo.s.sol b/script/BaseSepoliaDemo.s.sol index 9e55111..067aa8a 100644 --- a/script/BaseSepoliaDemo.s.sol +++ b/script/BaseSepoliaDemo.s.sol @@ -18,6 +18,7 @@ contract NitroValidatorScriptParser is NitroValidator { } } +/// @dev Uses vm.ffi to run the off-chain hint tools; invoke the script with Foundry's `--ffi` flag. contract BaseSepoliaDemo is Script { using CborDecode for bytes; using LibBytes for bytes; diff --git a/src/CertManager.sol b/src/CertManager.sol index cc2edf3..658d1bf 100644 --- a/src/CertManager.sol +++ b/src/CertManager.sol @@ -46,6 +46,8 @@ contract CertManager is ICertManager { // certHash -> VerifiedCert mapping(bytes32 => bytes) public verified; + // certHash -> parent cert hash used during cold verification + mapping(bytes32 => bytes32) internal verifiedParent; IP384Verifier public immutable p384Verifier; @@ -77,28 +79,29 @@ contract CertManager is ICertManager { /// @notice Verify a CA certificate against its (already-cached) parent and cache the result. /// @dev Idempotent with a cache short-circuit: if `cert` is already verified and unexpired, the - /// cached record is returned and `signatureHints` is ignored (so "" is valid on a re-call). - /// On a cold cert, `signatureHints` must contain the real off-chain inverse hints for the - /// cert signature; they are re-verified on-chain, so a wrong hint only reverts. Pass the - /// root's hash (or 0 for the pinned root) as `parentCertHash`. + /// cached record is returned and `signatureHints` is ignored, but `parentCertHash` must + /// match the parent used during cold verification. On a cold cert, `signatureHints` must + /// contain the real off-chain inverse hints for the cert signature; they are re-verified + /// on-chain, so a wrong hint only reverts. Pass 0 only when submitting the pinned root; + /// otherwise pass the cached parent cert hash. function verifyCACertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) external returns (bytes32) { bytes32 certHash = keccak256(cert); - _verifyCert(cert, certHash, true, _loadVerified(parentCertHash), signatureHints); + _verifyCert(cert, certHash, true, parentCertHash, signatureHints); return certHash; } /// @notice Verify a leaf (client) certificate against its (already-cached) parent and cache it. /// @dev Same cache short-circuit and hint semantics as {verifyCACertWithHints}: on a cold cert /// `signatureHints` must hold the real off-chain inverse hints (re-verified on-chain); on a - /// cached cert they are ignored. + /// cached cert they are ignored, but `parentCertHash` must match the cold verification parent. function verifyClientCertWithHints(bytes memory cert, bytes32 parentCertHash, bytes memory signatureHints) external returns (VerifiedCert memory) { - return _verifyCert(cert, keccak256(cert), false, _loadVerified(parentCertHash), signatureHints); + return _verifyCert(cert, keccak256(cert), false, parentCertHash, signatureHints); } function loadVerified(bytes32 certHash) external view returns (VerifiedCert memory) { @@ -109,9 +112,10 @@ contract CertManager is ICertManager { bytes memory certificate, bytes32 certHash, bool ca, - VerifiedCert memory parent, + bytes32 parentCertHash, bytes memory signatureHints ) internal returns (VerifiedCert memory) { + VerifiedCert memory parent = _loadVerified(parentCertHash); if (certHash != ROOT_CA_CERT_HASH) { require(parent.pubKey.length > 0, "parent cert unverified"); require(!_certificateExpired(parent.notAfter), "parent cert expired"); @@ -124,9 +128,27 @@ contract CertManager is ICertManager { if (cert.pubKey.length != 0) { require(!_certificateExpired(cert.notAfter), "cert expired"); require(cert.ca == ca, "cert is not a CA"); + if (certHash != ROOT_CA_CERT_HASH) { + require(verifiedParent[certHash] == parentCertHash, "parent cert mismatch"); + } return cert; } + cert = _verifyUncachedCert(certificate, ca, parent, signatureHints); + _saveVerified(certHash, cert); + verifiedParent[certHash] = parentCertHash; + + emit CertVerified(certHash); + + return cert; + } + + function _verifyUncachedCert( + bytes memory certificate, + bool ca, + VerifiedCert memory parent, + bytes memory signatureHints + ) internal view returns (VerifiedCert memory cert) { Asn1Ptr root = certificate.root(); Asn1Ptr tbsCertPtr = certificate.firstChildOf(root); (uint64 notAfter, int64 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey) = @@ -144,11 +166,6 @@ contract CertManager is ICertManager { cert = VerifiedCert({ ca: ca, notAfter: notAfter, maxPathLen: maxPathLen, subjectHash: subjectHash, pubKey: pubKey }); - _saveVerified(certHash, cert); - - emit CertVerified(certHash); - - return cert; } function _parseTbs(bytes memory certificate, Asn1Ptr ptr, bool ca) diff --git a/src/NitroValidator.sol b/src/NitroValidator.sol index f4ddf0c..a760895 100644 --- a/src/NitroValidator.sol +++ b/src/NitroValidator.sol @@ -91,8 +91,8 @@ contract NitroValidator { /// well-formed, but deliberately does NOT enforce: /// - Freshness / anti-replay: `ptrs.timestamp` is only checked to be non-zero and `nonce` /// is only length-bounded. A valid attestation can be replayed until its leaf cert - /// expires. Callers that need freshness must compare `ptrs.timestamp` to `block.timestamp` - /// and/or match `ptrs.nonce` against a challenge they issued. + /// expires. Callers that need freshness must compare `ptrs.timestamp / 1000` to + /// `block.timestamp` and/or match `ptrs.nonce` against a challenge they issued. /// - Signature non-malleability: low-S is not enforced (see {ECDSA384Curve.CURVE_LOW_S_MAX}), /// so do not use `signature` (or its hash) as a uniqueness key — dedupe on attestation /// fields instead. diff --git a/test/hinted/HintedNitroAttestation.t.sol b/test/hinted/HintedNitroAttestation.t.sol index 84c9efc..44f35a2 100644 --- a/test/hinted/HintedNitroAttestation.t.sol +++ b/test/hinted/HintedNitroAttestation.t.sol @@ -103,6 +103,38 @@ contract HintedNitroAttestationTest is Test { certManager.verifyCACertWithHints(caCert, bytes32(0), ""); } + function test_HintedCACertRejectsCachedParentMismatch() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + + bytes memory rootCert = attestationTbs.slice(ptrs.cabundle[0]); + bytes32 rootHash = keccak256(rootCert); + bytes memory ca1 = attestationTbs.slice(ptrs.cabundle[1]); + bytes memory ca1Hints = hintCollector.collectCertSignatureHints(ca1, certManager.loadVerified(rootHash).pubKey); + bytes32 ca1Hash = certManager.verifyCACertWithHints(ca1, rootHash, ca1Hints); + + bytes memory ca2 = attestationTbs.slice(ptrs.cabundle[2]); + bytes memory ca2Hints = hintCollector.collectCertSignatureHints(ca2, certManager.loadVerified(ca1Hash).pubKey); + certManager.verifyCACertWithHints(ca2, ca1Hash, ca2Hints); + + vm.expectRevert("parent cert mismatch"); + certManager.verifyCACertWithHints(ca2, rootHash, ""); + } + + function test_HintedClientCertRejectsCachedParentMismatch() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + _cacheCertBundleWithHints(attestationTbs); + + bytes memory rootCert = attestationTbs.slice(ptrs.cabundle[0]); + bytes memory clientCert = attestationTbs.slice(ptrs.cert); + + vm.expectRevert("parent cert mismatch"); + certManager.verifyClientCertWithHints(clientCert, keccak256(rootCert), ""); + } + function test_HintedCACertRejectsExpiredCachedCert() public { bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); (bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation); From edaad2fd8b17c9e3407220d4ba3354026ab2ea3d Mon Sep 17 00:00:00 2001 From: Elena Nadolinski Date: Thu, 11 Jun 2026 23:13:07 -0700 Subject: [PATCH 10/10] Update hinted Nitro size table --- docs/hinted-p384-nitro-attestation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hinted-p384-nitro-attestation.md b/docs/hinted-p384-nitro-attestation.md index cd827ca..efaf1cd 100644 --- a/docs/hinted-p384-nitro-attestation.md +++ b/docs/hinted-p384-nitro-attestation.md @@ -437,7 +437,7 @@ Runtime sizes (`forge build --sizes`); EIP-170 limit is 24,576 bytes: | contract | runtime size | margin | |----------|-------------:|-------:| | `P384Verifier` | 7,805 | 16,771 | -| `CertManager` | 19,394 | 5,182 | +| `CertManager` | 19,620 | 4,956 | | `NitroValidator` | 14,062 | 10,514 | (Test-only helper contracts are not part of the deployable contract set.)