Skip to content

Repository files navigation

cip8-verifier

A Cardano wallet key owning an EVM Safe: on-chain verification of CIP-8/COSE signData signatures from Cardano Ed25519 keys, and a set-once registry that binds such a key to a Safe as an ERC-1271 owner.

Self-contained Foundry project: no network access needed — all dependencies are vendored in lib/ (see PINS below). From the repo root:

forge test -vv          # 128 tests; GAS lines match gas-output.txt (up to test-scheduling order)
                        # (the differential campaign replay skips unless CAMPAIGN_DIR is set —
                        #  docs/REVIEW.md, The differential campaign)

Security status

Unaudited as a whole. The vendored curve core (SCL) carries upstream audit provenance — see the PINS table, including its caveats. The composed system built on it — the registry, the CIP-8 envelope verification, the ERC-1271 surface, and the deployment machinery — is original to this repository and has not yet received an independent external audit. One is planned, and its report will be published in this repository regardless of outcome. Until then, the deployments recorded in docs/DEPLOYMENT.md are test deployments: review them, attack them, reproduce them — but do not gate a high-value account on this code.

Documentation

  • docs/ARCHITECTURE.md — what the system does and how the pieces relate
  • docs/CRYPTO_SPEC.md — the exact bytes and acceptance rules
  • docs/THREAT_MODEL.md — attacker model and the numbered invariants (INV-NN)
  • docs/REVIEW.md — how changes are tested, reviewed, and adjudicated
  • docs/DEPLOYMENT.md — deterministic deployment procedure and the record of live deployments
  • docs/WALLET_SURVEY.md — per-wallet provenance behind the accepted header shapes
  • AGENTS.md — house rules for every contributor, human or agent
  • SECURITY.md — scope of adversarial work and vulnerability reporting

Contents

  • src/CardanoOwnerRegistry.sol — the candidate production contract: per-Safe set-once registration running the sequence of docs/CRYPTO_SPEC.md, Registration (credential, key acceptance, header template, proof of possession over a payload naming the Safe and chain), plus the ERC-1271 isValidSignature(bytes32,bytes) interface with the bounded decode of docs/CRYPTO_SPEC.md, ERC-1271 wire format, which never reverts on adversarial input.
  • src/Ed25519.sol — expansion and the verification equation, first-party over the vendored SCL curve primitives, with libsodium's acceptance rules. Its header is the single owner of the deviation list (reject-only, fail-closed).
  • src/Blake2b224.sol — BLAKE2b-224 of 32 bytes via the EIP-152 precompile.
  • bench/comparators/ — third-party verifiers (mel-project/ed25519-sol, Near-One/rainbow-bridge) kept verbatim for benchmarking; their divergence from libsodium is pinned in their tests as EXPECTED assertions, not logs.
  • test/Core.t.sol — the acceptance-set suite: all 205 libsodium-valid vectors must verify, all 16 libsodium-rejected vectors and the 16 forgeries against the verification equation must refuse (assertFalse; a revert counts as refusal, acceptance fails the build).
  • test/EdgeSuite.t.sol — the fixed ed25519-speccheck differential corpus: 12 hash-pinned upstream cases plus seven generated torsion-bearing cases must match their recorded PyNaCl/libsodium verdict; none of them carries a deviation.
  • test/WeakKeys.t.sol — deviation 3: the twenty-key table recomputed from the curve, and two oracle-accepted signatures per key refused at expansion beside accepted adjacent controls.
  • test/Registry.t.sol — registration invariants + ERC-1271 behavior.
  • test/EnvelopeOracle.t.sol — the CIP-30 envelope differential corpus replayed: recorded verdicts from an independent COSE implementation (the Cardano Foundation CIP-30 DataSignature parser, pinned by jar sha256) are re-asserted through the registry's own entry points, the oracle's independently serialized Sig_structure bytes must equal CardanoCip8.sigStructure's wherever it parsed an envelope, and every vector it accepts that this repository rejects carries a named narrowness class (docs/REVIEW.md, The envelope oracle).
  • test/SafeIntegration.t.sol — the registry as owner of real Safes: the canonical Safe v1.5.0 singleton and proxy factory (and, as comparative evidence that pre-v1.5.0 Safes cannot use it, the v1.4.1 singleton) etched from the hash-pinned vendored runtimes in fixtures/safe_artifacts.json; deterministic factory provisioning, registration and owner actions through real execTransaction, selector-trace evidence that v1.5.0 calls only isValidSignature(bytes32,bytes) while v1.4.1 demands the unimplemented legacy interface and reverts, and the owner-threshold firing matrix (docs/THREAT_MODEL.md, Deployment assumptions).
  • fixtures/ — the committed vector corpus; scripts/ — its generators.
  • tools/lint.py — the mechanical half of AGENTS.md, over every file the repository owns, tracked or unignored (vendored trees, fixtures and captures excepted; ignored products such as campaign/ are never read): every cited test and path exists, every Foundry test is named by the convention and asserts, no revert expectation is bare, no hex literal is retyped in a suite or generator, every reason string in src/ is named by a test, every gas figure in prose is labeled, no box-drawing character or curly quote anywhere. CI runs its self-test (a synthetic tree planting one instance of every violation class beside ignored paths that must stay silent) and then the lint, and fails on any finding.

