Skip to content

About

Contains circuits used by the contracts and clients.

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

libID-circuits

Noir zero-knowledge circuits for libID's login flows. The proving artifacts (ACIR + verification keys) and the Solidity verifiers derived from them, which libID-contracts compiles and deploys, are not committed — they ship exclusively as GitHub Release assets, rebuilt from these sources by the release workflow under the pinned toolchain. bb runs in this repo and nowhere else.

libID does not implement its own zero-knowledge language or proving system. These circuits use Noir with Aztec's Barretenberg proving backend. We are grateful to the Aztec team for developing and sharing this stack as a public good, especially for its excellent browser proving support, which fits libID's client-side proving use case almost perfectly.

The circuits

Circuit Package Proves
circuits/bearer-link bearer_link X's and GitHub's bearer link plus the account as keys. The identity response reveals only the anchors around the id and the handle; the circuit opens the two committed values as the platform sent them, checks that each is one whole JSON token (an id of digits, a handle of printable ASCII without " or \), folds the handle, and outputs idNode = SHA256(idTag || id) and handleNode = SHA256(handleTag || fold(handle)) under the tags the verifier passes. 16 public inputs: the two bearer commitments, the id and handle commitments and the two nodes, each as two 16-byte halves, then each tag as one Field with its length. Neither value appears in plaintext; see the note below the table. The bearer is at most 128 bytes, the handle 39, the id 20 digits.
circuits/oidc-google oidc_google Possession of a Google OIDC JWT: verifies the RSASSA-PKCS1-v1_5 signature over header.payload and exposes the Authorization Digest carried in nonce, SHA256(aud), the id node SHA256("libid.google.user-id" || sub), the handle node SHA256("libid.google.handle" || fold(email)) (the sub and the address are not disclosed; each is checked to be one JSON string and the address is folded in the circuit), exp, and the modulus that verified. The Platform Verifier alone decides whether that modulus is trusted.

A node is an unsalted tagged SHA-256, so anyone can hash a guess and compare. The id and handle are not disclosed, which is weaker than secret. In the bearer-link flows their byte lengths are public. GitHub ids are sequential, so a GitHub id node is effectively public.

CI enforces nargo fmt --check on every package.

lib/identity is the library the identity circuits share: commitment openings, the tagged node hash, the token checks and the fold, and identity_link, the relation bearer-link runs. A circuit checks that a value is exactly the token the platform sent; its format is the platform's.

The platform table is this repository's. lib/identity/platforms.json holds each platform's node tags, the circuits' buffers and whether its id is decimal; lib/identity/handles.json holds the shared vector table every language runs. scripts/identity_table.py generates the constants, src/table.nr, and one test per vector, src/table_tests.nr, from them:

scripts/identity_table.py          # with nargo on PATH; writes both files
scripts/identity_table.py --check  # what CI runs

Every release ships both JSON files beside each circuit's verifier, and libID-contracts generates its Solidity, Rust and TypeScript tables from the shipped copies, so an edit here reaches every language through the release it pins.

scripts/identity-link-witness.py writes a bearer-link prover file from a synthetic X or GitHub identity-link witness. The committed circuits/bearer-link/Prover.toml (X) and Prover-github.toml are written from fixtures/x-identity-link-witness.json and fixtures/github-identity-link-witness.json: the identity_link_witness members of libID-contracts' synthetic solidity/contracts/ceremony/test/fixtures/{x,github}-ceremony-session.json (libid-rs' cargo run -p libid-tlsn --example ceremony_fixtures), whose handles (Alice_1, OctoCat) exercise the fold, each beside a provenance object with "synthetic": true. CI regenerates both and fails on a difference. After copying a fixture, scripts/check-fixtures.py <libID-contracts checkout> checks both files and scripts/google-fixture-witness.py's DIGEST against those sessions.

The script refuses a fixture without that marker.

Toolchain

toolchain.env is the single source of truth:

NARGO_VERSION=1.0.0-rc.3
BB_VERSION=6.0.0-rc.2

Install exactly those:

curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash
noirup --version 1.0.0-rc.3
curl -L https://raw.githubusercontent.com/AztecProtocol/aztec-packages/master/barretenberg/bbup/install | bash
bbup --version 6.0.0-rc.2

The two pins are one pair: a Barretenberg release builds against a single Noir commit (noir/noir-repo in aztec-packages at the bb tag) and reads only the ACIR that compiler writes. bb 6.0.0-rc.2 builds against Noir 1.0.0-rc.3.

The pins matter because the whole chain is deterministic in the toolchain:

src/main.nr --nargo compile--> ACIR json --bb write_vk--> vk --bb write_solidity_verifier--> Verifier.sol

The vk derives from the ACIR alone; the Solidity verifier from the vk alone. Different toolchain versions produce different vks, and a different vk is a different on-chain verifier — so bumping either pin means the web prover bundle and the deployed verifier contracts must roll together. Both scripts refuse to run under any other version.

nargo compile fetches the tag-pinned git dependencies in each Nargo.toml (zkpassport/noir_rsa, noir-lang/bignum, sha256, noir_base64, keccak256) into ~/nargo on first run; after that the build works from the local cache.

Building the artifacts

