1mod constraints;
7pub use constraints::{ConstraintExpr, ConstraintKind, ConstraintNode, MAX_CONSTRAINT_NODES};
8
9use serde::de::Error as _;
10use serde::{Deserialize, Serialize};
11use std::collections::HashSet;
12use taceo_oprf_types::crypto::OprfPublicKey;
13use world_id_primitives::rp::RpId;
14use world_id_primitives::{FieldElement, PrimitiveError, WorldIdProof};
15
16#[repr(u8)]
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum RequestVersion {
20 V1 = 1,
22}
23
24impl serde::Serialize for RequestVersion {
25 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
26 where
27 S: serde::Serializer,
28 {
29 let v = *self as u8;
30 serializer.serialize_u8(v)
31 }
32}
33
34impl<'de> serde::Deserialize<'de> for RequestVersion {
35 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
36 where
37 D: serde::Deserializer<'de>,
38 {
39 let v = u8::deserialize(deserializer)?;
40 match v {
41 1 => Ok(Self::V1),
42 _ => Err(serde::de::Error::custom("unsupported version")),
43 }
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(deny_unknown_fields)]
50pub struct ProofRequest {
51 pub id: String,
53 pub version: RequestVersion,
55 pub created_at: u64,
57 pub expires_at: u64,
59 pub rp_id: RpId,
61 pub action: FieldElement,
66 pub oprf_public_key: OprfPublicKey,
68 pub signature: k256::ecdsa::Signature,
70 pub nonce: FieldElement,
72 #[serde(rename = "proof_requests")]
74 pub requests: Vec<RequestItem>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub constraints: Option<ConstraintExpr<'static>>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct RequestItem {
84 pub identifier: String,
88
89 pub issuer_schema_id: FieldElement,
92 #[serde(skip_serializing_if = "Option::is_none")]
98 pub signal: Option<String>,
99
100 pub genesis_issued_at_min: Option<u64>,
105
106 pub session_id: Option<FieldElement>,
111}
112
113impl RequestItem {
114 #[must_use]
116 pub const fn new(
117 identifier: String,
118 issuer_schema_id: FieldElement,
119 signal: Option<String>,
120 genesis_issued_at_min: Option<u64>,
121 session_id: Option<FieldElement>,
122 ) -> Self {
123 Self {
124 identifier,
125 issuer_schema_id,
126 signal,
127 genesis_issued_at_min,
128 session_id,
129 }
130 }
131
132 #[must_use]
134 pub fn signal_hash(&self) -> FieldElement {
135 if let Some(signal) = &self.signal {
136 FieldElement::from_arbitrary_raw_bytes(signal.as_bytes())
137 } else {
138 FieldElement::ZERO
139 }
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145#[serde(deny_unknown_fields)]
146pub struct ProofResponse {
147 pub id: String,
149 pub version: RequestVersion,
151 pub responses: Vec<ResponseItem>,
153}
154
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
161#[serde(deny_unknown_fields)]
162pub struct ResponseItem {
163 pub identifier: String,
167
168 pub issuer_schema_id: FieldElement,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub proof: Option<WorldIdProof>,
173 #[serde(skip_serializing_if = "Option::is_none")]
178 pub nullifier: Option<FieldElement>,
179 #[serde(skip_serializing_if = "Option::is_none")]
185 pub session_id: Option<FieldElement>,
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub error: Option<String>,
189}
190
191impl ProofResponse {
192 #[must_use]
194 pub fn constraints_satisfied(&self, constraints: &ConstraintExpr<'_>) -> bool {
195 let provided: HashSet<&str> = self
196 .responses
197 .iter()
198 .filter(|item| item.error.is_none())
199 .map(|item| item.identifier.as_str())
200 .collect();
201
202 constraints.evaluate(&|t| provided.contains(t))
203 }
204}
205
206impl ProofRequest {
207 #[must_use]
215 pub fn credentials_to_prove(&self, available: &HashSet<String>) -> Option<Vec<&RequestItem>> {
216 let requested: HashSet<&str> = self
218 .requests
219 .iter()
220 .map(|r| r.identifier.as_str())
221 .collect();
222
223 let is_selectable =
225 |identifier: &str| available.contains(identifier) && requested.contains(identifier);
226
227 if self.constraints.is_none() {
229 return if self
230 .requests
231 .iter()
232 .all(|r| available.contains(&r.identifier))
233 {
234 Some(self.requests.iter().collect())
235 } else {
236 None
237 };
238 }
239
240 let selected_identifiers = select_expr(self.constraints.as_ref().unwrap(), &is_selectable)?;
242 let selected_set: HashSet<&str> = selected_identifiers.into_iter().collect();
243
244 let result: Vec<&RequestItem> = self
246 .requests
247 .iter()
248 .filter(|r| selected_set.contains(r.identifier.as_str()))
249 .collect();
250 Some(result)
251 }
252
253 #[must_use]
255 pub fn find_request_by_issuer_schema_id(
256 &self,
257 issuer_schema_id: FieldElement,
258 ) -> Option<&RequestItem> {
259 self.requests
260 .iter()
261 .find(|r| r.issuer_schema_id == issuer_schema_id)
262 }
263
264 #[must_use]
266 pub const fn is_expired(&self, now: u64) -> bool {
267 now > self.expires_at
268 }
269
270 pub fn digest_hash(&self) -> Result<[u8; 32], PrimitiveError> {
282 use k256::sha2::{Digest, Sha256};
283
284 let mut writer = Vec::new();
285 let mut hasher = Sha256::new();
286 self.nonce.serialize_as_bytes(&mut writer)?;
287 hasher.update(&writer);
288 hasher.update(self.created_at.to_be_bytes());
290 Ok(hasher.finalize().into())
291 }
292
293 pub fn validate_response(&self, response: &ProofResponse) -> Result<(), ValidationError> {
299 if self.id != response.id {
301 return Err(ValidationError::RequestIdMismatch);
302 }
303 if self.version != response.version {
304 return Err(ValidationError::VersionMismatch);
305 }
306
307 let provided: HashSet<&str> = response
309 .responses
310 .iter()
311 .filter(|r| r.error.is_none())
312 .map(|r| r.identifier.as_str())
313 .collect();
314
315 match &self.constraints {
316 None => {
318 for req in &self.requests {
319 if !provided.contains(req.identifier.as_str()) {
320 return Err(ValidationError::MissingCredential(req.identifier.clone()));
321 }
322 }
323 Ok(())
324 }
325 Some(expr) => {
326 if !expr.validate_max_depth(2) {
327 return Err(ValidationError::ConstraintTooDeep);
328 }
329 if !expr.validate_max_nodes(MAX_CONSTRAINT_NODES) {
330 return Err(ValidationError::ConstraintTooLarge);
331 }
332 if expr.evaluate(&|t| provided.contains(t)) {
333 Ok(())
334 } else {
335 Err(ValidationError::ConstraintNotSatisfied)
336 }
337 }
338 }
339 }
340
341 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
346 let v: Self = serde_json::from_str(json)?;
347 let mut seen: HashSet<String> = HashSet::new();
349 for r in &v.requests {
350 let t = r.issuer_schema_id.to_string();
351 if !seen.insert(t.clone()) {
352 return Err(serde_json::Error::custom(format!(
353 "duplicate issuer schema id: {t}"
354 )));
355 }
356 }
357 Ok(v)
358 }
359
360 pub fn to_json(&self) -> Result<String, serde_json::Error> {
365 serde_json::to_string(self)
366 }
367
368 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
373 serde_json::to_string_pretty(self)
374 }
375}
376
377impl ProofResponse {
378 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
383 serde_json::from_str(json)
384 }
385
386 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
391 serde_json::to_string_pretty(self)
392 }
393
394 #[must_use]
396 pub fn successful_credentials(&self) -> Vec<String> {
397 self.responses
398 .iter()
399 .filter(|r| r.error.is_none())
400 .map(|r| r.issuer_schema_id.to_string())
401 .collect()
402 }
403}
404
405#[derive(Debug, thiserror::Error, PartialEq, Eq)]
407pub enum ValidationError {
408 #[error("Request ID mismatch")]
410 RequestIdMismatch,
411 #[error("Version mismatch")]
413 VersionMismatch,
414 #[error("Missing required credential: {0}")]
416 MissingCredential(String),
417 #[error("Constraints not satisfied")]
419 ConstraintNotSatisfied,
420 #[error("Constraints nesting exceeds maximum allowed depth")]
422 ConstraintTooDeep,
423 #[error("Constraints exceed maximum allowed size")]
425 ConstraintTooLarge,
426}
427
428fn select_node<'a, F>(node: &'a ConstraintNode<'a>, pred: &F) -> Option<Vec<&'a str>>
430where
431 F: Fn(&str) -> bool,
432{
433 match node {
434 ConstraintNode::Type(t) => pred(t.as_ref()).then(|| vec![t.as_ref()]),
435 ConstraintNode::Expr(e) => select_expr(e, pred),
436 }
437}
438
439fn select_expr<'a, F>(expr: &'a ConstraintExpr<'a>, pred: &F) -> Option<Vec<&'a str>>
440where
441 F: Fn(&str) -> bool,
442{
443 match expr {
444 ConstraintExpr::All { all } => {
445 let mut seen: std::collections::HashSet<&'a str> = std::collections::HashSet::new();
446 let mut out: Vec<&'a str> = Vec::new();
447 for n in all {
448 let sub = select_node(n, pred)?;
449 for s in sub {
450 if seen.insert(s) {
451 out.push(s);
452 }
453 }
454 }
455 Some(out)
456 }
457 ConstraintExpr::Any { any } => any.iter().find_map(|n| select_node(n, pred)),
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use alloy::uint;
465 use k256::ecdsa::{signature::Signer, SigningKey};
466
467 fn test_signature() -> k256::ecdsa::Signature {
469 let signing_key = SigningKey::from_bytes(&[1u8; 32].into()).unwrap();
470 signing_key.sign(b"test")
471 }
472
473 fn test_oprf_public_key() -> OprfPublicKey {
474 use ark_ec::AffineRepr;
476 OprfPublicKey::new(ark_babyjubjub::EdwardsAffine::generator())
477 }
478
479 fn test_nonce() -> FieldElement {
480 FieldElement::from(1u64)
481 }
482
483 fn test_field_element(n: u64) -> FieldElement {
484 FieldElement::from(n)
485 }
486
487 #[test]
488 fn constraints_all_any_nested() {
489 let id1 = test_field_element(1);
491 let id2 = test_field_element(2);
492 let id3 = test_field_element(3);
493
494 let response = ProofResponse {
495 id: "req_123".into(),
496 version: RequestVersion::V1,
497 responses: vec![
498 ResponseItem {
499 identifier: "test_req_1".into(),
500 issuer_schema_id: id1,
501 proof: Some(WorldIdProof::default()),
502 nullifier: Some(test_field_element(1001)),
503 session_id: None,
504 error: None,
505 },
506 ResponseItem {
507 identifier: "test_req_2".into(),
508 issuer_schema_id: id2,
509 proof: Some(WorldIdProof::default()),
510 nullifier: Some(test_field_element(1002)),
511 session_id: None,
512 error: None,
513 },
514 ResponseItem {
515 identifier: "test_req_3".into(),
516 issuer_schema_id: id3,
517 proof: None,
518 nullifier: None,
519 session_id: None,
520 error: Some("credential_not_available".into()),
521 },
522 ],
523 };
524
525 let expr = ConstraintExpr::All {
527 all: vec![
528 ConstraintNode::Type("test_req_1".into()),
529 ConstraintNode::Expr(ConstraintExpr::Any {
530 any: vec![
531 ConstraintNode::Type("test_req_2".into()),
532 ConstraintNode::Type("test_req_4".into()),
533 ],
534 }),
535 ],
536 };
537
538 assert!(response.constraints_satisfied(&expr));
539
540 let fail_expr = ConstraintExpr::All {
542 all: vec![
543 ConstraintNode::Type("test_req_1".into()),
544 ConstraintNode::Type("test_req_3".into()),
545 ],
546 };
547 assert!(!response.constraints_satisfied(&fail_expr));
548 }
549
550 #[test]
551 fn test_digest_hash() {
552 let request = ProofRequest {
553 id: "test_request".into(),
554 version: RequestVersion::V1,
555 created_at: 1_700_000_000,
556 expires_at: 1_700_100_000,
557 rp_id: RpId::from(uint!(1_U160)),
558 action: FieldElement::ZERO,
559 oprf_public_key: test_oprf_public_key(),
560 signature: test_signature(),
561 nonce: test_nonce(),
562 requests: vec![RequestItem {
563 identifier: "orb".into(),
564 issuer_schema_id: test_field_element(1),
565 signal: Some("test_signal".into()),
566 genesis_issued_at_min: None,
567 session_id: None,
568 }],
569 constraints: None,
570 };
571
572 let digest1 = request.digest_hash().unwrap();
573 assert_eq!(digest1.len(), 32);
575
576 let digest2 = request.digest_hash().unwrap();
578 assert_eq!(digest1, digest2);
579
580 let request2 = ProofRequest {
582 nonce: test_field_element(3),
583 ..request
584 };
585 let digest3 = request2.digest_hash().unwrap();
586 assert_ne!(digest1, digest3);
587 }
588
589 #[test]
590 fn request_validate_response_none_constraints_means_all() {
591 let request = ProofRequest {
592 id: "req_1".into(),
593 version: RequestVersion::V1,
594 created_at: 1_735_689_600,
595 expires_at: 1_735_689_600, rp_id: RpId::from(uint!(1_U160)),
597 action: FieldElement::ZERO,
598 oprf_public_key: test_oprf_public_key(),
599 signature: test_signature(),
600 nonce: test_nonce(),
601 requests: vec![
602 RequestItem {
603 identifier: "orb".into(),
604 issuer_schema_id: test_field_element(1),
605 signal: None,
606 genesis_issued_at_min: None,
607 session_id: None,
608 },
609 RequestItem {
610 identifier: "document".into(),
611 issuer_schema_id: test_field_element(2),
612 signal: None,
613 genesis_issued_at_min: None,
614 session_id: None,
615 },
616 ],
617 constraints: None,
618 };
619
620 let ok = ProofResponse {
621 id: "req_1".into(),
622 version: RequestVersion::V1,
623 responses: vec![
624 ResponseItem {
625 identifier: "orb".into(),
626 issuer_schema_id: test_field_element(1),
627 proof: Some(WorldIdProof::default()),
628 nullifier: None,
629 session_id: None,
630 error: None,
631 },
632 ResponseItem {
633 identifier: "document".into(),
634 issuer_schema_id: test_field_element(2),
635 proof: Some(WorldIdProof::default()),
636 nullifier: None,
637 session_id: None,
638 error: None,
639 },
640 ],
641 };
642 assert!(request.validate_response(&ok).is_ok());
643
644 let missing = ProofResponse {
645 id: "req_1".into(),
646 version: RequestVersion::V1,
647 responses: vec![ResponseItem {
648 identifier: "orb".into(),
649 issuer_schema_id: test_field_element(1),
650 proof: Some(WorldIdProof::default()),
651 nullifier: None,
652 session_id: None,
653 error: None,
654 }],
655 };
656 let err = request.validate_response(&missing).unwrap_err();
657 assert!(matches!(err, ValidationError::MissingCredential(_)));
658 }
659
660 #[test]
661 fn constraint_depth_enforced() {
662 let deep = ConstraintExpr::All {
664 all: vec![ConstraintNode::Expr(ConstraintExpr::Any {
665 any: vec![ConstraintNode::Expr(ConstraintExpr::All {
666 all: vec![ConstraintNode::Type("orb".into())],
667 })],
668 })],
669 };
670
671 let request = ProofRequest {
672 id: "req_2".into(),
673 version: RequestVersion::V1,
674 created_at: 1_735_689_600,
675 expires_at: 1_735_689_600,
676 rp_id: RpId::from(uint!(1_U160)),
677 action: test_field_element(1),
678 oprf_public_key: test_oprf_public_key(),
679 signature: test_signature(),
680 nonce: test_nonce(),
681 requests: vec![RequestItem {
682 identifier: "orb".into(),
683 issuer_schema_id: test_field_element(1),
684 signal: None,
685 genesis_issued_at_min: None,
686 session_id: None,
687 }],
688 constraints: Some(deep),
689 };
690
691 let response = ProofResponse {
692 id: "req_2".into(),
693 version: RequestVersion::V1,
694 responses: vec![ResponseItem {
695 identifier: "orb".into(),
696 issuer_schema_id: test_field_element(1),
697 proof: Some(WorldIdProof::default()),
698 nullifier: None,
699 session_id: None,
700 error: None,
701 }],
702 };
703
704 let err = request.validate_response(&response).unwrap_err();
705 assert!(matches!(err, ValidationError::ConstraintTooDeep));
706 }
707
708 #[test]
709 #[allow(clippy::too_many_lines)]
710 fn constraint_node_limit_boundary_passes() {
711 let id10 = test_field_element(10);
714 let id11 = test_field_element(11);
715 let id12 = test_field_element(12);
716 let id13 = test_field_element(13);
717 let id14 = test_field_element(14);
718 let id15 = test_field_element(15);
719 let id16 = test_field_element(16);
720 let id17 = test_field_element(17);
721 let id18 = test_field_element(18);
722
723 let expr = ConstraintExpr::All {
724 all: vec![
725 ConstraintNode::Type("test_req_10".into()),
726 ConstraintNode::Expr(ConstraintExpr::Any {
727 any: vec![
728 ConstraintNode::Type("test_req_11".into()),
729 ConstraintNode::Type("test_req_12".into()),
730 ConstraintNode::Type("test_req_13".into()),
731 ConstraintNode::Type("test_req_14".into()),
732 ],
733 }),
734 ConstraintNode::Expr(ConstraintExpr::Any {
735 any: vec![
736 ConstraintNode::Type("test_req_15".into()),
737 ConstraintNode::Type("test_req_16".into()),
738 ConstraintNode::Type("test_req_17".into()),
739 ConstraintNode::Type("test_req_18".into()),
740 ],
741 }),
742 ],
743 };
744
745 let request = ProofRequest {
746 id: "req_nodes_ok".into(),
747 version: RequestVersion::V1,
748 created_at: 1_735_689_600,
749 expires_at: 1_735_689_600,
750 rp_id: RpId::from(uint!(1_U160)),
751 action: test_field_element(5),
752 oprf_public_key: test_oprf_public_key(),
753 signature: test_signature(),
754 nonce: test_nonce(),
755 requests: vec![
756 RequestItem {
757 identifier: "test_req_10".into(),
758 issuer_schema_id: id10,
759 signal: None,
760 genesis_issued_at_min: None,
761 session_id: None,
762 },
763 RequestItem {
764 identifier: "test_req_11".into(),
765 issuer_schema_id: id11,
766 signal: None,
767 genesis_issued_at_min: None,
768 session_id: None,
769 },
770 RequestItem {
771 identifier: "test_req_12".into(),
772 issuer_schema_id: id12,
773 signal: None,
774 genesis_issued_at_min: None,
775 session_id: None,
776 },
777 RequestItem {
778 identifier: "test_req_13".into(),
779 issuer_schema_id: id13,
780 signal: None,
781 genesis_issued_at_min: None,
782 session_id: None,
783 },
784 RequestItem {
785 identifier: "test_req_14".into(),
786 issuer_schema_id: id14,
787 signal: None,
788 genesis_issued_at_min: None,
789 session_id: None,
790 },
791 RequestItem {
792 identifier: "test_req_15".into(),
793 issuer_schema_id: id15,
794 signal: None,
795 genesis_issued_at_min: None,
796 session_id: None,
797 },
798 RequestItem {
799 identifier: "test_req_16".into(),
800 issuer_schema_id: id16,
801 signal: None,
802 genesis_issued_at_min: None,
803 session_id: None,
804 },
805 RequestItem {
806 identifier: "test_req_17".into(),
807 issuer_schema_id: id17,
808 signal: None,
809 genesis_issued_at_min: None,
810 session_id: None,
811 },
812 RequestItem {
813 identifier: "test_req_18".into(),
814 issuer_schema_id: id18,
815 signal: None,
816 genesis_issued_at_min: None,
817 session_id: None,
818 },
819 ],
820 constraints: Some(expr),
821 };
822
823 let response = ProofResponse {
825 id: "req_nodes_ok".into(),
826 version: RequestVersion::V1,
827 responses: vec![
828 ResponseItem {
829 identifier: "test_req_10".into(),
830 issuer_schema_id: id10,
831 proof: Some(WorldIdProof::default()),
832 nullifier: None,
833 session_id: None,
834 error: None,
835 },
836 ResponseItem {
837 identifier: "test_req_11".into(),
838 issuer_schema_id: id11,
839 proof: Some(WorldIdProof::default()),
840 nullifier: None,
841 session_id: None,
842 error: None,
843 },
844 ResponseItem {
845 identifier: "test_req_15".into(),
846 issuer_schema_id: id15,
847 proof: Some(WorldIdProof::default()),
848 nullifier: None,
849 session_id: None,
850 error: None,
851 },
852 ],
853 };
854
855 assert!(request.validate_response(&response).is_ok());
857 }
858
859 #[test]
860 #[allow(clippy::too_many_lines)]
861 fn constraint_node_limit_exceeded_fails() {
862 let expr = ConstraintExpr::All {
865 all: vec![
866 ConstraintNode::Type("t0".into()),
867 ConstraintNode::Expr(ConstraintExpr::Any {
868 any: vec![
869 ConstraintNode::Type("t1".into()),
870 ConstraintNode::Type("t2".into()),
871 ConstraintNode::Type("t3".into()),
872 ConstraintNode::Type("t4".into()),
873 ],
874 }),
875 ConstraintNode::Expr(ConstraintExpr::Any {
876 any: vec![
877 ConstraintNode::Type("t5".into()),
878 ConstraintNode::Type("t6".into()),
879 ConstraintNode::Type("t7".into()),
880 ConstraintNode::Type("t8".into()),
881 ConstraintNode::Type("t9".into()),
882 ],
883 }),
884 ],
885 };
886
887 let request = ProofRequest {
888 id: "req_nodes_too_many".into(),
889 version: RequestVersion::V1,
890 created_at: 1_735_689_600,
891 expires_at: 1_735_689_600,
892 rp_id: RpId::from(uint!(1_U160)),
893 action: test_field_element(1),
894 oprf_public_key: test_oprf_public_key(),
895 signature: test_signature(),
896 nonce: test_nonce(),
897 requests: vec![
898 RequestItem {
899 identifier: "test_req_20".into(),
900 issuer_schema_id: test_field_element(20),
901 signal: None,
902 genesis_issued_at_min: None,
903 session_id: None,
904 },
905 RequestItem {
906 identifier: "test_req_21".into(),
907 issuer_schema_id: test_field_element(21),
908 signal: None,
909 genesis_issued_at_min: None,
910 session_id: None,
911 },
912 RequestItem {
913 identifier: "test_req_22".into(),
914 issuer_schema_id: test_field_element(22),
915 signal: None,
916 genesis_issued_at_min: None,
917 session_id: None,
918 },
919 RequestItem {
920 identifier: "test_req_23".into(),
921 issuer_schema_id: test_field_element(23),
922 signal: None,
923 genesis_issued_at_min: None,
924 session_id: None,
925 },
926 RequestItem {
927 identifier: "test_req_24".into(),
928 issuer_schema_id: test_field_element(24),
929 signal: None,
930 genesis_issued_at_min: None,
931 session_id: None,
932 },
933 RequestItem {
934 identifier: "test_req_25".into(),
935 issuer_schema_id: test_field_element(25),
936 signal: None,
937 genesis_issued_at_min: None,
938 session_id: None,
939 },
940 RequestItem {
941 identifier: "test_req_26".into(),
942 issuer_schema_id: test_field_element(26),
943 signal: None,
944 genesis_issued_at_min: None,
945 session_id: None,
946 },
947 RequestItem {
948 identifier: "test_req_27".into(),
949 issuer_schema_id: test_field_element(27),
950 signal: None,
951 genesis_issued_at_min: None,
952 session_id: None,
953 },
954 RequestItem {
955 identifier: "test_req_28".into(),
956 issuer_schema_id: test_field_element(28),
957 signal: None,
958 genesis_issued_at_min: None,
959 session_id: None,
960 },
961 RequestItem {
962 identifier: "test_req_29".into(),
963 issuer_schema_id: test_field_element(29),
964 signal: None,
965 genesis_issued_at_min: None,
966 session_id: None,
967 },
968 ],
969 constraints: Some(expr),
970 };
971
972 let response = ProofResponse {
974 id: "req_nodes_too_many".into(),
975 version: RequestVersion::V1,
976 responses: vec![ResponseItem {
977 identifier: "test_req_20".into(),
978 issuer_schema_id: test_field_element(20),
979 proof: Some(WorldIdProof::default()),
980 nullifier: None,
981 session_id: None,
982 error: None,
983 }],
984 };
985
986 let err = request.validate_response(&response).unwrap_err();
987 assert!(matches!(err, ValidationError::ConstraintTooLarge));
988 }
989
990 #[test]
991 fn request_single_credential_parse_and_validate() {
992 let req = ProofRequest {
993 id: "req_18c0f7f03e7d".into(),
994 version: RequestVersion::V1,
995 created_at: 1_725_381_192,
996 expires_at: 1_725_381_492,
997 rp_id: RpId::from(uint!(1_U160)),
998 action: test_field_element(1),
999 oprf_public_key: test_oprf_public_key(),
1000 signature: test_signature(),
1001 nonce: test_nonce(),
1002 requests: vec![RequestItem {
1003 identifier: "test_req_1".into(),
1004 issuer_schema_id: test_field_element(1),
1005 signal: Some("abcd-efgh-ijkl".into()),
1006 genesis_issued_at_min: Some(1_725_381_192),
1007 session_id: Some(test_field_element(55)),
1008 }],
1009 constraints: None,
1010 };
1011
1012 assert_eq!(req.id, "req_18c0f7f03e7d");
1013 assert_eq!(req.requests.len(), 1);
1014
1015 let resp = ProofResponse {
1017 id: req.id.clone(),
1018 version: RequestVersion::V1,
1019 responses: vec![ResponseItem {
1020 identifier: "test_req_1".into(),
1021 issuer_schema_id: test_field_element(1),
1022 proof: Some(WorldIdProof::default()),
1023 nullifier: Some(test_field_element(1001)),
1024 session_id: None,
1025 error: None,
1026 }],
1027 };
1028 assert!(req.validate_response(&resp).is_ok());
1029 }
1030
1031 #[test]
1032 fn request_multiple_credentials_all_constraint_and_failure() {
1033 let req = ProofRequest {
1034 id: "req_18c0f7f03e7d".into(),
1035 version: RequestVersion::V1,
1036 created_at: 1_725_381_192,
1037 expires_at: 1_725_381_492,
1038 rp_id: RpId::from(uint!(1_U160)),
1039 action: test_field_element(1),
1040 oprf_public_key: test_oprf_public_key(),
1041 signature: test_signature(),
1042 nonce: test_nonce(),
1043 requests: vec![
1044 RequestItem {
1045 identifier: "test_req_1".into(),
1046 issuer_schema_id: test_field_element(1),
1047 signal: Some("abcd-efgh-ijkl".into()),
1048 genesis_issued_at_min: Some(1_725_381_192),
1049 session_id: Some(test_field_element(100)),
1050 },
1051 RequestItem {
1052 identifier: "test_req_2".into(),
1053 issuer_schema_id: test_field_element(2),
1054 signal: Some("abcd-efgh-ijkl".into()),
1055 genesis_issued_at_min: Some(1_725_381_192),
1056 session_id: Some(test_field_element(12)),
1057 },
1058 ],
1059 constraints: Some(ConstraintExpr::All {
1060 all: vec![
1061 ConstraintNode::Type("test_req_1".into()),
1062 ConstraintNode::Type("test_req_2".into()),
1063 ],
1064 }),
1065 };
1066
1067 let resp = ProofResponse {
1069 id: req.id.clone(),
1070 version: RequestVersion::V1,
1071 responses: vec![
1072 ResponseItem {
1073 identifier: "test_req_2".into(),
1074 issuer_schema_id: test_field_element(2),
1075 proof: Some(WorldIdProof::default()),
1076 nullifier: Some(test_field_element(1001)),
1077 session_id: None,
1078 error: None,
1079 },
1080 ResponseItem {
1081 identifier: "test_req_1".into(),
1082 issuer_schema_id: test_field_element(1),
1083 proof: None,
1084 nullifier: None,
1085 session_id: None,
1086 error: Some("credential_not_available".into()),
1087 },
1088 ],
1089 };
1090
1091 let err = req.validate_response(&resp).unwrap_err();
1092 assert!(matches!(err, ValidationError::ConstraintNotSatisfied));
1093 }
1094
1095 #[test]
1096 fn request_more_complex_constraints_nested_success() {
1097 let req = ProofRequest {
1098 id: "req_18c0f7f03e7d".into(),
1099 version: RequestVersion::V1,
1100 created_at: 1_725_381_192,
1101 expires_at: 1_725_381_492,
1102 rp_id: RpId::from(uint!(1_U160)),
1103 action: test_field_element(1),
1104 oprf_public_key: test_oprf_public_key(),
1105 signature: test_signature(),
1106 nonce: test_nonce(),
1107 requests: vec![
1108 RequestItem {
1109 identifier: "test_req_1".into(),
1110 issuer_schema_id: test_field_element(1),
1111 signal: Some("abcd-efgh-ijkl".into()),
1112 genesis_issued_at_min: None,
1113 session_id: None,
1114 },
1115 RequestItem {
1116 identifier: "test_req_2".into(),
1117 issuer_schema_id: test_field_element(2),
1118 signal: Some("mnop-qrst-uvwx".into()),
1119 genesis_issued_at_min: None,
1120 session_id: None,
1121 },
1122 RequestItem {
1123 identifier: "test_req_3".into(),
1124 issuer_schema_id: test_field_element(3),
1125 signal: Some("abcd-efgh-ijkl".into()),
1126 genesis_issued_at_min: None,
1127 session_id: None,
1128 },
1129 ],
1130 constraints: Some(ConstraintExpr::All {
1131 all: vec![
1132 ConstraintNode::Type("test_req_3".into()),
1133 ConstraintNode::Expr(ConstraintExpr::Any {
1134 any: vec![
1135 ConstraintNode::Type("test_req_1".into()),
1136 ConstraintNode::Type("test_req_2".into()),
1137 ],
1138 }),
1139 ],
1140 }),
1141 };
1142
1143 let resp = ProofResponse {
1145 id: req.id.clone(),
1146 version: RequestVersion::V1,
1147 responses: vec![
1148 ResponseItem {
1149 identifier: "test_req_3".into(),
1150 issuer_schema_id: test_field_element(3),
1151 proof: Some(WorldIdProof::default()),
1152 nullifier: Some(test_field_element(1001)),
1153 session_id: None,
1154 error: None,
1155 },
1156 ResponseItem {
1157 identifier: "test_req_1".into(),
1158 issuer_schema_id: test_field_element(1),
1159 proof: Some(WorldIdProof::default()),
1160 nullifier: Some(test_field_element(1002)),
1161 session_id: None,
1162 error: None,
1163 },
1164 ],
1165 };
1166
1167 assert!(req.validate_response(&resp).is_ok());
1168 }
1169
1170 #[test]
1171 fn response_success_and_with_session_and_failure_parse() {
1172 let orb_id_str = test_field_element(100).to_string();
1174 let gov_id_str = test_field_element(101).to_string();
1175
1176 let ok_json = format!(
1177 r#"{{
1178 "id": "req_18c0f7f03e7d",
1179 "version": 1,
1180 "responses": [
1181 {{
1182 "identifier": "orb",
1183 "issuer_schema_id": "{orb_id_str}",
1184 "proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
1185 "nullifier": "0x00000000000000000000000000000000000000000000000000000000000003e9"
1186 }}
1187 ]
1188}}"#
1189 );
1190 let ok = ProofResponse::from_json(&ok_json).unwrap();
1191 assert_eq!(ok.successful_credentials(), vec![orb_id_str.clone()]);
1192
1193 let fail_json = format!(
1195 r#"{{
1196 "id": "req_18c0f7f03e7d",
1197 "version": 1,
1198 "responses": [
1199 {{ "identifier": "orb", "issuer_schema_id": "{orb_id_str}", "proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000", "nullifier": "0x00000000000000000000000000000000000000000000000000000000000003e9" }},
1200 {{ "identifier": "gov_id", "issuer_schema_id": "{gov_id_str}", "error": "credential_not_available" }}
1201 ]
1202}}"#
1203 );
1204 let fail = ProofResponse::from_json(&fail_json).unwrap();
1205 assert_eq!(fail.successful_credentials(), vec![orb_id_str.clone()]);
1206
1207 let sess_json = format!(
1209 r#"{{
1210 "id": "req_18c0f7f03e7d",
1211 "version": 1,
1212 "responses": [
1213 {{
1214 "identifier": "orb",
1215 "issuer_schema_id": "{orb_id_str}",
1216 "proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
1217 "nullifier": "0x00000000000000000000000000000000000000000000000000000000000003e9",
1218 "session_id": "0x00000000000000000000000000000000000000000000000000000000000003ea"
1219 }}
1220 ]
1221}}"#
1222 );
1223 let sess = ProofResponse::from_json(&sess_json).unwrap();
1224 assert_eq!(sess.successful_credentials(), vec![orb_id_str]);
1225 assert!(sess.responses[0].session_id.is_some());
1226 }
1227
1228 #[test]
1229 fn request_rejects_duplicate_issuer_schema_ids_on_parse() {
1230 let id1 = test_field_element(1);
1233 let req = ProofRequest {
1234 id: "req_dup".into(),
1235 version: RequestVersion::V1,
1236 created_at: 1_725_381_192,
1237 expires_at: 1_725_381_492,
1238 rp_id: RpId::from(uint!(1_U160)),
1239 action: test_field_element(5),
1240 oprf_public_key: test_oprf_public_key(),
1241 signature: test_signature(),
1242 nonce: test_nonce(),
1243 requests: vec![
1244 RequestItem {
1245 identifier: "test_req_1".into(),
1246 issuer_schema_id: id1,
1247 signal: None,
1248 genesis_issued_at_min: None,
1249 session_id: None,
1250 },
1251 RequestItem {
1252 identifier: "test_req_2".into(),
1253 issuer_schema_id: id1, signal: None,
1255 genesis_issued_at_min: None,
1256 session_id: None,
1257 },
1258 ],
1259 constraints: None,
1260 };
1261
1262 let json = req.to_json().unwrap();
1264 let err = ProofRequest::from_json(&json).unwrap_err();
1265 let msg = err.to_string();
1266 assert!(
1267 msg.contains("duplicate issuer schema id"),
1268 "Expected error message to contain 'duplicate issuer schema id', got: {msg}"
1269 );
1270 }
1271
1272 #[test]
1273 fn credentials_to_prove_none_constraints_requires_all_and_drops_if_missing() {
1274 let orb_id = test_field_element(100);
1275 let passport_id = test_field_element(101);
1276
1277 let req = ProofRequest {
1278 id: "req".into(),
1279 version: RequestVersion::V1,
1280 created_at: 1_735_689_600,
1281 expires_at: 1_735_689_600, rp_id: RpId::from(uint!(1_U160)),
1283 action: test_field_element(5),
1284 oprf_public_key: test_oprf_public_key(),
1285 signature: test_signature(),
1286 nonce: test_nonce(),
1287 requests: vec![
1288 RequestItem {
1289 identifier: "orb".into(),
1290 issuer_schema_id: orb_id,
1291 signal: None,
1292 genesis_issued_at_min: None,
1293 session_id: None,
1294 },
1295 RequestItem {
1296 identifier: "passport".into(),
1297 issuer_schema_id: passport_id,
1298 signal: None,
1299 genesis_issued_at_min: None,
1300 session_id: None,
1301 },
1302 ],
1303 constraints: None,
1304 };
1305
1306 let available_ok: HashSet<String> = ["orb".to_string(), "passport".to_string()]
1307 .into_iter()
1308 .collect();
1309 let sel_ok = req.credentials_to_prove(&available_ok).unwrap();
1310 assert_eq!(sel_ok.len(), 2);
1311 assert_eq!(sel_ok[0].issuer_schema_id, orb_id);
1312 assert_eq!(sel_ok[1].issuer_schema_id, passport_id);
1313
1314 let available_missing: HashSet<String> = std::iter::once("orb".to_string()).collect();
1315 assert!(req.credentials_to_prove(&available_missing).is_none());
1316 }
1317
1318 #[test]
1319 fn credentials_to_prove_with_constraints_all_and_any() {
1320 let orb_id = test_field_element(100);
1322 let passport_id = test_field_element(101);
1323 let national_id_id = test_field_element(102);
1324
1325 let req = ProofRequest {
1326 id: "req".into(),
1327 version: RequestVersion::V1,
1328 created_at: 1_735_689_600,
1329 expires_at: 1_735_689_600, rp_id: RpId::from(uint!(1_U160)),
1331 action: test_field_element(1),
1332 oprf_public_key: test_oprf_public_key(),
1333 signature: test_signature(),
1334 nonce: test_nonce(),
1335 requests: vec![
1336 RequestItem {
1337 identifier: "orb".into(),
1338 issuer_schema_id: orb_id,
1339 signal: None,
1340 genesis_issued_at_min: None,
1341 session_id: None,
1342 },
1343 RequestItem {
1344 identifier: "passport".into(),
1345 issuer_schema_id: passport_id,
1346 signal: None,
1347 genesis_issued_at_min: None,
1348 session_id: None,
1349 },
1350 RequestItem {
1351 identifier: "national_id".into(),
1352 issuer_schema_id: national_id_id,
1353 signal: None,
1354 genesis_issued_at_min: None,
1355 session_id: None,
1356 },
1357 ],
1358 constraints: Some(ConstraintExpr::All {
1359 all: vec![
1360 ConstraintNode::Type("orb".into()),
1361 ConstraintNode::Expr(ConstraintExpr::Any {
1362 any: vec![
1363 ConstraintNode::Type("passport".into()),
1364 ConstraintNode::Type("national_id".into()),
1365 ],
1366 }),
1367 ],
1368 }),
1369 };
1370
1371 let available1: HashSet<String> = ["orb".to_string(), "passport".to_string()]
1373 .into_iter()
1374 .collect();
1375 let sel1 = req.credentials_to_prove(&available1).unwrap();
1376 assert_eq!(sel1.len(), 2);
1377 assert_eq!(sel1[0].issuer_schema_id, orb_id);
1378 assert_eq!(sel1[1].issuer_schema_id, passport_id);
1379
1380 let available2: HashSet<String> = ["orb".to_string(), "national_id".to_string()]
1382 .into_iter()
1383 .collect();
1384 let sel2 = req.credentials_to_prove(&available2).unwrap();
1385 assert_eq!(sel2.len(), 2);
1386 assert_eq!(sel2[0].issuer_schema_id, orb_id);
1387 assert_eq!(sel2[1].issuer_schema_id, national_id_id);
1388
1389 let available3: HashSet<String> = std::iter::once("passport".to_string()).collect();
1391 assert!(req.credentials_to_prove(&available3).is_none());
1392 }
1393}