1use ark_bn254::Fr;
6use ark_ec::{AffineRepr, CurveGroup};
7use ark_ff::{PrimeField, Zero};
8use eddsa_babyjubjub::EdDSAPublicKey;
9use taceo_oprf::core::{dlog_equality::DLogEqualityProof, oprf::BlindingFactor};
10use world_id_primitives::{
11 AuthenticatorPublicKeySet, FieldElement, MAX_AUTHENTICATOR_KEYS, merkle::MerkleInclusionProof,
12};
13
14use crate::circuit_inputs::{NullifierProofCircuitInput, QueryProofCircuitInput};
15
16type BaseField = ark_babyjubjub::Fq;
17type Affine = ark_babyjubjub::EdwardsAffine;
18
19#[derive(Debug, thiserror::Error)]
20pub enum ProofInputError {
22 #[error("The specified Merkle tree depth is invalid (expected: {expected}, got: {is}).")]
24 InvalidMerkleTreeDepth {
25 expected: usize,
27 is: BaseField,
29 },
30 #[error("The set of authenticator public keys is invalid.")]
32 InvalidAuthenticatorPublicKeySet,
33 #[error("The provided Merkle tree inclusion proof is invalid.")]
35 InvalidMerkleTreeInclusionProof,
36 #[error("The signature over the nonce and RP ID is invalid.")]
38 InvalidQuerySignature,
39 #[error("The provided blinding factor is invalid.")]
41 InvalidBlindingFactor,
42 #[error(
44 "The provided credential has expired (expires_at: {expires_at}, check_timestamp: {current_timestamp})."
45 )]
46 CredentialExpired {
47 current_timestamp: u64,
49 expires_at: u64,
51 },
52 #[error(
54 "The provided credential has a genesis issued at date that is too old (genesis_issued_at: {genesis_issued_at}, check_timestamp: {genesis_issued_at_min})."
55 )]
56 CredentialGenesisExpired {
57 genesis_issued_at_min: u64,
59 genesis_issued_at: u64,
61 },
62 #[error("The value '{name}' is out of bounds (got: {is}, limit: {limit}).")]
64 ValueOutOfBounds {
65 name: &'static str,
67 is: BaseField,
69 limit: BaseField,
71 },
72 #[error("The credential signature is invalid for the given issuer public key.")]
74 InvalidCredentialSignature,
75 #[error(
77 "The provided point '{name}' is not a valid point in the prime-order subgroup of the BabyJubJub curve."
78 )]
79 InvalidBabyJubJubPoint {
80 name: &'static str,
82 },
83 #[error("The provided OPRF DlogEquality proof is invalid.")]
85 InvalidOprfProof,
86 #[error("The provided unblinded OPRF response point is invalid.")]
88 InvalidOprfResponse,
89 #[error(
91 "The provided session ID commitment is invalid for the given id and session id randomness."
92 )]
93 InvalidSessionId,
94 #[error(
96 "The provided proof request has expired (expires_at: {expires_at}, check_timestamp: {current_timestamp})."
97 )]
98 ProofRequestExpired {
99 current_timestamp: u64,
101 expires_at: u64,
103 },
104 #[error("The proof's expires_at {expires_at} happens before the created_at {created_at}.")]
106 InvalidExpiresAt { created_at: u64, expires_at: u64 },
107}
108
109pub fn check_query_input_validity<const TREE_DEPTH: usize>(
117 inputs: &QueryProofCircuitInput<TREE_DEPTH>,
118) -> Result<Affine, ProofInputError> {
119 if inputs.depth != BaseField::new((TREE_DEPTH as u64).into()) {
121 return Err(ProofInputError::InvalidMerkleTreeDepth {
122 expected: TREE_DEPTH,
123 is: inputs.depth,
124 });
125 }
126 let idx_u64 = u64::try_from(FieldElement::from(inputs.mt_index)).map_err(|_| {
129 ProofInputError::ValueOutOfBounds {
130 name: "Merkle tree index",
131 is: inputs.mt_index,
132 limit: BaseField::new((1u64 << TREE_DEPTH).into()),
133 }
134 })?;
135 if idx_u64 >= (1u64 << TREE_DEPTH) {
136 return Err(ProofInputError::ValueOutOfBounds {
137 name: "Merkle tree index",
138 is: inputs.mt_index,
139 limit: BaseField::new((1u64 << TREE_DEPTH).into()),
140 });
141 }
142
143 let pk_set = AuthenticatorPublicKeySet::new(
145 inputs
146 .pk
147 .iter()
148 .map(|&x| EdDSAPublicKey { pk: x })
149 .collect(),
150 )
151 .map_err(|_| ProofInputError::InvalidAuthenticatorPublicKeySet)?;
152 let pk_set_hash = pk_set.leaf_hash();
153 let merkle_tree_inclusion_proof = MerkleInclusionProof::new(
154 FieldElement::from(inputs.merkle_root),
155 idx_u64,
156 inputs.siblings.map(FieldElement::from),
157 );
158 if !merkle_tree_inclusion_proof.is_valid(FieldElement::from(pk_set_hash)) {
159 return Err(ProofInputError::InvalidMerkleTreeInclusionProof);
160 }
161
162 let pk_index_usize = usize::try_from(FieldElement::from(inputs.pk_index)).map_err(|_| {
164 ProofInputError::ValueOutOfBounds {
165 name: "Authenticator PubKey index",
166 is: inputs.pk_index,
167 limit: BaseField::new((MAX_AUTHENTICATOR_KEYS as u64).into()),
168 }
169 })?;
170 let pk = pk_set
171 .get(pk_index_usize)
172 .ok_or_else(|| ProofInputError::ValueOutOfBounds {
173 name: "Authenticator PubKey index",
174 is: inputs.pk_index,
175 limit: BaseField::new((MAX_AUTHENTICATOR_KEYS as u64).into()),
176 })?;
177
178 if !inputs.r.is_on_curve() || !inputs.r.is_in_correct_subgroup_assuming_on_curve() {
179 return Err(ProofInputError::InvalidBabyJubJubPoint {
180 name: "Query Signature R",
181 });
182 }
183 if !pk.pk.is_on_curve() || !pk.pk.is_in_correct_subgroup_assuming_on_curve() {
184 return Err(ProofInputError::InvalidBabyJubJubPoint {
185 name: "Authenticator Public Key",
186 });
187 }
188
189 let _rp_id_u64 = u64::try_from(FieldElement::from(inputs.rp_id)).map_err(|_| {
190 ProofInputError::ValueOutOfBounds {
191 name: "RP Id",
192 is: inputs.rp_id,
193 limit: BaseField::new(u64::MAX.into()),
194 }
195 })?;
196 let query = world_id_primitives::authenticator::oprf_query_digest(
197 idx_u64,
198 FieldElement::from(inputs.action),
199 FieldElement::from(inputs.rp_id),
200 );
201 let signature = eddsa_babyjubjub::EdDSASignature {
202 r: inputs.r,
203 s: inputs.s,
204 };
205
206 if !pk.verify(*query, &signature) {
207 return Err(ProofInputError::InvalidQuerySignature);
208 }
209
210 let blinding_factor = BlindingFactor::from_scalar(inputs.beta)
211 .map_err(|_| ProofInputError::InvalidBlindingFactor)?;
212 let query_point = taceo_oprf::core::oprf::client::blind_query(*query, blinding_factor);
213
214 Ok(query_point.blinded_query())
215}
216
217#[expect(
225 clippy::too_many_lines,
226 reason = "necessary checks for input validity should be in one function"
227)]
228pub fn check_nullifier_input_validity<const TREE_DEPTH: usize>(
229 inputs: &NullifierProofCircuitInput<TREE_DEPTH>,
230) -> Result<FieldElement, ProofInputError> {
231 let blinded_query = check_query_input_validity(&inputs.query_input)?;
233
234 let current_timestamp_u64 = u64::try_from(FieldElement::from(inputs.current_timestamp))
237 .map_err(|_| ProofInputError::ValueOutOfBounds {
238 name: "current timestamp",
239 is: inputs.current_timestamp,
240 limit: BaseField::new(u64::MAX.into()),
241 })?;
242 let credential_expires_at_u64 = u64::try_from(FieldElement::from(inputs.cred_expires_at))
243 .map_err(|_| ProofInputError::ValueOutOfBounds {
244 name: "credential expiry timestamp",
245 is: inputs.cred_expires_at,
246 limit: BaseField::new(u64::MAX.into()),
247 })?;
248 if credential_expires_at_u64 <= current_timestamp_u64 {
250 return Err(ProofInputError::CredentialExpired {
251 current_timestamp: current_timestamp_u64,
252 expires_at: credential_expires_at_u64,
253 });
254 }
255 let genesis_issued_at_u64 = u64::try_from(FieldElement::from(inputs.cred_genesis_issued_at))
257 .map_err(|_| ProofInputError::ValueOutOfBounds {
258 name: "credential genesis issued at",
259 is: inputs.cred_genesis_issued_at,
260 limit: BaseField::new(u64::MAX.into()),
261 })?;
262 let genesis_issued_at_min_u64 =
263 u64::try_from(FieldElement::from(inputs.cred_genesis_issued_at_min)).map_err(|_| {
264 ProofInputError::ValueOutOfBounds {
265 name: "credential genesis issued at minimum bound",
266 is: inputs.cred_genesis_issued_at_min,
267 limit: BaseField::new(u64::MAX.into()),
268 }
269 })?;
270 if genesis_issued_at_min_u64 > genesis_issued_at_u64 {
271 return Err(ProofInputError::CredentialGenesisExpired {
272 genesis_issued_at_min: genesis_issued_at_min_u64,
273 genesis_issued_at: genesis_issued_at_u64,
274 });
275 }
276
277 let blinded_subject = sub(
278 FieldElement::from(inputs.query_input.mt_index),
279 FieldElement::from(inputs.cred_sub_blinding_factor),
280 );
281
282 let cred_hash = hash_credential(
283 FieldElement::from(inputs.issuer_schema_id),
284 blinded_subject,
285 FieldElement::from(inputs.cred_genesis_issued_at),
286 FieldElement::from(inputs.cred_expires_at),
287 FieldElement::from(inputs.cred_hashes[0]),
288 FieldElement::from(inputs.cred_hashes[1]),
289 FieldElement::from(inputs.cred_id),
290 );
291 let pk = EdDSAPublicKey { pk: inputs.cred_pk };
292
293 let signature = eddsa_babyjubjub::EdDSASignature {
294 r: inputs.cred_r,
295 s: inputs.cred_s,
296 };
297
298 if !inputs.cred_r.is_on_curve() || !inputs.cred_r.is_in_correct_subgroup_assuming_on_curve() {
299 return Err(ProofInputError::InvalidBabyJubJubPoint {
300 name: "Credential Signature R",
301 });
302 }
303 if !pk.pk.is_on_curve() || !pk.pk.is_in_correct_subgroup_assuming_on_curve() {
304 return Err(ProofInputError::InvalidBabyJubJubPoint {
305 name: "Credential Public Key",
306 });
307 }
308
309 if !pk.verify(*cred_hash, &signature) {
310 return Err(ProofInputError::InvalidCredentialSignature);
311 }
312
313 if !inputs.oprf_pk.is_on_curve() || !inputs.oprf_pk.is_in_correct_subgroup_assuming_on_curve() {
315 return Err(ProofInputError::InvalidBabyJubJubPoint {
316 name: "OPRF Public Key",
317 });
318 }
319 if !inputs.oprf_response_blinded.is_on_curve()
320 || !inputs
321 .oprf_response_blinded
322 .is_in_correct_subgroup_assuming_on_curve()
323 {
324 return Err(ProofInputError::InvalidBabyJubJubPoint {
325 name: "OPRF Blinded Response",
326 });
327 }
328
329 let dlog_proof = DLogEqualityProof::new(inputs.dlog_e, inputs.dlog_s);
331 dlog_proof
332 .verify(
333 inputs.oprf_pk,
334 blinded_query,
335 inputs.oprf_response_blinded,
336 Affine::generator(),
337 )
338 .map_err(|_| ProofInputError::InvalidOprfProof)?;
339
340 if !inputs.oprf_response.is_on_curve()
342 || !inputs
343 .oprf_response
344 .is_in_correct_subgroup_assuming_on_curve()
345 {
346 return Err(ProofInputError::InvalidBabyJubJubPoint {
347 name: "OPRF Unblinded Response",
348 });
349 }
350 let expected_blinded_response = (inputs.oprf_response * inputs.query_input.beta).into_affine();
351 if expected_blinded_response != inputs.oprf_response_blinded {
352 return Err(ProofInputError::InvalidOprfResponse);
353 }
354
355 if !inputs.id_commitment.is_zero() {
357 let expected_commitment = session_id_commitment(
358 FieldElement::from(inputs.query_input.mt_index),
359 FieldElement::from(inputs.id_commitment_r),
360 );
361 if expected_commitment != FieldElement::from(inputs.id_commitment) {
362 return Err(ProofInputError::InvalidSessionId);
363 }
364 }
365
366 let nullifier = oprf_finalize_hash(
368 *world_id_primitives::authenticator::oprf_query_digest(
369 #[expect(
370 clippy::missing_panics_doc,
371 reason = "checked in check_query_input_validity"
372 )]
373 u64::try_from(FieldElement::from(inputs.query_input.mt_index)).unwrap(),
374 FieldElement::from(inputs.query_input.action),
375 FieldElement::from(inputs.query_input.rp_id),
376 ),
377 inputs.oprf_response,
378 );
379
380 Ok(nullifier)
381}
382
383fn sub(leaf_index: FieldElement, blinding_factor: FieldElement) -> FieldElement {
387 let sub_ds = Fr::from_be_bytes_mod_order(b"H_CS(id, r)");
388 let mut input = [sub_ds, *leaf_index, *blinding_factor];
389 poseidon2::bn254::t3::permutation_in_place(&mut input);
390 input[1].into()
391}
392fn oprf_finalize_hash(query: BaseField, oprf_response: Affine) -> FieldElement {
394 let finalize_ds = Fr::from_be_bytes_mod_order(super::OPRF_PROOF_DS);
395 let mut input = [finalize_ds, query, oprf_response.x, oprf_response.y];
396 poseidon2::bn254::t4::permutation_in_place(&mut input);
397 input[1].into()
398}
399
400fn session_id_commitment(user_id: FieldElement, commitment_rand: FieldElement) -> FieldElement {
402 let sub_ds = Fr::from_be_bytes_mod_order(b"H(id, r)");
403 let mut input = [sub_ds, *user_id, *commitment_rand];
404 poseidon2::bn254::t3::permutation_in_place(&mut input);
405 input[1].into()
406}
407
408fn hash_credential(
410 issuer_schema_id: FieldElement,
411 sub: FieldElement,
412 genesis_issued_at: FieldElement,
413 expires_at: FieldElement,
414 claims_hash: FieldElement,
415 associated_data_commitment: FieldElement,
416 id: FieldElement,
417) -> FieldElement {
418 let cred_ds = Fr::from_be_bytes_mod_order(b"POSEIDON2+EDDSA-BJJ");
419 let mut input = [
420 cred_ds,
421 *issuer_schema_id,
422 *sub,
423 *genesis_issued_at,
424 *expires_at,
425 *claims_hash,
426 *associated_data_commitment,
427 *id,
428 ];
429 poseidon2::bn254::t8::permutation_in_place(&mut input);
430 input[1].into()
431}
432
433#[cfg(test)]
434mod tests {
435 use crate::circuit_inputs::{NullifierProofCircuitInput, QueryProofCircuitInput};
436 use ark_ec::twisted_edwards::Affine;
437 use std::str::FromStr;
438
439 use crate::proof::errors::{check_nullifier_input_validity, check_query_input_validity};
440
441 fn get_valid_query_proof_input() -> QueryProofCircuitInput<30> {
443 QueryProofCircuitInput {
444 pk: [Affine {
445 x: ark_babyjubjub::Fq::from_str(
446 "19037598474602150174935475944965340829216795940473064039209388058233204431288",
447 ).unwrap(),
448 y: ark_babyjubjub::Fq::from_str(
449 "3549932221586364715003722955756497910920276078443163728621283280434115857197",
450 ).unwrap(),
451 },
452 Affine::zero(),
453 Affine::zero(),
454 Affine::zero(),
455 Affine::zero(),
456 Affine::zero(),
457 Affine::zero(),
458 ],
459 pk_index: ark_bn254::Fr::from(0u64),
460 s: ark_babyjubjub::Fr::from_str(
461 "2692248185200295468055279425612708965310378163906753799023551825366269352327",
462 ).unwrap(),
463 r: Affine {
464 x: ark_babyjubjub::Fq::from_str(
465 "14689596469778385278298478829656243946283084496217945909620117398922933730711",
466 ).unwrap(),
467 y: ark_babyjubjub::Fq::from_str(
468 "4424830738973486800075394160997493242162871494907432163152597205147606706197",
469 ).unwrap(),
470 },
471 merkle_root: ark_bn254::Fr::from_str("4959814736111706042728533661656003495359474679272202023690954858781105690707").unwrap(),
472 depth: ark_babyjubjub::Fq::from(30u64),
473 mt_index: ark_bn254::Fr::from(1u64),
474 siblings: [
475 ark_bn254::Fr::from_str("0").unwrap(),
476 ark_bn254::Fr::from_str("15621590199821056450610068202457788725601603091791048810523422053872049975191").unwrap(),
477 ark_bn254::Fr::from_str("15180302612178352054084191513289999058431498575847349863917170755410077436260").unwrap(),
478 ark_bn254::Fr::from_str("20846426933296943402289409165716903143674406371782261099735847433924593192150").unwrap(),
479 ark_bn254::Fr::from_str("19570709311100149041770094415303300085749902031216638721752284824736726831172").unwrap(),
480 ark_bn254::Fr::from_str("11737142173000203701607979434185548337265641794352013537668027209469132654026").unwrap(),
481 ark_bn254::Fr::from_str("11865865012735342650993929214218361747705569437250152833912362711743119784159").unwrap(),
482 ark_bn254::Fr::from_str("1493463551715988755902230605042557878234810673525086316376178495918903796315").unwrap(),
483 ark_bn254::Fr::from_str("18746103596419850001763894956142528089435746267438407061601783590659355049966").unwrap(),
484 ark_bn254::Fr::from_str("21234194473503024590374857258930930634542887619436018385581872843343250130100").unwrap(),
485 ark_bn254::Fr::from_str("14681119568252857310414189897145410009875739166689283501408363922419813627484").unwrap(),
486 ark_bn254::Fr::from_str("13243470632183094581890559006623686685113540193867211988709619438324105679244").unwrap(),
487 ark_bn254::Fr::from_str("19463898140191333844443019106944343282402694318119383727674782613189581590092").unwrap(),
488 ark_bn254::Fr::from_str("10565902370220049529800497209344287504121041033501189980624875736992201671117").unwrap(),
489 ark_bn254::Fr::from_str("5560307625408070902174028041423028597194394554482880015024167821933869023078").unwrap(),
490 ark_bn254::Fr::from_str("20576730574720116265513866548855226316241518026808984067485384181494744706390").unwrap(),
491 ark_bn254::Fr::from_str("11166760821615661136366651998133963805984915741187325490784169611245269155689").unwrap(),
492 ark_bn254::Fr::from_str("13692603500396323648417392244466291089928913430742736835590182936663435788822").unwrap(),
493 ark_bn254::Fr::from_str("11129674755567463025028188404867541558752927519269975708924528737249823830641").unwrap(),
494 ark_bn254::Fr::from_str("6673535049007525806710184801639542254440636510496168661971704157154828514023").unwrap(),
495 ark_bn254::Fr::from_str("7958154589163466663626421142270206662020519181323839780192984613274682930816").unwrap(),
496 ark_bn254::Fr::from_str("3739156991379607404516753076057250171966250101655747790592556040569841550790").unwrap(),
497 ark_bn254::Fr::from_str("1334107297020502384420211493664486465203492095766400031330900935069700302301").unwrap(),
498 ark_bn254::Fr::from_str("20357028769054354174264046872903423695314313082869184437966002491602414517674").unwrap(),
499 ark_bn254::Fr::from_str("19392290367394672558538719012722289280213395590510602524366987685302929990731").unwrap(),
500 ark_bn254::Fr::from_str("7360502715619830055199267117332475946442427205382059394111067387016428818088").unwrap(),
501 ark_bn254::Fr::from_str("9629177338475347225553791169746168712988898028547587350296027054067573957412").unwrap(),
502 ark_bn254::Fr::from_str("21877160135037839571797468541807904053886800340144060811298025652177410263004").unwrap(),
503 ark_bn254::Fr::from_str("7105691694342706282901391345307729036900705570482804586768449537652208350743").unwrap(),
504 ark_bn254::Fr::from_str("15888057581779748293164452094398990053773731478520540058125130669204703869637").unwrap(),
505 ],
506 beta: ark_babyjubjub::Fr::from_str("1277277022932719396321614946989807194659268059729440522321681213750340643042").unwrap(),
507 rp_id: ark_bn254::Fr::from_str("14631649082411674499").unwrap(),
508 action: ark_bn254::Fr::from_str("8982441576518976929447725179565370305223105654688049122733783421407497941726").unwrap(),
509 nonce: ark_bn254::Fr::from_str("8530676162050357218814694371816107906694725175836943927290214963954696613748").unwrap(),
510 }
511 }
512
513 #[test]
514 fn test_valid_query_proof_input() {
515 let inputs = get_valid_query_proof_input();
516 let _ = check_query_input_validity(&inputs).unwrap();
517 }
518
519 #[test]
520 fn test_invalid_query_proof_input() {
521 let inputs = get_valid_query_proof_input();
522 {
523 let mut inputs = inputs.clone();
524 inputs.depth = ark_babyjubjub::Fq::from(29u64); assert!(matches!(
526 check_query_input_validity(&inputs).unwrap_err(),
527 super::ProofInputError::InvalidMerkleTreeDepth { .. }
528 ));
529 }
530 {
531 let mut inputs = inputs.clone();
532 inputs.mt_index = ark_bn254::Fr::from(1073741824u64);
534 assert!(matches!(
535 check_query_input_validity(&inputs).unwrap_err(),
536 super::ProofInputError::ValueOutOfBounds {
537 name: "Merkle tree index",
538 ..
539 }
540 ));
541 }
542 {
543 let mut inputs = inputs.clone();
544 inputs.merkle_root = ark_bn254::Fr::from(12345u64);
545 assert!(matches!(
546 check_query_input_validity(&inputs).unwrap_err(),
547 super::ProofInputError::InvalidMerkleTreeInclusionProof
548 ));
549 }
550 {
551 let mut inputs = inputs.clone();
552 inputs.pk_index = ark_bn254::Fr::from(7u64); assert!(matches!(
554 check_query_input_validity(&inputs).unwrap_err(),
555 super::ProofInputError::ValueOutOfBounds {
556 name: "Authenticator PubKey index",
557 ..
558 }
559 ));
560 }
561 {
562 let mut inputs = inputs.clone();
563 inputs.r = Affine {
564 x: ark_babyjubjub::Fq::from(1u64),
565 y: ark_babyjubjub::Fq::from(2u64),
566 };
567 assert!(matches!(
568 check_query_input_validity(&inputs).unwrap_err(),
569 super::ProofInputError::InvalidBabyJubJubPoint {
570 name: "Query Signature R"
571 }
572 ));
573 }
574 {
575 let mut inputs = inputs.clone();
576 inputs.pk[0] = Affine {
577 x: ark_babyjubjub::Fq::from(1u64),
578 y: ark_babyjubjub::Fq::from(2u64),
579 };
580
581 let pk_set = world_id_primitives::AuthenticatorPublicKeySet::new(
583 inputs
584 .pk
585 .iter()
586 .map(|&x| eddsa_babyjubjub::EdDSAPublicKey { pk: x })
587 .collect(),
588 )
589 .unwrap();
590 let mut current = pk_set.leaf_hash();
591 let idx =
592 u64::try_from(world_id_primitives::FieldElement::from(inputs.mt_index)).unwrap();
593 for (i, sibling) in inputs.siblings.iter().enumerate() {
594 let sibling_fr = *world_id_primitives::FieldElement::from(*sibling);
595 if (idx >> i) & 1 == 0 {
596 let mut state = poseidon2::bn254::t2::permutation(&[current, sibling_fr]);
597 state[0] += current;
598 current = state[0];
599 } else {
600 let mut state = poseidon2::bn254::t2::permutation(&[sibling_fr, current]);
601 state[0] += sibling_fr;
602 current = state[0];
603 }
604 }
605 inputs.merkle_root = current;
606
607 assert!(matches!(
608 check_query_input_validity(&inputs).unwrap_err(),
609 super::ProofInputError::InvalidBabyJubJubPoint {
610 name: "Authenticator Public Key"
611 }
612 ));
613 }
614 {
615 let mut inputs = inputs.clone();
616 inputs.action = ark_bn254::Fr::from(12345u64);
617 assert!(matches!(
618 check_query_input_validity(&inputs).unwrap_err(),
619 super::ProofInputError::InvalidQuerySignature
620 ));
621 }
622 }
623
624 fn get_valid_nullifier_proof_input() -> NullifierProofCircuitInput<30> {
625 NullifierProofCircuitInput {
626 query_input: get_valid_query_proof_input(),
627 issuer_schema_id: ark_bn254::Fr::from(1u64),
628 cred_pk: Affine {
629 x: ark_babyjubjub::Fq::from_str(
630 "15406775215557320288232407896017344573719706795510112309920214099347968981892",
631 )
632 .unwrap(),
633 y: ark_babyjubjub::Fq::from_str(
634 "486388649729314270871358770861421181497883381447163109744630700259216042819",
635 )
636 .unwrap(),
637 },
638 cred_hashes: [
639 ark_bn254::Fr::from_str(
640 "14272087287699568472569351444185311392108883722570788958733484799744115401870",
641 )
642 .unwrap(),
643 ark_bn254::Fr::from_str("0").unwrap(),
644 ],
645 cred_genesis_issued_at: ark_bn254::Fr::from(1770125923u64),
646 cred_expires_at: ark_bn254::Fr::from(1770125983u64),
647 cred_s: ark_babyjubjub::Fr::from_str(
648 "1213918488111680600555111454085490191981091366153388773926786471247948539005",
649 )
650 .unwrap(),
651 cred_r: Affine {
652 x: ark_babyjubjub::Fq::from_str(
653 "15844586803954862856390946258558419582000810449135704981677693963391564067969",
654 )
655 .unwrap(),
656 y: ark_babyjubjub::Fq::from_str(
657 "592710378120172403096018676235519447487818389124797234601458948988041235710",
658 )
659 .unwrap(),
660 },
661 current_timestamp: ark_bn254::Fr::from(1770125908u64),
662 cred_genesis_issued_at_min: ark_bn254::Fr::from(0u64),
663 cred_sub_blinding_factor: ark_bn254::Fr::from_str(
664 "12170146734368267085913078854954627576787934009906407554611507307540342380837",
665 )
666 .unwrap(),
667 cred_id: ark_bn254::Fr::from(3198767490419873482u64),
668 id_commitment_r: ark_bn254::Fr::from_str(
669 "11722352184830287916674945948108962396487445899741105828127518108056503126019",
670 )
671 .unwrap(),
672 id_commitment: ark_bn254::Fr::from(0u64),
673 dlog_e: ark_bn254::Fr::from_str(
674 "20738873297635092620048980552264360096607713029337408079647701591795211132447",
675 )
676 .unwrap(),
677 dlog_s: ark_babyjubjub::Fr::from_str(
678 "409914485496464180245985942628922659137136006706846380135829705769429965654",
679 )
680 .unwrap(),
681 oprf_pk: Affine {
682 x: ark_babyjubjub::Fq::from_str(
683 "2124016492737602714904869498047199181102594928943726277329982080254326092458",
684 )
685 .unwrap(),
686 y: ark_babyjubjub::Fq::from_str(
687 "13296886400185574560491768605341786437896334271868835545571935419923854148448",
688 )
689 .unwrap(),
690 },
691 oprf_response_blinded: Affine {
692 x: ark_babyjubjub::Fq::from_str(
693 "186021305824089989598292966483056363224488147240980559441958002546059602483",
694 )
695 .unwrap(),
696 y: ark_babyjubjub::Fq::from_str(
697 "16813058203546508924422863380215026034284821141284206571184467783067057954778",
698 )
699 .unwrap(),
700 },
701 oprf_response: Affine {
702 x: ark_babyjubjub::Fq::from_str(
703 "10209445202057032226639052993170591937356545068582397532992536070677055126187",
704 )
705 .unwrap(),
706 y: ark_babyjubjub::Fq::from_str(
707 "21877375411477040679486668720099554257785799784699842830375906922948306109699",
708 )
709 .unwrap(),
710 },
711 signal_hash: ark_bn254::Fr::from_str(
712 "37938388892362834151584770384290207919364301626797345218722464515205243407",
713 )
714 .unwrap(),
715 }
716 }
717
718 #[test]
719 fn test_valid_nullifier_proof_input() {
720 let inputs = get_valid_nullifier_proof_input();
721 let _ = check_nullifier_input_validity(&inputs).unwrap();
722 }
723
724 #[test]
725 fn test_invalid_nullifier_proof_input() {
726 let inputs = get_valid_nullifier_proof_input();
727 {
728 let mut inputs = inputs.clone();
729 inputs.current_timestamp =
730 ark_babyjubjub::Fq::from_str("123465723894591324701234982134000070").unwrap(); assert!(matches!(
732 check_nullifier_input_validity(&inputs).unwrap_err(),
733 super::ProofInputError::ValueOutOfBounds {
734 name: "current timestamp",
735 ..
736 }
737 ));
738 }
739 {
740 let mut inputs = inputs.clone();
741 inputs.current_timestamp = inputs.cred_expires_at;
742 assert!(matches!(
743 check_nullifier_input_validity(&inputs).unwrap_err(),
744 super::ProofInputError::CredentialExpired { .. }
745 ));
746 }
747 {
748 let mut inputs = inputs.clone();
749 inputs.cred_genesis_issued_at_min = ark_bn254::Fr::from(1770125924u64);
751 assert!(matches!(
752 check_nullifier_input_validity(&inputs).unwrap_err(),
753 super::ProofInputError::CredentialGenesisExpired { .. }
754 ));
755 }
756 {
757 let mut inputs = inputs.clone();
758 inputs.cred_r = Affine {
759 x: ark_babyjubjub::Fq::from(1u64),
760 y: ark_babyjubjub::Fq::from(2u64),
761 };
762 assert!(matches!(
763 check_nullifier_input_validity(&inputs).unwrap_err(),
764 super::ProofInputError::InvalidBabyJubJubPoint {
765 name: "Credential Signature R"
766 }
767 ));
768 }
769 {
770 let mut inputs = inputs.clone();
771 inputs.cred_pk = Affine {
772 x: ark_babyjubjub::Fq::from(1u64),
773 y: ark_babyjubjub::Fq::from(2u64),
774 };
775 assert!(matches!(
776 check_nullifier_input_validity(&inputs).unwrap_err(),
777 super::ProofInputError::InvalidBabyJubJubPoint {
778 name: "Credential Public Key"
779 }
780 ));
781 }
782 {
783 let mut inputs = inputs.clone();
784 inputs.cred_s = ark_babyjubjub::Fr::from(12345u64);
785 assert!(matches!(
786 check_nullifier_input_validity(&inputs).unwrap_err(),
787 super::ProofInputError::InvalidCredentialSignature
788 ));
789 }
790 {
791 let mut inputs = inputs.clone();
792 inputs.oprf_pk = Affine {
793 x: ark_babyjubjub::Fq::from(1u64),
794 y: ark_babyjubjub::Fq::from(2u64),
795 };
796 assert!(matches!(
797 check_nullifier_input_validity(&inputs).unwrap_err(),
798 super::ProofInputError::InvalidBabyJubJubPoint {
799 name: "OPRF Public Key"
800 }
801 ));
802 }
803 {
804 let mut inputs = inputs.clone();
805 inputs.oprf_response_blinded = Affine {
806 x: ark_babyjubjub::Fq::from(1u64),
807 y: ark_babyjubjub::Fq::from(2u64),
808 };
809 assert!(matches!(
810 check_nullifier_input_validity(&inputs).unwrap_err(),
811 super::ProofInputError::InvalidBabyJubJubPoint {
812 name: "OPRF Blinded Response"
813 }
814 ));
815 }
816 {
817 let mut inputs = inputs.clone();
818 inputs.dlog_s = ark_babyjubjub::Fr::from(12345u64);
819 assert!(matches!(
820 check_nullifier_input_validity(&inputs).unwrap_err(),
821 super::ProofInputError::InvalidOprfProof
822 ));
823 }
824 {
825 let mut inputs = inputs.clone();
826 inputs.oprf_response = Affine {
827 x: ark_babyjubjub::Fq::from(1u64),
828 y: ark_babyjubjub::Fq::from(2u64),
829 };
830 assert!(matches!(
831 check_nullifier_input_validity(&inputs).unwrap_err(),
832 super::ProofInputError::InvalidBabyJubJubPoint {
833 name: "OPRF Unblinded Response"
834 }
835 ));
836 }
837 {
838 let mut inputs = inputs.clone();
839 use ark_ec::AffineRepr;
841 inputs.oprf_response = ark_babyjubjub::EdwardsAffine::generator();
842 assert!(matches!(
843 check_nullifier_input_validity(&inputs).unwrap_err(),
844 super::ProofInputError::InvalidOprfResponse
845 ));
846 }
847 {
848 let mut inputs = inputs.clone();
849 inputs.id_commitment = ark_bn254::Fr::from(12345u64);
850 assert!(matches!(
851 check_nullifier_input_validity(&inputs).unwrap_err(),
852 super::ProofInputError::InvalidSessionId
853 ));
854 }
855 }
856
857 #[test]
865 fn test_packed_cred_id_matches_credential_hash() {
866 use super::hash_credential;
867 use ark_ff::BigInt;
868 use world_id_primitives::{Credential, FieldElement};
869
870 let mut credential = Credential::new();
871 credential.id = 0x1234_5678_9ABC_DEF0;
872 credential.issuer_version = 7;
873 credential.issuer_schema_id = 42;
874 credential.sub = FieldElement::from(100u64);
875 credential.genesis_issued_at = 1_000_000;
876 credential.expires_at = 2_000_000;
877
878 let direct = credential.hash().unwrap();
879
880 let packed_cred_id: ark_babyjubjub::Fq =
881 BigInt([credential.id, u64::from(credential.issuer_version), 0, 0]).into();
882
883 let via_validation = hash_credential(
884 FieldElement::from(credential.issuer_schema_id),
885 credential.sub,
886 FieldElement::from(credential.genesis_issued_at),
887 FieldElement::from(credential.expires_at),
888 credential.claims_hash().unwrap(),
889 credential.associated_data_commitment,
890 FieldElement::from(packed_cred_id),
891 );
892
893 assert_eq!(
894 direct, via_validation,
895 "packed cred_id must reproduce Credential::hash"
896 );
897
898 let unpacked_cred_id: ark_babyjubjub::Fq = credential.id.into();
899 let via_validation_unpacked = hash_credential(
900 FieldElement::from(credential.issuer_schema_id),
901 credential.sub,
902 FieldElement::from(credential.genesis_issued_at),
903 FieldElement::from(credential.expires_at),
904 credential.claims_hash().unwrap(),
905 credential.associated_data_commitment,
906 FieldElement::from(unpacked_cred_id),
907 );
908
909 assert_ne!(
910 direct, via_validation_unpacked,
911 "passing only id (without issuer_version) must NOT reproduce Credential::hash"
912 );
913 }
914}