Skip to content

Commit 64efc01

Browse files
committed
feat(proof): integrate claims co-proof
1 parent 95c96fb commit 64efc01

23 files changed

Lines changed: 1249 additions & 81 deletions

File tree

crates/authenticator/Cargo.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,17 @@ embed-zkeys = ["world-id-proof/embed-zkeys"]
2323
compress-zkeys = ["embed-zkeys", "world-id-proof/compress-zkeys"]
2424
# Compress the tar archive with zstd
2525
zstd-compress-zkeys = ["embed-zkeys", "world-id-proof/zstd-compress-zkeys"]
26-
# Embed the Noir ownership prover/verifier (built ad-hoc with nargo)
26+
# Embed Noir prover/verifier artifacts (built ad-hoc with nargo)
2727
embed-ownership-prover = ["world-id-proof/embed-ownership-prover"]
2828
embed-ownership-verifier = ["world-id-proof/embed-ownership-verifier"]
29-
embed-noir-artifacts = ["embed-ownership-prover", "embed-ownership-verifier"]
29+
embed-claims-prover = ["world-id-proof/embed-claims-prover"]
30+
embed-claims-verifier = ["world-id-proof/embed-claims-verifier"]
31+
embed-noir-artifacts = [
32+
"embed-ownership-prover",
33+
"embed-ownership-verifier",
34+
"embed-claims-prover",
35+
"embed-claims-verifier",
36+
]
3037
# Embed everything
3138
embed-zk-artifacts = ["embed-zkeys", "embed-noir-artifacts"]
3239

crates/authenticator/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@ World ID is an anonymous proof of human for the age of AI.
44

55
This crate provides the functionality for a World ID Authenticator.
66