scripts/build.sh writes, per circuit, into a local gitignored artifacts/<circuit>/ (never committed):

  • <package>.json — ACIR + ABI from nargo compile, with the absolute source paths nargo embeds in file_map normalized to relative form so the file is byte-identical regardless of the machine that built it (the bytecode, abi, debug_symbols and hash fields are machine-independent as produced);
  • vk — Barretenberg verification key (bb write_vk --oracle_hash keccak, keccak because the consumer is EVM);
  • vk_hash — its 32-byte hash;
  • <Contract>.sol — the EVM Solidity verifier (bb write_solidity_verifier -t evm --optimized on the vk, see "Regenerating a Solidity verifier"), the contract named after the circuit directory: bearer-link/BearerLinkHonkVerifier.sol, oidc-google/OidcGoogleHonkVerifier.sol;
  • platforms.json and handles.json — the platform table and the vectors the circuit was built from, copied from lib/identity/.
scripts/build.sh              # build into ./artifacts/ (requires the pinned toolchain)
scripts/build.sh --out <dir>  # build into <dir> (what the workflows use)

The output is byte-reproducible: with the pinned toolchain, any machine produces identical bytes (the path normalization above removes the only machine-specific content), so a release built in CI is byte-identical to a local build from the same sources.

Regenerating a Solidity verifier

scripts/build.sh writes every verifier. scripts/gen-verifier.sh is the step it runs per circuit, and regenerates one from a vk alone — no nargo — for example to byte-compare an unpacked release against a local bb:

scripts/gen-verifier.sh bearer-link                          # artifacts/bearer-link/BearerLinkHonkVerifier.sol
scripts/gen-verifier.sh oidc-google --artifacts ~/unpacked   # from a downloaded release's vk

The verifier is bb's optimized template with zero knowledge (-t evm --optimized): one contract, <Circuit>HonkVerifier is IVerifier, deployed without linking. The Proving Circuit is zero-knowledge by specification, so the target is never evm-no-zk.

The interchange format is raw bb write_solidity_verifier output plus exactly one rewrite: the contract is renamed off bb's fixed HonkVerifier to <Circuit>HonkVerifier (the verifiers compile in one project). The names the current circuits ship under are pinned in the script and checked on every run, so renaming a circuit directory fails the build instead of silently renaming the contract consumers compile. forge fmt is the consumer's: it formats the shipped file under its own foundry.toml before compiling it.

Compiling a verifier

Compile the verifiers without via_ir, for EVM version cancun or later:

  • solc's IR pipeline cannot lay out the stack of the verifier's assembly (solc 0.8.33: "Could not create stack layout after 1000 iterations"); the legacy pipeline compiles it;
  • the assembly uses MCOPY, a Cancun opcode.

The switch is solc's --via-ir (standard JSON settings.viaIR), Foundry's via_ir. A Foundry project that builds everything else via IR keeps it on and compiles only the verifiers on the legacy pipeline:

[profile.default]
via_ir = true
additional_compiler_profiles = [{ name = "verifiers", via_ir = false }]
compilation_restrictions = [{ paths = "contracts/circuits/*HonkVerifier.sol", via_ir = false }]

Both fit EIP-170's runtime size limit on the legacy pipeline with the optimizer on.

scripts/check-verifiers.sh compiles every verifier that way, then proves each circuit's committed witnesses, circuits/<circuit>/Prover*.toml: each proof must verify with bb verify and with the verifier deployed to anvil, and fail with one bit flipped. CI runs it after every build. It needs Foundry (forge, anvil, cast) besides the pinned nargo and bb:

scripts/build.sh && scripts/check-verifiers.sh

circuits/oidc-google/Prover.toml is written by scripts/google-fixture-witness.py: a Google-shaped ID token signed by a synthetic RSA-2048 key, its constants in the script, for the fixture sub 100000000000000000001 and email Fixture@Example.com, mixed case so the fold is exercised.

Releases

Publishing a GitHub Release tagged v<version> builds the artifacts from source with the pinned toolchain (scripts/build.sh) and attaches:

  • libid-circuits-<version>-<circuit>.tar.gz — one per circuit, containing <package>.json, vk, vk_hash, <Contract>.sol, platforms.json and handles.json;
  • manifest.json — {version, tag, commit, toolchain: {nargo, bb}, tarballs: {<tarball>: {sha256, files: {<name>: sha256}}}}.

Consuming a release

Pin a release tag. Download the circuit's tarball and manifest.json, check the tarball's sha256 against the manifest, unpack, check <Contract>.sol against its entry in files, run forge fmt over it under your own foundry.toml, and compile it without via_ir (see "Compiling a verifier"). No bb, no nargo: the verifier is derived here, once, by the toolchain the manifest names.

platforms.json and handles.json in the same tarball, checked against files the same way, are the table the circuit was built from: generate your tags, buffers and vectors from them instead of keeping a copy. oidc-google's tags are compiled in; bearer-link takes its tags as public inputs, so the verifying contract must pass the tags platforms.json names.

Verification keys under the pinned toolchain (nargo 1.0.0-rc.3, bb 6.0.0-rc.2):

Circuit vk_hash
bearer-link 0x0ae6b31e32fb572fb6c7de574ed92545dd6c4a81cc62a1d1be1a8c8e58e0e2a3
oidc-google 0x24f940247d3108dacfc80e7e34b4a5efd0513a7c402a74565c20f099ec2aadba

About

Contains circuits used by the contracts and clients.

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages