Prove you may hire a car — reveal nothing
- RDF credentialsprivate, on-device
- SPARQL eligibility querythe public relation
- ZK proof (UltraHonk)generated in your tab
- Desk sees “Eligible”+ the proof, nothing else
@prefix cred: <https://www.w3.org/2018/credentials#> .
@prefix sch: <https://schema.org/> .
@prefix ex: <https://example.org/vc/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
# ── Gov-ID credential (issued by HM Passport Office) ──────────────
ex:govId a cred:VerifiableCredential ;
cred:issuer <https://gov.uk/issuers/hmpo> ;
cred:holder ex:holder ;
sch:birthDate "1994-04-02"^^xsd:date ;
ex:age "30"^^xsd:integer . # ← the value proven hidden
# ── Driving-licence credential (issued by the DVLA) ───────────────
ex:licence a cred:VerifiableCredential ;
cred:issuer <https://gov.uk/issuers/dvla> ;
cred:holder ex:holder ; # ← same holder as gov-ID
ex:licenceNumber "MORGA753116SM9IJ" ; # ← stays private
ex:licenceValid true ;
ex:statusListIndex "48172"^^xsd:integer . # ← revocation slot (hidden)Two W3C Verifiable Credentials — a gov-ID and a DVLA driving licence. The car-hire desk never sees this Turtle; only commitments to it and the eligibility verdict.
# Car-hire desk eligibility — answered with ZERO disclosure of the data.
ASK {
?id ex:age ?age ;
cred:holder ?person .
?licence cred:holder ?person ; # same holder across both VCs
ex:licenceValid true .
FILTER( ?age >= 25 ) # ← the only age fact revealed: ≥ 25
}30 ≥ 25 — provable. The age value itself is the private witness and is never disclosed.
fresh proof in your tab (default)
Idle — choose an age and generate a proof.
A full car-hire presentation composes one sub-proof per relation: 2× scan + filter (age ≥ 25) + 2× hidden-issuer + revocation + join (same holder) + holder proof-of-possession. Below, each member shows its honest status. Exactly one of the 8 members — the age-gate filter_int — is proven and verified live in your browser; the other 7 are compiled, gate-counted and proven by the native crate over bb, but are not run in-browser here. A green tick means a real in-tab cryptographic check happened.
- 1. Scan · gov-ID graph
scan_k2_n64_r8· 34,821 gatesthe age + holder rows genuinely come from the committed gov-ID credentialcomposed (native) - 2. Scan · DVLA graph
scan_k2_n64_r8· 34,821 gatesthe holder + validity rows genuinely come from the committed licence credentialcomposed (native) - 3. Filter · age ≥ 25
filter_int_d2· 17,416 gatesthe hidden age satisfies ≥ 25 — the only age fact disclosed (proven live here)queued - 4. Hidden issuer · gov-ID
hidden_issuer_d4· 16,946 gatesthe gov-ID was signed by a trusted issuer, without revealing which keycomposed (native) - 5. Hidden issuer · DVLA
hidden_issuer_d4· 16,946 gatesthe licence was signed by a trusted issuer, without revealing which keycomposed (native) - 6. Revocation · not revoked
revoke_unset_d10· 899 gatesthe licence is not revoked, at a hidden status-list indexcomposed (native) - 7. Join · same holder
join_eq_na16_nb16· 7,025 gatesboth credentials share one holder, without disclosing whocomposed (native) - 8. Holder · proof of possession
holder_pok· 10,334 gatesthe presenter possesses the bound holder keycomposed (native)
Composed verdict
—
run the age-gate to reach a verdict
| The desk sees (public) | Stays private |
|---|---|
| Eligible: true — the holder may hire a car | The exact date of birth and age (only “≥ 25 is true” leaks) |
| The predicates asked (age ≥ 25; a valid licence; one shared holder) | The licence number and every other undisclosed credential field |
| Commitments C(G) to both credential graphs + a freshness nonce | The holder’s identity (the join value hides behind a commitment) |
| That both credentials were signed by issuers in the desk’s trusted set | Which specific issuer key signed (on the hidden-issuer path) |
A full car-hire presentation composes one sub-proof per relation. Each member binds its statement to a public commitment and proves one SPARQL-shaped primitive over committed credential graphs. Below: what each verifies, what the desk sees (public) versus what stays the holder’s secret (hidden), and — the honest part — whether it runs live in your tab today or is only compiled / composed.
Live vs designed. Exactly one statement is proven live in your browser: the age-gate FILTER (filter_int_d2). The other members are wired (compiled + gate-counted + exercised by native tests) or designed (gadget complete, with a documented open seam) — not run in-browser. The full six-member cross-credential composition has never been assembled and verified as one unit. The deployed demo does not verify any signature, scan, join, revocation or holder key.
filter_int_d2· 17,416 gatesA hidden value satisfies a predicate — here, that the renter’s age is ≥ 25.
Public: challenge, operand_enc, op, bound, expected (the verdict)
Hidden: the value’s exact decimal digits (the age)
Primitive: range / comparison over a hidden integer (in-circuit blake3 token binding)
Status: the age-gate is the ONLY statement proven live in your browser tab (filter_int_d2); d1/d3/d4 are wired
scan_k2_n64_r8· 34,821 gatesEvery disclosed row genuinely came from a committed credential graph — and no matching row was hidden (completeness).
Public: challenge, per-graph commitments, the BGP pattern, the disclosed rows, row_count, attribution
Hidden: the per-graph row counts and the term encodings
Primitive: set-membership + completeness (BGP triple-pattern scan over committed graphs)
Status: all 8 scan members compile with native prove / verify / tamper tests; not run in-browser
join_eq_na16_nb16· 7,025 gatesTwo credentials share one value at the join slots (one holder across both VCs) — without disclosing who.
Public: challenge, the two graph commitments, a hiding join_commitment, the join slots
Hidden: both graphs’ encodings, the two joined rows, the shared join value, and a blinder
Primitive: equality-join over committed graphs (single-prover; the joined entity never appears in a public input)
Status: all 4 join members compile + tested natively; the cross-credential join is not run in-browser
hidden_issuer_d4· 16,946 gatesA trusted issuer signed the credential — without revealing which authority’s key signed it.
Public: challenge, the signed message m, the trusted-issuer key-set root
Hidden: the issuer public key, the signature, and which key-set member it is
Primitive: in-circuit signature verification + set-membership (Schnorr over Baby-JubJub + hidden-key Merkle membership)
Status: gadget + binding complete; ADDITIVE only and inherits the open soundness audit — clear-key attestation is still mandatory
holder_pok· 10,334 gatesThe presenter possesses the holder key the credential was issued to (proof of possession).
Public: challenge, the issuer-attested holder_pk_digest
Hidden: the holder secret key and the holder public-key point
Primitive: proof-of-possession / proof-of-knowledge (Baby-JubJub key-pair PoK)
Status: compiled; the in-circuit hidden-key tier is DEFERRED at the manifest seam — the landed binding is the clear-key tier that discloses the holder point
revoke_unset_d10· 899 gatesThe licence is NOT revoked, at a hidden status-list index (non-revocation).
Public: challenge, the status-list root, the index_commitment
Hidden: the status-list index, the status bit (= 0), the blinding, and the Merkle siblings
Primitive: non-membership / hidden-index status (Merkle inclusion + bit-unset + index cross-binding)
Status: compiled; the clear-index path still leaks the index unless the committed-index path is used
“Proves” statements are as designed / as landed, not an audited guarantee. The composition verifier is not yet sound (sq-qhy4) and the estate is research-grade and not externally audited — see the honest-limits note below.
A full car-hire presentation composes one sub-proof per relation. This page proves the age-gate member live in your tab; the others are real sparq circuits (compiled + gate-counted today) that the native crate proves over bb and that compose into one ProofManifest. Wiring the remaining members into the browser is tracked as a follow-up.
| Circuit | Proves | Gates | In-tab |
|---|---|---|---|
| filter_int_d2 | age ≥ 25 over the hidden integer age — proven live on this page | 17,416 | live |
| scan_k2_n64_r8 | BGP scan over each committed credential graph | 34,821 | composed |
| join_eq_na16_nb16 | both credentials share one holder, without disclosing who | 7,025 | composed |
| hidden_issuer_d4 | signed by a trusted issuer, without revealing which key | 16,946 | composed |
| revoke_unset_d10 | the licence is not revoked, at a hidden status-list index | 899 | composed |
| holder_pok | the presenter possesses the bound holder key | 10,334 | composed |
Gate counts are the measured bb gates snapshot (non-canonical, for scale). All members sit roughly 15× under the bb.js single-thread browser ceiling.
The age-gate proof on this page is real: a genuine UltraHonk proof of the sparq filter_int circuit, generated and verified by @aztec/bb.js in your browser. The exact age is the private witness and never appears in the public inputs.
On proof size and speed. The proof is inherently KB-scale: UltraHonk is a transparent, no-trusted-setup proof system whose proofs are kilobytes of field elements — not the ~200 byte single proof of a Groth16 SNARK, and there is no compact mode (a sub-KB single proof would need recursive aggregation, which is out of scope here). This page uses the keccak-oracle flavour (verifierTarget: 'evm'), which is ~43% smaller than the default while staying fully zero-knowledge. As a hard guardrail, the demo never uses any *-no-zk flavour (disableZk: true): those strip the ZK masking and would make the private age recoverable in principle. On plain GitHub Pages a service-worker shim is the only lever that unlocks multithreaded proving (timings are non-canonical and host-dependent).
But the sparq ZK estate is research-grade and has NOT been externally audited. No external accredited cryptographer has reviewed any part of sparq’s bespoke cryptography. The verifier-soundness claim rests entirely on sparq’s own internal, single-model self-audits. An external-cryptographer audit is required before any ZK security, privacy or attestation claim may be relied upon in production (tracked as bead sq-qhy4, and as gap CR-G1 in compliance/cryptoreview/).
The published security posture (SECURITY.md) treats the v1 verifier as NOT sound for a relying party. An internal re-audit finds it “sound as landed” under a stated threat model — but that is remediation progress, not a production guarantee, and it is single-model (pending re-review). Known privacy deferrals remain: holder possession is not yet bound to the specific credential, and the status-list IRI/version are disclosed (linkability).
Bottom line: this page demonstrates the flow and the mechanics of a sparq ZK query-proof, with a real proof in your tab. It does not assert that the proof is cryptographically trustworthy for production use. Treat any “verified” result here as a research demonstration.
The age-gate proves age ≥ 25 over a hidden date of birth with a real UltraHonk zero-knowledge proof, generated and verified entirely in your browser — the exact age, the licence number, and the holder’s identity are never revealed.
Details & full security caveat
A renter holds two W3C Verifiable Credentials — a gov-ID and a DVLA driving licence. The car-hire desk must learn only that the renter is ≥ 25, holds a valid, non-revoked licence, and that both credentials belong to the same person — and nothing else: not the date of birth, not the licence number, not the identity.
The proof runs in-tab via the third-party bb.js UltraHonk prover over a Noir circuit; the ~MB prover chunk loads only when you run the demo. The v1 verifier is research-grade: sound as landed under its stated threat model, but not externally audited and pending re-review — treat the timings and gate counts as indicative engineering numbers, not an audited cryptographic guarantee. Reproduction commands, the full circuit/public-input contract, and the soundness/linkability discussion live in the crate README and SKILL.md.