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/.github/workflows/test.yml b/.github/workflows/test.yml index 509dde8..2d70a28 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,3 +43,34 @@ 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: 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" + run: | + forge test --ffi -vvv --match-test test_OffchainWitness + id: ffi 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..888d42d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +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/dl-solarity/solidity-lib 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 23d383e..69d72b0 100644 --- a/README.md +++ b/README.md @@ -3,56 +3,150 @@ 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 + `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)` + - **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"); + 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 } } ``` -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); + +## 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` (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. +- **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 -forge install forge build ``` @@ -61,3 +155,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/docs/hinted-p384-nitro-attestation.md b/docs/hinted-p384-nitro-attestation.md new file mode 100644 index 0000000..efaf1cd --- /dev/null +++ b/docs/hinted-p384-nitro-attestation.md @@ -0,0 +1,625 @@ +# 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 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. + +--- + +## 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. 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. + +> 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 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. + +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. + +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: + +``` +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 +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 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 + 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 + +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 + +### 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, checked against their original cached parent, +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; 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 +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,620 | 4,956 | +| `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, 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 | +|-----------|-----------|------------------| +| 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 | +| 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 +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 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 + 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. + +### 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` (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 / 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 + 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 +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. Run the demo with Foundry FFI enabled (`--ffi`); +without it, Foundry disables `vm.ffi` by default. + +--- + +## Appendix: the code change + +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 +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/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 b947571..0000000 --- a/lib/solidity-lib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b94757194de6436062c2d68118c0352be84ac4be 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 new file mode 100644 index 0000000..067aa8a --- /dev/null +++ b/script/BaseSepoliaDemo.s.sol @@ -0,0 +1,157 @@ +// 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 "../test/helpers/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); + } +} + +/// @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; + 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..658d1bf 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 @@ -47,8 +46,14 @@ contract CertManager is ICertManager { // certHash -> VerifiedCert mapping(bytes32 => bytes) public verified; + // certHash -> parent cert hash used during cold verification + mapping(bytes32 => bytes32) internal verifiedParent; - 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 +66,59 @@ 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; + /// @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"); } - function verifyClientCert(bytes memory cert, bytes32 parentCertHash) external returns (VerifiedCert memory) { - return _verifyCert(cert, keccak256(cert), false, _loadVerified(parentCertHash)); + /// @notice DEPRECATED — always reverts. Use {verifyClientCertWithHints}. + 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 + /// @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, 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, 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, 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, parentCertHash, signatureHints); + } + + function loadVerified(bytes32 certHash) external view returns (VerifiedCert memory) { + return _loadVerified(certHash); + } + + function _verifyCert( + bytes memory certificate, + bytes32 certHash, + bool ca, + 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(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,11 +126,29 @@ 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"); + 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) = @@ -102,16 +161,11 @@ 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 }); - _saveVerified(certHash, cert); - - emit CertVerified(certHash); - - return cert; } function _parseTbs(bytes memory certificate, Asn1Ptr ptr, bool ca) @@ -191,7 +245,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 +329,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 +356,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/ECDSA384Curve.sol b/src/ECDSA384Curve.sol index 948792d..8e19b03 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) @@ -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 04927f6..e072abe 100644 --- a/src/ICertManager.sol +++ b/src/ICertManager.sol @@ -10,7 +10,23 @@ interface ICertManager { bytes pubKey; } + // --- Active (hinted) entrypoints --- + + 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); + + // --- 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/IP384Verifier.sol b/src/IP384Verifier.sol new file mode 100644 index 0000000..1512877 --- /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 view returns (bool); +} diff --git a/src/NitroValidator.sol b/src/NitroValidator.sol index f5b8f57..a760895 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,40 @@ contract NitroValidator { signature = attestation.slice(signaturePtr.start(), signaturePtr.length()); } - function validateAttestation(bytes memory attestationTbs, bytes memory signature) public returns (Ptrs memory) { + /// @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"); + } + + /// @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. + /// @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 / 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. + /// - 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 + /// on-chain, so a wrong hint only reverts and can never forge a valid signature. + 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 +134,30 @@ 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) + /// @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) { 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 +247,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/P384Verifier.sol b/src/P384Verifier.sol new file mode 100644 index 0000000..d815303 --- /dev/null +++ b/src/P384Verifier.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {ECDSA384Curve} from "./ECDSA384Curve.sol"; +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, + bytes memory pubKey, + bytes memory inverseHints + ) external view returns (bool) { + return ECDSA384.verifyWithHints(_p384(), hash, signature, pubKey, inverseHints); + } + + function _p384() internal pure returns (ECDSA384.Parameters memory) { + return ECDSA384.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/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/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/CertManager.t.sol b/test/CertManager.t.sol index a0aabb0..60ba0c6 100644 --- a/test/CertManager.t.sol +++ b/test/CertManager.t.sol @@ -1,9 +1,12 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; +pragma solidity ^0.8.26; import {Test, console} from "forge-std/Test.sol"; import {CertManager} from "../src/CertManager.sol"; +import {ICertManager} from "../src/ICertManager.sol"; import {Asn1Decode, LibAsn1Ptr, Asn1Ptr} from "../src/Asn1Decode.sol"; +import {P384Verifier} from "../src/P384Verifier.sol"; +import {P384HintCollector} from "./helpers/HintedNitroTestHelpers.sol"; contract Asn1DecodeHarness { using Asn1Decode for bytes; @@ -19,42 +22,12 @@ contract Asn1DecodeHarness { } contract CertManagerTest is Test { - CertManager public certManager; Asn1DecodeHarness public harness; function setUp() public { - vm.warp(1732580000); - certManager = new CertManager(); harness = new Asn1DecodeHarness(); } - 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)); - } - // 's' INTEGER from cabundle[3] (2026-04-02 attestation): DER-encoded with a 0x00 // sign-padding byte, leaving valueLength=47. Verifies hi/lo are correctly zero-padded // to the full 48-byte scalar rather than packed flush against the stripped bytes. @@ -69,6 +42,8 @@ contract CertManagerTest is Test { } // Cert chain from the 2026-04-02 ~15:35 UTC dev attestation that produced the live revert. + // CB0 is the AWS Nitro root (keccak256(CB0) == CertManager.ROOT_CA_CERT_HASH, pinned in the + // constructor), so the chain is verified starting from CB1. bytes constant CB0 = hex"3082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff6"; bytes constant CB1 = @@ -78,14 +53,31 @@ contract CertManagerTest is Test { bytes constant CB3 = hex"308202bf30820245a003020102021500f2a021cc6a8466d5fe18e8f487b975f35c7c5b05300a06082a8648ce3d040303308189313c303a06035504030c33396162323162376434663562373139622e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c65301e170d3236303430323135303431355a170d3236303430333135303431355a30818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30396535656634623664666630323535622e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b810400220362000420ecb0a645c3e6b4437c1c4149c3f1d0af3e90f0343b0e1e3a47d8b314420266843c345a1e64b01b43331f911ab537b78b355a2419eeb00f1ecd6d4fbd7186ab41603ff2a0430cab7b597362a1054ed4cb8ff32dd76b2c108224b42648bbf026a366306430120603551d130101ff040830060101ff020100300e0603551d0f0101ff040403020204301d0603551d0e0416041499b2d026ad0212b670d2550fb292ca51fdbf8d66301f0603551d230418301680146a2a38eb4940a886ad9e57e4b7da9dde7f4eb700300a06082a8648ce3d040303036800306502310096ca96c46b5a05de3ad9ecdeaab8670916137461d306cf2fcd8a308885eb6063d96de2e28a1a4ad8c2214e1d1479b5b8023000caf59019bfbcc6f6ed365e5a892ceaa2eda9c549dc01460f5fe650814ebe0e7ee855d3bcffde95afd2e82e21df0eac"; - // CB3's 's' component has a 47-byte DER encoding; unpatched code reverts with "invalid sig". - function test_VerifyCACert_ShortS_Regression() public { + // CB3's 's' component has a 47-byte DER encoding; unpatched Asn1Decode.uint384At misreads it + // and the corrupted scalar is rejected by ECDSA384.verify, reverting with "invalid sig". + // Verified through the hinted flow (verifyCACertWithHints) since this branch removed the + // non-hinted verification path. + function test_VerifyCACertWithHints_ShortS_Regression() public { vm.warp(1775145600); - CertManager cm = new CertManager(); - cm.verifyCACert(CB0, 0); - cm.verifyCACert(CB1, keccak256(CB0)); - cm.verifyCACert(CB2, keccak256(CB1)); - cm.verifyCACert(CB3, keccak256(CB2)); // reverts with "invalid sig" on unpatched code + CertManager cm = new CertManager(new P384Verifier()); + P384HintCollector collector = new P384HintCollector(); + + // CB0 (AWS Nitro root) is pinned in the constructor. + bytes32 parentHash = keccak256(CB0); + assertEq(parentHash, cm.ROOT_CA_CERT_HASH(), "CB0 must be the pinned root"); + + parentHash = _verifyCA(cm, collector, CB1, parentHash); + parentHash = _verifyCA(cm, collector, CB2, parentHash); + _verifyCA(cm, collector, CB3, parentHash); // reverts with "invalid sig" on unpatched code + } + + function _verifyCA(CertManager cm, P384HintCollector collector, bytes memory cert, bytes32 parentHash) + internal + returns (bytes32) + { + bytes memory parentPubKey = cm.loadVerified(parentHash).pubKey; + bytes memory hints = collector.collectCertSignatureHints(cert, parentPubKey); + return cm.verifyCACertWithHints(cert, parentHash, hints); } function testFuzz_uint384At_LeadingZeros(uint8 numZeros, uint128 hiSeed, uint256 loSeed) public view { 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/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); + } +} 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/helpers/CertManagerDemo.sol b/test/helpers/CertManagerDemo.sol new file mode 100644 index 0000000..40ef0ca --- /dev/null +++ b/test/helpers/CertManagerDemo.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +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. +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/helpers/ECDSA384HintCollector.sol b/test/helpers/ECDSA384HintCollector.sol new file mode 100644 index 0000000..002c072 --- /dev/null +++ b/test/helpers/ECDSA384HintCollector.sol @@ -0,0 +1,1086 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {MemoryUtils} from "../../src/vendor/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 ECDSA384HintCollectorLib { + using MemoryUtils for *; + using U384HintCollector 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) = U384HintCollector.init2(signature_); + (inputs_.x, inputs_.y) = U384HintCollector.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_ + ? U384HintCollector.initCallWithHints(params_.p, inverseHints_) + : U384HintCollector.initCall(params_.p); + + /// accept s only from the lower part of the curve + if ( + 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, U384HintCollector.hintCursor(call)); + } + + if (!_isOnCurve(call, params_.p, params_.a, params_.b, inputs_.x, inputs_.y)) { + return (false, U384HintCollector.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 = U384HintCollector.moddiv(call, hashedMessage_.init(), inputs_.s, params_.n); + uint256 scalar2 = U384HintCollector.moddiv(call, inputs_.r, inputs_.s, params_.n); + _checkpoint(1); + + { + 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( + 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); + } + + U384HintCollector.modAssign(call, scalar1, params_.n); + _checkpoint(4); + + return (U384HintCollector.eq(scalar1, inputs_.r), U384HintCollector.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 ( + U384HintCollector.eqInteger(x, 0) || U384HintCollector.eq(x, p) || U384HintCollector.eqInteger(y, 0) + || U384HintCollector.eq(y, p) + ) { + return false; + } + + uint256 LHS = U384HintCollector.modexp(call, y, 2); + uint256 RHS = U384HintCollector.modexp(call, x, 3); + + if (!U384HintCollector.eqInteger(a, 0)) { + RHS = U384HintCollector.modadd(RHS, U384HintCollector.modmul(call, x, a), p); // x^3 + a*x + } + + if (!U384HintCollector.eqInteger(b, 0)) { + RHS = U384HintCollector.modadd(RHS, b, p); // x^3 + a*x + b + } + + return U384HintCollector.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 (U384HintCollector.eqInteger(y1, 0)) { + return (0, 0); + } + + uint256 m1 = U384HintCollector.modexp(call, x1, 2); + U384HintCollector.modmulAssign(call, m1, three); + U384HintCollector.modaddAssign(m1, a, p); + + uint256 m2 = U384HintCollector.modshl1(y1, p); + U384HintCollector.moddivAssign(call, m1, m2); + + x2 = U384HintCollector.modexp(call, m1, 2); + U384HintCollector.modsubAssign(x2, x1, p); + U384HintCollector.modsubAssign(x2, x1, p); + + y2 = U384HintCollector.modsub(x1, x2, p); + U384HintCollector.modmulAssign(call, y2, m1); + U384HintCollector.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 (U384HintCollector.eqInteger(y1, 0)) { + return (0, 0); + } + + uint256 m1 = U384HintCollector.modexp(call, x1, 2); + U384HintCollector.modmulAssign(call, m1, three); + U384HintCollector.modaddAssign(m1, a, p); + + uint256 m2 = U384HintCollector.modshl1(y1, p); + U384HintCollector.moddivAssign(call, m1, m2); + + x2 = U384HintCollector.modexp(call, m1, 2); + U384HintCollector.modsubAssign(x2, x1, p); + U384HintCollector.modsubAssign(x2, x1, p); + + y2 = U384HintCollector.modsub(x1, x2, p); + U384HintCollector.modmulAssign(call, y2, m1); + U384HintCollector.modsubAssign(y2, y1, p); + + if (U384HintCollector.eqInteger(y2, 0)) { + return (0, 0); + } + + U384HintCollector.modexpAssignTo(call, m1, x2, 2); + U384HintCollector.modmulAssign(call, m1, three); + U384HintCollector.modaddAssign(m1, a, p); + + U384HintCollector.modshl1AssignTo(m2, y2, p); + U384HintCollector.moddivAssign(call, m1, m2); + + U384HintCollector.modexpAssignTo(call, x1, m1, 2); + U384HintCollector.modsubAssign(x1, x2, p); + U384HintCollector.modsubAssign(x1, x2, p); + + U384HintCollector.modsubAssignTo(y1, x2, x1, p); + U384HintCollector.modmulAssign(call, y1, m1); + U384HintCollector.modsubAssign(y1, y2, p); + + if (U384HintCollector.eqInteger(y1, 0)) { + return (0, 0); + } + + U384HintCollector.modexpAssignTo(call, m1, x1, 2); + U384HintCollector.modmulAssign(call, m1, three); + U384HintCollector.modaddAssign(m1, a, p); + + U384HintCollector.modshl1AssignTo(m2, y1, p); + U384HintCollector.moddivAssign(call, m1, m2); + + U384HintCollector.modexpAssignTo(call, x2, m1, 2); + U384HintCollector.modsubAssign(x2, x1, p); + U384HintCollector.modsubAssign(x2, x1, p); + + U384HintCollector.modsubAssignTo(y2, x1, x2, p); + U384HintCollector.modmulAssign(call, y2, m1); + U384HintCollector.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 (U384HintCollector.eq(x1, x2)) { + if (U384HintCollector.eq(y1, y2)) { + return _twiceAffine(call, p, three, a, x1, y1); + } + + return (0, 0); + } + + uint256 m1 = U384HintCollector.modsub(y1, y2, p); + uint256 m2 = U384HintCollector.modsub(x1, x2, p); + + U384HintCollector.moddivAssign(call, m1, m2); + + x3 = U384HintCollector.modexp(call, m1, 2); + U384HintCollector.modsubAssign(x3, x1, p); + U384HintCollector.modsubAssign(x3, x2, p); + + y3 = U384HintCollector.modsub(x1, x3, p); + U384HintCollector.modmulAssign(call, y3, m1); + U384HintCollector.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 U384HintCollector { + 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/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/hinted/HintedNitroAttestation.t.sol b/test/hinted/HintedNitroAttestation.t.sol new file mode 100644 index 0000000..44f35a2 --- /dev/null +++ b/test/hinted/HintedNitroAttestation.t.sol @@ -0,0 +1,731 @@ +// 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 {CertManagerDemo} from "../helpers/CertManagerDemo.sol"; +import {ICertManager} from "../../src/ICertManager.sol"; +import {CborDecode} from "../../src/CborDecode.sol"; +import {P384Verifier} from "../../src/P384Verifier.sol"; +import {Sha2Ext} from "../../src/Sha2Ext.sol"; +import {P384HintCollector} from "../helpers/HintedNitroTestHelpers.sol"; + +contract NitroValidatorParseHarness is NitroValidator { + constructor(CertManager certManager, P384Verifier p384Verifier) NitroValidator(certManager, p384Verifier) {} + + function parseAttestation(bytes memory attestationTbs) external pure returns (Ptrs memory) { + return _parseAttestation(attestationTbs); + } +} + +contract HintedNitroAttestationTest is Test { + using CborDecode for bytes; + + 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; + P384Verifier p384Verifier; + P384HintCollector 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. + p384Verifier = new P384Verifier(); + certManager = new CertManager(p384Verifier); + validator = new NitroValidator(certManager, p384Verifier); + parser = new NitroValidatorParseHarness(certManager, p384Verifier); + hintCollector = new P384HintCollector(); + } + + 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); + + 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"); + validator.validateAttestationWithHints(attestationTbs, signature, attestationHints); + } + + function test_HintedCACertRejectsMutatedHint() 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"); + certManager.verifyCACertWithHints(caCert, parentHash, hints); + } + + function test_HintedCACertRejectsTruncatedHint() 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"); + certManager.verifyCACertWithHints(caCert, parentHash, hints); + } + + 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"); + 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); + 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); + 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"); + certManager.verifyCACertWithHints(caCert, parentHash, ""); + } + + 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); + certManager.verifyCACertWithHints(caCert, parentHash, hints); + + vm.expectRevert("cert is not a CA"); + certManager.verifyClientCertWithHints(caCert, parentHash, ""); + } + + function test_HintedValidationRequiresWarmCache() public { + bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64())); + (bytes memory attestationTbs, bytes memory signature) = validator.decodeAttestationTbs(attestation); + CertManager freshCertManager = new CertManager(p384Verifier); + NitroValidator freshValidator = new NitroValidator(freshCertManager, p384Verifier); + + vm.expectRevert("inverse hint underflow"); + freshValidator.validateAttestationWithHints(attestationTbs, signature, ""); + } + + 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); + 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(); + validator.validateAttestationWithHints(attestationTbs, signature, attestationHints); + } + + function test_DeployableContractsFitEIP170() public view { + console.log("==== DEPLOYABLE CONTRACT SIZES ===="); + console.log("P384Verifier runtime bytes :", address(p384Verifier).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(certManager).code.length, EIP170_RUNTIME_LIMIT); + assertLe(address(validator).code.length, EIP170_RUNTIME_LIMIT); + } + + function test_DeployableCertManagerDisablesUnhintedEntrypoints() public { + vm.expectRevert("use hinted cert verification"); + certManager.verifyCACert("", bytes32(0)); + + vm.expectRevert("use hinted cert verification"); + certManager.verifyClientCert("", bytes32(0)); + + vm.expectRevert("use hinted attestation verification"); + 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 ===="); + 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); + NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs); + + bytes memory rootCert = attestationTbs.slice(ptrs.cabundle[0]); + bytes32 parentHash = keccak256(rootCert); + bytes memory parentPubKey = certManager.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 = certManager.verifyCACertWithHints(caCert, parentHash, offchainHints); + parentPubKey = certManager.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 = + certManager.verifyClientCertWithHints(clientCert, parentHash, offchainClientHints); + signaturesChecked += 1; + + _assertOffchainAttestationHints(attestation, attestationTbs, signature, leaf.pubKey); + signaturesChecked += 1; + + console.log("==== OFFCHAIN WITNESS GENERATOR ===="); + console.log("signatures checked :", signaturesChecked); + } + + 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); + + 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)); + 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 _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 = certManager.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 = 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 :"); + 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 = certManager.verifyCACertWithHints(caCert, parentHash, hints); + uint256 currentGas = g0 - gasleft(); + parent = certManager.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 = certManager.verifyClientCertWithHints(clientCert, parentHash, clientHints); + uint256 clientCurrentGas = g0 - gasleft(); + + bytes32 leafHash = keccak256(clientCert); + 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"); + + 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(); + 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); + 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 = certManager.loadVerified(parentHash).pubKey; + hints = hintCollector.collectCertSignatureHints(caCert, parentPubKey); + } + parentHash = certManager.verifyCACertWithHints(caCert, parentHash, hints); + } + + bytes memory clientCert = attestationTbs.slice(ptrs.cert); + parentPubKey = certManager.loadVerified(parentHash).pubKey; + bytes memory clientHints = hintCollector.collectCertSignatureHints(clientCert, parentPubKey); + 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(), "/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 _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(), "/tools/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"); + + 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(), "/tools/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 _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 + // 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"; + } +} 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/tools/p384_hints.js b/tools/p384_hints.js new file mode 100644 index 0000000..2945d9b --- /dev/null +++ b/tools/p384_hints.js @@ -0,0 +1,761 @@ +#!/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; + +if (require.main === module) { + 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 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. +`); +} + +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 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; + } + + 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 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) { + 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), + 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) { + throw new Error("CBOR read out of bounds"); + } + const major = initial >> 5; + const ai = initial & 0x1f; + 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; + 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; + 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}`); + } + + if (end > bytes.length) { + throw new Error("CBOR item length out of bounds"); + } + + return { major, ai, value, indefinite, 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 }; + } + if (ai === 31) { + return { value: 0n, headerLength: 1, indefinite: true }; + } + 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}`); +} + +module.exports = { + collectAttestationHintBytes, + collectCertSignatureHintBytes, + collectVerifyHintBytes, + parseAttestationPayload, + parseAttestationSignature, + parseCertPublicKey, + parseCertSignature, + readBytes, +};