Credential circuit
The circuit proves that an anonymous approved issuer signed a private, version-one credential; the credential is valid for the required programme and country; the presenter controls its bound hardware, holder and revocation keys; and its revocation commitment is absent from a LeanIMT+ root returned by the proof.
Signed body
The credential is the private Credential type in src/types.nr. The issuer
signs its fixed, versioned commitment:
policy = Poseidon4(
programme_hash,
country_code,
not_before,
expires_at
)
keys = Poseidon3(
device_key_commitment,
holder_key_commitment,
revocation_commitment
)
Poseidon3(
credential_domain,
policy,
keys
)
The circuit reconstructs this commitment and verifies the issuer's Baby Jubjub
EdDSA signature. It hashes the same private issuer key into a private binary
Merkle path with ZK-Kit's binary-merkle-root package and returns the resulting
root publicly, so the verifier can check it against the trusted on-chain
approved-issuer registry without learning which issuer signed.
The hardware P-256 key, Baby Jubjub holder key and separate Baby Jubjub revocation key all sign the private session message:
Poseidon4(presentation_domain, challenge, audience, credential_commitment)
The P-256 signature consumes the canonical 32-byte big-endian encoding of that
field element as its message digest. It is encoded as compact r || s with
low-s normalization, as Noir's P-256 verifier requires. Its public key is
private and opens the signed device_key_commitment, so it does not become a
presentation identifier.
Public inputs are current time, challenge, audience, required programme and required country. The circuit returns the proven approved-issuer and revocation roots as public outputs. After proof verification, the application must check both roots against their trusted on-chain registries.
Constraints
- Verify issuer EdDSA over the reconstructed credential commitment.
- Prove the same private issuer key belongs to the approved-issuer root returned publicly, over a private depth witness capped at the circuit's maximum tree depth.
- Check
not_before <= current_time <= expires_at. - Check the private programme and country equal the public requirements.
- Open the device-key commitment and verify its private P-256 session signature.
- Open the holder-key commitment and verify its private EdDSA session signature.
- Open the revocation commitment and verify its private EdDSA session signature.
- Prove that commitment is absent from a private LeanIMT+ witness and return the resulting root publicly.
The issuer, holder and revocation signature checks rely on cofactored EdDSA verification (the key is multiplied by 8).
The verifier is src/eddsa.nr, a copy of noir-lang/eddsa with one added
constraint. Upstream converts the Poseidon challenge with noir-edwards
v0.2.5's ScalarField::<64>::from, which takes its WNAF slices from an
unconstrained hint and only range-checks them below the field modulus, never
against the input. A prover could replace the challenge with one and verify
S = 0, R8 = -8A for any key. The local copy asserts that the slices
reconstruct the hash. A noir-edwards bump must re-check that conversion
before this copy is dropped or the dependency rebuilt.
The canonical empty revocation root is zero and requires a zeroed depth-zero witness.
Toolchain
The native toolchain is pinned, and the two halves must match: a circuit
compiled by one Noir release and proved by a barretenberg built for another
fails inside bb's deserializer with Length is too large.
noirup -v 1.0.0-beta.22
bbup -v 5.0.0
Running
yarn fixtures
yarn test
yarn execute
yarn test:constraints
bb prove -b target/credential.json -w target/credential.gz -o target/proof --write_vk --verify
generate-fixtures.ts builds the deterministic circuit fixture directly. The
credential SDK reproduces the same commitments and builds this ABI through
buildZkPresentationInputs; the issuer-service end-to-end test proves and
verifies the resulting witness.
What the npm package is for
This package publishes the Noir sources so the circuit behind a published proof
can be read, audited and rebuilt from the same inputs. It is not a Noir
dependency: the package is a bin crate, and nargo rejects a dependency on one.
Published artifacts
nargo compile writes to target/, which is not published. The compiled
circuit and its verification key ship separately, in
@coordination-network/zk-artifacts. Regenerate them from
this circuit, and commit the result, with:
yarn sync:zk-artifacts
Runtime proofs
The credential package exposes ZkCredentialProofSystem, plus one-shot
generateZkPresentationProof and verifyZkPresentationProof helpers. Callers
provide the trusted compiled circuit artifact, which
@coordination-network/zk-artifacts publishes as
credential/circuit.json. The verifier checks the proof, independently binds
all five public parameters to its expected time, challenge, audience, programme
and country, and returns both proven roots to the application for its on-chain
registry checks.
The JavaScript runtime versions are pinned to the native toolchain:
@noir-lang/noir_js@1.0.0-beta.22 and @aztec/bb.js@5.0.0. A circuit upgrade
must update these versions and regenerate the artifacts together.
Nargo 1.0.0-beta.22 reports a Brillig coverage warning in noir-edwards
dbl_internal. The dependency constrains both returned coordinates, so this is
a compiler false positive in noir-edwards v0.2.5.