7+
An RP can optionally attach sparse private-claim predicates to a credential request. On native
8+
targets, `generate_proof` then returns the regular Circom nullifier proof and a Noir claims
9+
co-proof in the same response item. The two proofs are bound through their shared nullifier,
10+
credential issuer, RP/action, and credential time policy.
11+
712
More information can be found in the [World ID Developer Documentation](https://docs.world.org/world-id).

crates/authenticator/src/prove.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ use world_id_primitives::OwnershipProof;
1818
use world_id_primitives::TREE_DEPTH;
1919
#[cfg(not(target_arch = "wasm32"))]
2020
use world_id_proof::{
21+
ClaimsProver,
2122
circuit_inputs::OwnershipProofCircuitInput,
23+
claims_proof::{ClaimsProofContext, ClaimsProofInput, generate_claims_proof_with_prover},
2224
ownership_proof::generate_ownership_proof_with_prover,
2325
};
2426

@@ -336,6 +338,24 @@ impl Authenticator {
336338
.nullifier_material()
337339
.map_err(AuthenticatorError::ZkArtifactError)?;
338340

341+
#[cfg(not(target_arch = "wasm32"))]
342+
let claims_prover = if items_to_prove.iter().any(|item| item.claims.is_some()) {
343+
Some(
344+
self.zk_artifact_source
345+
.claims_prover()
346+
.map_err(AuthenticatorError::ZkArtifactError)?,
347+
)
348+
} else {
349+
None
350+
};
351+
352+
#[cfg(target_arch = "wasm32")]
353+
if items_to_prove.iter().any(|item| item.claims.is_some()) {
354+
return Err(AuthenticatorError::Generic(
355+
"claims proofs are not available on wasm32".to_owned(),
356+
));
357+
}
358+
339359
// 3. Generate per-credential proofs for the selected items
340360
let creds_by_schema: std::collections::HashMap<u64, &CredentialInput> = credentials
341361
.iter()
@@ -356,6 +376,8 @@ impl Authenticator {
356376
resolved_session_id,
357377
proof_request.proof_type,
358378
proof_request.created_at,
379+
#[cfg(not(target_arch = "wasm32"))]
380+
claims_prover.as_ref(),
359381
)?;
360382
responses.push(response_item);
361383
}
@@ -415,11 +437,14 @@ impl Authenticator {
415437
session_id: Option<SessionId>,
416438
proof_type: ProofType,
417439
request_timestamp: u64,
440+
#[cfg(not(target_arch = "wasm32"))] claims_prover: Option<&ClaimsProver>,
418441
) -> Result<ResponseItem, AuthenticatorError> {
419442
let mut rng = rand::rngs::OsRng;
420443

421444
let merkle_root: FieldElement = oprf_nullifier.query_proof_input.merkle_root.into();
422445
let action_from_query: FieldElement = oprf_nullifier.query_proof_input.action.into();
446+
let rp_id: FieldElement = oprf_nullifier.query_proof_input.rp_id.into();
447+
let oprf_response = oprf_nullifier.verifiable_oprf_output.unblinded_response;
423448

424449
let expires_at_min = request_item.effective_expires_at_min(request_timestamp);
425450

@@ -439,7 +464,7 @@ impl Authenticator {
439464

440465
// Construct the appropriate response item based on proof type
441466
let nullifier_fe: FieldElement = nullifier.into();
442-
let response_item = if proof_type.is_session() {
467+
let mut response_item = if proof_type.is_session() {
443468
let session_nullifier = SessionNullifier::new(nullifier_fe, action_from_query)?;
444469
ResponseItem::new_session(
445470
request_item.identifier.clone(),
@@ -458,6 +483,35 @@ impl Authenticator {
458483
)
459484
};
460485

486+
#[cfg(not(target_arch = "wasm32"))]
487+
if let Some(claims_request) = &request_item.claims {
488+
let prover = claims_prover.ok_or_else(|| {
489+
AuthenticatorError::Generic(
490+
"claims prover was not loaded for a claims proof request".to_owned(),
491+
)
492+
})?;
493+
let context = ClaimsProofContext {
494+
issuer_schema_id: credential.issuer_schema_id.into(),
495+
credential_public_key: credential.issuer.clone(),
496+
current_timestamp: expires_at_min.into(),
497+
cred_genesis_issued_at_min: request_item.genesis_issued_at_min.unwrap_or(0).into(),
498+
nullifier: nullifier_fe,
499+
rp_id,
500+
action: action_from_query,
501+
};
502+
response_item.claims_proof = Some(generate_claims_proof_with_prover(
503+
ClaimsProofInput {
504+
credential,
505+
credential_sub_blinding_factor,
506+
leaf_index: self.leaf_index(),
507+
oprf_response,
508+
context,
509+
request: claims_request,
510+
},
511+
prover.clone(),
512+
)?);
513+
}
514+
461515
Ok(response_item)
462516
}
463517

crates/core/Cargo.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,17 @@ embed-zkeys = ["authenticator", "world-id-proof/embed-zkeys"]
2525
compress-zkeys = ["authenticator", "embed-zkeys", "world-id-proof/compress-zkeys"]
2626
# Compress the tar archive with zstd
2727
zstd-compress-zkeys = ["authenticator", "embed-zkeys", "world-id-proof/zstd-compress-zkeys"]
28-
# Embed the Noir ownership prover/verifier (built ad-hoc with nargo)
28+
# Embed Noir prover/verifier artifacts (built ad-hoc with nargo)
2929
embed-ownership-prover = ["authenticator", "world-id-proof/embed-ownership-prover"]
3030
embed-ownership-verifier = ["authenticator", "world-id-proof/embed-ownership-verifier"]
31-
embed-noir-artifacts = ["embed-ownership-prover", "embed-ownership-verifier"]
31+
embed-claims-prover = ["authenticator", "world-id-proof/embed-claims-prover"]
32+
embed-claims-verifier = ["authenticator", "world-id-proof/embed-claims-verifier"]
33+
embed-noir-artifacts = [
34+
"embed-ownership-prover",
35+
"embed-ownership-verifier",
36+
"embed-claims-prover",
37+
"embed-claims-verifier",
38+
]
3239
# Embed everything
3340
embed-zk-artifacts = ["embed-zkeys", "embed-noir-artifacts"]
3441

crates/core/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ pub use world_id_primitives::Signer;
3030
#[cfg(feature = "authenticator")]
3131
pub use world_id_proof::{artifacts, nullifier_proof};
3232

33+
#[cfg(all(feature = "authenticator", not(target_arch = "wasm32")))]
34+
pub use world_id_proof::claims_proof;
35+
3336
#[cfg(any(feature = "authenticator", feature = "rp"))]
3437
pub use world_id_primitives::request as requests;
3538

crates/core/tests/generate_proof.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ async fn e2e_authenticator_generate_proof() -> Result<()> {
314314
signal: Some(b"my_signal".to_vec()),
315315
genesis_issued_at_min: None,
316316
expires_at_min: None,
317+
claims: None,
317318
}],
318319
constraints: None,
319320
};

crates/primitives/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ pub use session::{SessionId, SessionNullifier, SessionRef};
6161

6262
/// Contains the quintessential zero-knowledge proof type.
6363
pub mod proof;
64-
pub use proof::{OwnershipProof, ZeroKnowledgeProof};
64+
pub use proof::{
65+
ClaimStatement, ClaimsProof, ClaimsProofRequest, OwnershipProof, ZeroKnowledgeProof,
66+
};
6567

6668
/// Contains types specifically related to relying parties.
6769
pub mod rp;

crates/primitives/src/proof.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,31 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
33

44
use crate::FieldElement;
55

6+
/// A strict range predicate over one credential claim.
7+
///
8+
/// At least one bound must be present. The claim remains private; only the predicate and
9+
/// whether it was satisfied are disclosed by the claims proof.
10+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11+
#[serde(deny_unknown_fields)]
12+
pub struct ClaimStatement {
13+
/// Zero-based index of the claim in the credential's fixed claim vector.
14+
pub claim_index: u8,
15+
/// When present, proves `min_exclusive < claim`.
16+
#[serde(default, skip_serializing_if = "Option::is_none")]
17+
pub min_exclusive: Option<FieldElement>,
18+
/// When present, proves `claim < max_exclusive`.
19+
#[serde(default, skip_serializing_if = "Option::is_none")]
20+
pub max_exclusive: Option<FieldElement>,
21+
}
22+
23+
/// Optional claims predicates requested alongside a nullifier proof.
24+
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
25+
#[serde(deny_unknown_fields)]
26+
pub struct ClaimsProofRequest {
27+
/// Predicates to prove. Each claim index may occur at most once.
28+
pub statements: Vec<ClaimStatement>,
29+
}
30+
631
/// Encoded World ID Proof.
732
///
833
/// Internally, the first 4 elements are a compressed Groth16 proof
@@ -108,6 +133,18 @@ pub struct OwnershipProof {
108133
pub merkle_root: FieldElement,
109134
}
110135

136+
/// A presentation-specific proof over private issuer-attested credential claims.
137+
///
138+
/// The verifier supplies the remaining public inputs from the RP request, nullifier response,
139+
/// and issuer registry. The statements are repeated here so the response is self-describing.
140+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141+
pub struct ClaimsProof {
142+
/// The WHIR R1CS proof from ProveKit.
143+
pub proof: provekit_common::WhirR1CSProof,
144+
/// Public predicates proven over the private claims.
145+
pub statements: Vec<ClaimStatement>,
146+
}
147+
111148
#[cfg(test)]
112149
mod tests {
113150
use ruint::uint;
@@ -197,6 +234,27 @@ mod tests {
197234
assert_eq!(proof, decoded);
198235
}
199236

237+
#[test]
238+
fn test_claims_proof_json_roundtrip() {
239+
let proof = ClaimsProof {
240+
proof: provekit_common::WhirR1CSProof {
241+
narg_string: vec![1, 2, 3],
242+
hints: vec![4, 5],
243+
#[cfg(debug_assertions)]
244+
pattern: vec![],
245+
},
246+
statements: vec![ClaimStatement {
247+
claim_index: 3,
248+
min_exclusive: Some(FieldElement::from(18u64)),
249+
max_exclusive: Some(FieldElement::from(65u64)),
250+
}],
251+
};
252+
253+
let json = serde_json::to_string(&proof).unwrap();
254+
let decoded: ClaimsProof = serde_json::from_str(&json).unwrap();
255+
assert_eq!(proof, decoded);
256+
}
257+
200258
/// A proof with a wrong (flipped) merkle root must not equal the original.
201259
///
202260
/// This test simulates the data-level check that a verifier would perform:

0 commit comments

Comments
 (0)