Wallet compatibility

Condensed from docs/WALLET_SURVEY.md, which holds the evidence tier and provenance behind every cell. Registration payloads are 73 bytes and owner-action payloads 86: a wallet path that hashes payloads at or below those sizes emits signatures this registry cannot accept.

Wallet Evidence Software path Hardware paths
Eternl live capture registered + validated on the live test deployment not surveyed
Lace live capture registered + validated on the live test deployment not surveyed
Typhon shipping bundle compatible, byte-verified Ledger compatible (never hashes); Trezor not surveyed
GameChanger shipping bundle compatible, byte-reproduced Ledger + Trezor compatible (one self-verifying builder, never hashes)
GeroWallet source compatible, byte-verified Ledger compatible (hashes only at ≥ 198 B); Trezor undetermined — COSE built in firmware
Vespr source compatible, golden-vector verified Ledger + Keystone incompatible — they hash at ≥ 50 B, below this protocol's payload sizes

Capture rows are the strongest tier: exercised end to end against a live deployment. Bundle and source rows are byte-verified against the pinned shipping artifact but not yet exercised live.

Regenerating fixtures (only needed if the corpus changes)

Needs uv. Each script pins its own dependencies inline (PEP 723), so uv run scripts/<name>.py is the entire invocation — no venv to prepare, and the pinned versions apply regardless of any active environment. Generation is deterministic (fixed seeds, fixed keys): rerunning the scripts must leave fixtures/ byte-identical. Scripts read and write fixtures/:

  • scripts/parse_rfc.py → fixtures/rfc_vectors.json from fixtures/rfc8032.txt (vectors are extracted from the RFC text mechanically, never transcribed).
  • scripts/gen_vectors.py → fixtures/vectors.json (205 valid) + fixtures/adversarial.json (16, each asserted libsodium-REJECTED before being written) + its section of fixtures/edge_report.txt (the oracle's characterization; every generator below that extends it owns one section, so the file is byte-identical whatever order they run in).
  • scripts/gen_blake2b.py → fixtures/blake2b_vectors.json + the H_INIT constant.
  • scripts/gen_poc.py → fixtures/poc.json (registry fixtures; addresses derived from blake2b224(pk); the legacy-interface hash is keccak256 of the fixture's own legacyData, computed with pycryptodome, and the suite hashes the same bytes on-chain before accepting the signature over it).
  • scripts/gen_wallet_fixtures.py → fixtures/wallet_e2e.json (real shipping-wallet signatures, extracted from the CIP-30 captures under e2e/captures/ and libsodium-verified before emission; exercised by test/WalletE2E.t.sol under the chain id and account each payload commits to).
  • scripts/gen_boundary.py → fixtures/boundary_points.json (small-order, negative-zero, non-canonical and non-square point encodings, each classification proven by the script's own Edwards arithmetic) + fixtures/small_order_forgeries.json (one equation-holding, nonzero-scalar forgery for every decompressible small-order encoding, each required to be rejected by the pinned libsodium oracle before emission).
  • scripts/gen_edge_suite.py → fixtures/edge_suite.json from the verbatim, hash-pinned fixtures/ed25519_speccheck_cases.json; it records and rechecks PyNaCl/libsodium's verdict and computes the deviation flag for every tuple from its key bytes, generates an accepted mixed-order family spanning all seven nonidentity torsion components, and extends fixtures/edge_report.txt with the oracle verdict and expected verifier result per edge condition.
  • scripts/gen_equation_forgeries.py → fixtures/equation_forgeries.json (forgeries against the verification equation, every one asserted libsodium-REJECTED and checked against the equation it targets before writing: neutral recomputations, mixed-order tuples satisfying the scalar-subtraction equation, and small-order R with the equation holding).
  • scripts/gen_weak_keys.py → fixtures/weak_keys.json (deviation 3: the twenty-key table with its labels and the simulated precomputation failures, plus two libsodium-accepted signatures per table key and per adjacent control; the deviation column is table membership computed from each vector's key bytes). --solidity prints the table as the body of Ed25519.isWeakKey.
  • scripts/gen_campaign.py → fixtures/campaign_report.txt + fixtures/campaign_manifest.json (chunk count, per-chunk sha256, vector count — what the replay verifies) + the gitignored 100,000-vector differential corpus they identify (docs/REVIEW.md, The differential campaign).
  • scripts/gen_campaign_check.py → fixtures/campaign_check/ (the small committed corpus pinning the manifest verification's sensitivity: a good replay plus its four mechanically derived tampered variants, generated with the campaign generator's own oracle-asserting machinery).
  • scripts/gen_safe_fixtures.py → fixtures/safe_suite.json (CIP-8 signatures binding the deterministic Safe addresses the integration suite provisions, plus the Safe EIP-712 transaction hashes they authorize; every signature libsodium-verified before emission, every address and hash re-derived in-suite from the etched canonical bytecode). It reads fixtures/safe_artifacts.json, which is not a generator output but a vendored retrieval pin — canonical Safe runtime bytecode with its provenance recorded inside the file and in the PINS table — so regeneration stays fully offline.
  • scripts/gen_envelope_oracle.py → fixtures/envelope_oracle.json (the CIP-30 envelope differential corpus: the real captures, a synthesized acceptance matrix over address types, header templates, payload domains and COSE tagging, and adversarial envelope mutations — every vector judged both by the off-chain pipeline, e2e/parse_signdata.parse, and by the independent CIP-30 oracle before emission, with the P1/P2/P3 gates of docs/REVIEW.md failing generation on any unexplained disagreement). The one generator needing more than uv: a Java 17+ runtime on PATH and, on the first run only, network access to fetch the three pinned jars from Maven Central into ~/.cache/cip8-verifier/; every use, cached or fresh, re-verifies their sha256 against the constants in the script, and the committed scripts/EnvelopeOracle.java driver is compiled by the pinned ecj at generation time. Build and test never touch Java.
  • scripts/cip8.py and scripts/oracle.py are not generators but what the generators import: the CIP-8 envelope (both header templates, the Sig_structure, both payload grammars) built byte for byte as CardanoCip8 builds it and asserted equal to cbor2's serialization on every call, and the libsodium verdict convention. The envelope oracle's runner and the off-chain gate e2e/parse_signdata.py deliberately import neither (docs/REVIEW.md, The envelope oracle); the gate's own refusals are exercised by uv run e2e/parse_signdata.py --selftest, which CI runs beside the regeneration.

PINS

Component Version / commit Provenance
forge 1.7.1 foundryup
solc 0.8.36 (pinned in foundry.toml) foundry-managed
evm_version cancun (pinned) —
optimizer on, runs = 1000000 —
lib/crypto-lib get-smooth/crypto-lib @ d714e9824e2e2e44be3c8fd498e0de651ddc9425 (2024-12-14), subset: src/ minus libMPC/, libSCL_BIP327.sol, Ed25519DelegationEIP7702.sol; plus external/. One deliberate diff against upstream: the seven public functions of libSCL_EIP6565.sol are flattened to internal, inlining the EdDSA core into its callers — no separately deployed library, no delegatecall, single-artifact CREATE2 deployment. Guarded by test_registryArtifactSelfContained, which fails without the flattening The EdDSA-relevant tree at this commit is byte-identical to the post-remediation audit state 248f2ba (2024-09-16, "Veridise last report"); the VERIDISE-002 s-range fix is commit c7552d0. The report's named review base c559657 resolves to c55965773c9fbb21bd3d037c2d027907456a6149 (2024-07-22): reachable from no branch or tag, but fetchable by SHA (git fetch origin c55965773c9fbb21bd3d037c2d027907456a6149), so the exact reviewed tree is obtainable and verifiable. Its direct child on that same unmerged line, 0af3c3cae84b29a14fa374a29824dc3abbb3d586 ("VERIDISE 002: check s value range", 2024-08-21), carries tree c87081cb947f4937e831597bbac923771c6ff78d — byte-identical to mainline c7552d0, an ancestor of 248f2ba and of the pinned commit. The reviewed line never merged as commits; its remediated tree is exactly the mainline state this pin descends from (verified 2026-08-29 against a fresh bare clone: fetch-by-SHA, branch/tag reachability, tree hashes, ancestry). The Veridise reports are silent on cofactor, torsion and subgroup handling and record that the reviewed source suite included EdDSA integration tests but Wycheproof vectors only for ECDSA; no audit conclusion is claimed for cofactor or torsion handling, which this repository pins against libsodium itself. The verification equation is composed first-party in src/Ed25519.sol from the vendored HashInternal, ecGenMulmuladdB4W, WeierStrass2Edwards and edCompress; the vendored Verify_LE is no longer called (docs/CRYPTO_SPEC.md, Signature acceptance)
lib/forge-std foundry-rs/forge-std master @ 37a36ca389095b2f677abb07642634573ba7e265 (2026-08-07): tag v1.16.2 (bf647bd6046f2f7da30d0c2bf435e5c76a780c1b, 2026-06-30) plus six later master commits — four workflow-only dependency bumps and two source changes, b96a46a954876eea1c0ffb48c63e1dfbac593781 (Vm interface update, 2026-07-20) and 37a36ca389095b2f677abb07642634573ba7e265 (StdStorage short-slot fix); six paths differ from the tag: lib/forge-std/src/Vm.sol, lib/forge-std/src/StdStorage.sol, their two tests, and two workflow files under the upstream .github/ The vendored tree is byte-identical to that commit's tree, .git excluded (verified 2026-09-03 against a fresh clone with a recursive diff: zero differences). Test and script tooling only: nothing in src/ imports it
bench/comparators/mel mel-project/ed25519-sol @ 3b36ec8c361c8fe29e6eff10019521454f888ff5 (2022-07-14, the tip of main at verification); Ed25519.sol sha256 42445e313d0a3684207ab455f04cc9e7a7075532d8956e0683274c26ee8efe67, Sha512.sol sha256 96a74497a31f38c57bbf989e66575ae72338bbce950cbbf9bc202cb52cd91eb8 Both files byte-identical to the upstream files of the same names under its src/ directory at that commit (git blob hashes equal, verified 2026-09-03). Benchmark material only, never imported by src/; its divergence from libsodium is pinned as expected in test/Mel.t.sol
bench/comparators/near Near-One/rainbow-bridge @ 63fb73ba3c3ced07b57c3040a5c6cdaff5d0f255 (2021-11-11), path contracts/eth/nearbridge/contracts/Ed25519.sol; Ed25519.sol sha256 e778b62f584609811cf18c78ec3a1271d087f391f88fbbaab23533900bb21efe; the same file blob is present at 0c6b0cfcb200e69a8d062c259fd3138d9bbfbde6 (2026-04-09) Byte-identical to the upstream file at the pinned commit (blob hash equal, verified 2026-09-03). Benchmark material only; GPL-3.0-or-later governs only itself (THIRD_PARTY_LICENSES.md); its divergence from libsodium is pinned as expected in test/Near.t.sol
PyNaCl 1.5.0 (bundles libsodium 1.0.18 — same lineage cardano-node pins) PyPI via uv, pinned in each script's PEP 723 header
Taming the many EdDSAs vectors novifinancial/ed25519-speccheck @ 65519336fda78a3d016e947df6d82848aca0c9da; cases.json sha256 08e47a36d9aead288664930505584f353fff113ab854f2800db1e4f5b3540450 The upstream file is vendored byte-identically as fixtures/ed25519_speccheck_cases.json; the generator verifies the digest before deriving the fixture and obtains every expectation independently from the pinned PyNaCl/libsodium oracle
cbor2 5.9.0 (the serialization every template and Sig_structure built by scripts/cip8.py is asserted equal to; the framing implementation of e2e/parse_signdata.py and scripts/gen_envelope_oracle.py, which import nothing from that builder; decoder in gen_wallet_fixtures.py and for the Byron fixture in gen_poc.py) PyPI via uv, pinned inline
pycryptodome 3.23.0 (keccak256 in gen_safe_fixtures.py for EIP-712 and CREATE2, and in gen_poc.py for the legacy-interface fixture hash; the suite re-derives every hash on the etched bytecode or on the fixture bytes) PyPI via uv, pinned inline
ecdsa 0.19.1 (secp256k1 address derivation for the suite's operator EOA in gen_safe_fixtures.py; the suite asserts vm.addr agrees) PyPI via uv, pinned inline
Safe v1.5.0 singleton runtime safe-smart-account tag v1.5.0 = commit dc437e8fba8b4805d76bcbd1c668c9fd3d1e83be; canonical address 0xFf51A5898e281Db6DfC7855790607438dF2ca44b; runtime keccak256 0xdda019cbd7c867a533a2a86e5c53434fdc50b13122b5a5ddb4a8df61b31c20f2, sha256 0xe4bb8e7d6bd13577e196f5c1d718add9fb2e007f09bff25faf06c1100b1409c7 (both also inside fixtures/safe_artifacts.json) eth_getCode 2026-08-28 from Etherscan API v2 (chain 1), Blockscout (chain 1) and the Base public RPC (chain 8453), all byte-identical; keccak256 equals the codeHash pinned in safe-global/safe-deployments @ 0974182c16c57ca6fe2b9bba8cffb8a7e55fb83c; VERSION() and both digests re-checked by test/SafeIntegration.t.sol on every run
Safe proxy factory v1.5.0 runtime same tag/commit; canonical address 0x14F2982D601c9458F93bd70B218933A6f8165e7b; runtime keccak256 0x967dae4cda22b0c9ef7f31b010bdc1ceb0af9904b0c3dc060b5302e4c18a4529, sha256 0xd7a2beaef456f3e3ff69c98f258b99b8453e16e9df58d153c0b2b4f366bd34ca; includes proxyCreationCode() output (fetched via eth_call from two of the same sources, byte-identical) same retrieval and safe-deployments cross-check; the suite asserts the etched factory returns the committed proxy creation code
Safe v1.4.1 singleton runtime (pre-v1.5.0 incompatibility evidence) safe-smart-account tag v1.4.1 = commit bf943f80fec5ac647159d26161446ac5d716a294; canonical address 0x41675C099F32341bf84BFc5382aF534df5C7461a; runtime keccak256 0x1fe2df852ba3299d6534ef416eefa406e56ced995bca886ab7a553e6d0c5e1c4, sha256 0x79756c276443953adde61bbc2bfa134c03b35cf45181dab9d4f9503df22f45c8 same retrieval and safe-deployments cross-check; kept solely as evidence that pre-v1.5.0 Safes validate contract owners through isValidSignature(bytes,bytes), which this registry does not implement
CIP-30 envelope oracle org.cardanofoundation:cip30-data-signature-parser 0.0.12 (release tag v0.0.12 = commit 33d6c9558575d42adc838c16eb9a51a6960bd5f1); shaded jar sha256 68ee9016b6bc27a57326007872eb7e27c72ecdef583649fd53f6f0134d5942b0 MIT, Cardano Foundation; fetched from Maven Central and digest-verified on every use by scripts/gen_envelope_oracle.py; generation-time only, never in the build, test or deployment path. Its COSE parsing (co.nstant.in.cbor) and Ed25519 backend (net.i2p.crypto eddsa), both shaded into the jar, share no code with libsodium, SCL or cbor2 — the independence the corpus exists to exercise. Companions pinned the same way: slf4j-api 2.0.5 (jar sha256 f4a2974509291acc49fda4a79b0d59e15e2b524095d6421c66391b92387af4c9, the parser's declared logging API) and ecj 3.46.0 (jar sha256 d0d43f8e2d7003e5efed612e2cbb5f01870043397d8f1bbe536fd9128f4fcbf7, compiles the committed runner on any Java 17+ runtime)
Slither 0.11.6 PyPI via uvx; CI static analysis, with vendored-path triage in slither.config.json

Slither runs all 102 detectors over the production compilation and fails CI on any unsuppressed first-party result: the CI command (uvx --from slither-analyzer==0.11.6 slither . --warn-unused-ignores, with slither.config.json in force) reports 0 results, and the job log is the record. slither.config.json excludes results whose source lies in the pinned vendored tree; it is not a static-analysis claim over that code. As a dated characterization, not a committed measurement and not a gate: an unfiltered run (--filter-paths '^$') under the same pins on 2026-09-03 reported 51 results. Fifty are wholly vendored: 46 informational and four medium — three false uninitialized-local reports for Solidity-zeroed fixed-size memory arrays and one intentional (len / 128) * 128 block-flooring operation in external/sha512/Sha2Ext.sol. The fifty-first is an incorrect-versions-of-solidity result spanning the vendored tree and all four first-party sources; the path filter drops that one too, so the gate makes no pragma-range claim — the range is pinned by solc in foundry.toml instead. The three first-party suppressions are source-local: the required blake2f assembly call, its exact EIP-152 input words, and the exact Ed25519 group order. --warn-unused-ignores keeps obsolete suppressions visible.

gas-output.txt is the committed benchmark output under exactly this configuration, its header naming the forge, solc, EVM version and optimizer pins it was produced under; every figure labeled measured comes from it.

License

MIT (LICENSE) for everything original to this repository. Vendored components and their licenses are enumerated in THIRD_PARTY_LICENSES.md.

About

A Cardano wallet key owning an EVM Safe: on-chain CIP-8/COSE signature verification and a set-once ERC-1271 owner registry

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages