1mod constraints;
6pub use constraints::{ConstraintExpr, ConstraintKind, ConstraintNode, MAX_CONSTRAINT_NODES};
7
8use crate::{
9 FieldElement, Nullifier, PrimitiveError, SessionId, SessionNullifier, ZeroKnowledgeProof,
10 rp::RpId,
11};
12use serde::{Deserialize, Serialize, de::Error as _};
13use std::collections::HashSet;
14use taceo_oprf::types::OprfKeyId;
15use uuid as _;
17
18#[repr(u8)]
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum RequestVersion {
22 V1 = 1,
24}
25
26impl serde::Serialize for RequestVersion {
27 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
28 where
29 S: serde::Serializer,
30 {
31 let v = *self as u8;
32 serializer.serialize_u8(v)
33 }
34}
35
36impl<'de> serde::Deserialize<'de> for RequestVersion {
37 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
38 where
39 D: serde::Deserializer<'de>,
40 {
41 let v = u8::deserialize(deserializer)?;
42 match v {
43 1 => Ok(Self::V1),
44 _ => Err(serde::de::Error::custom("unsupported version")),
45 }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(deny_unknown_fields)]
52pub struct ProofRequest {
53 pub id: String,
55 pub version: RequestVersion,
57 pub created_at: u64,
59 pub expires_at: u64,
61 pub rp_id: RpId,
63 pub oprf_key_id: OprfKeyId,
65 pub session_id: Option<SessionId>,
71 pub action: Option<FieldElement>,
76 #[serde(with = "crate::serde_utils::hex_signature")]
78 pub signature: alloy::signers::Signature,
79 pub nonce: FieldElement,
81 #[serde(rename = "proof_requests")]
83 pub requests: Vec<RequestItem>,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub constraints: Option<ConstraintExpr<'static>>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(deny_unknown_fields)]
92pub struct RequestItem {
93 pub identifier: String,
97
98 pub issuer_schema_id: u64,
102
103 #[serde(default, skip_serializing_if = "Option::is_none")]
115 #[serde(with = "crate::serde_utils::hex_bytes_opt")]
116 pub signal: Option<Vec<u8>>,
117
118 pub genesis_issued_at_min: Option<u64>,
124
125 pub expires_at_min: Option<u64>,
139}
140
141impl RequestItem {
142 #[must_use]
144 pub const fn new(
145 identifier: String,
146 issuer_schema_id: u64,
147 signal: Option<Vec<u8>>,
148 genesis_issued_at_min: Option<u64>,
149 expires_at_min: Option<u64>,
150 ) -> Self {
151 Self {
152 identifier,
153 issuer_schema_id,
154 signal,
155 genesis_issued_at_min,
156 expires_at_min,
157 }
158 }
159
160 #[must_use]
162 pub fn signal_hash(&self) -> FieldElement {
163 if let Some(signal) = &self.signal {
164 FieldElement::from_arbitrary_raw_bytes(signal)
165 } else {
166 FieldElement::ZERO
167 }
168 }
169
170 #[must_use]
175 pub const fn effective_expires_at_min(&self, request_created_at: u64) -> u64 {
176 match self.expires_at_min {
177 Some(value) => value,
178 None => request_created_at,
179 }
180 }
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185#[serde(deny_unknown_fields)]
186pub struct ProofResponse {
187 pub id: String,
189 pub version: RequestVersion,
191 #[serde(skip_serializing_if = "Option::is_none")]
200 pub session_id: Option<SessionId>,
201 #[serde(skip_serializing_if = "Option::is_none")]
204 pub error: Option<String>,
205 pub responses: Vec<ResponseItem>,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
225#[serde(deny_unknown_fields)]
226pub struct ResponseItem {
227 pub identifier: String,
231
232 pub issuer_schema_id: u64,
234
235 pub proof: ZeroKnowledgeProof,
237
238 #[serde(skip_serializing_if = "Option::is_none")]
242 pub nullifier: Option<Nullifier>,
243
244 #[serde(skip_serializing_if = "Option::is_none")]
248 pub session_nullifier: Option<SessionNullifier>,
249
250 pub expires_at_min: u64,
254}
255
256impl ProofResponse {
257 #[must_use]
260 pub fn constraints_satisfied(&self, constraints: &ConstraintExpr<'_>) -> bool {
261 if self.error.is_some() {
263 return false;
264 }
265
266 let provided: HashSet<&str> = self
267 .responses
268 .iter()
269 .map(|item| item.identifier.as_str())
270 .collect();
271
272 constraints.evaluate(&|t| provided.contains(t))
273 }
274}
275
276impl ResponseItem {
277 #[must_use]
279 pub const fn new_uniqueness(
280 identifier: String,
281 issuer_schema_id: u64,
282 proof: ZeroKnowledgeProof,
283 nullifier: Nullifier,
284 expires_at_min: u64,
285 ) -> Self {
286 Self {
287 identifier,
288 issuer_schema_id,
289 proof,
290 nullifier: Some(nullifier),
291 session_nullifier: None,
292 expires_at_min,
293 }
294 }
295
296 #[must_use]
298 pub const fn new_session(
299 identifier: String,
300 issuer_schema_id: u64,
301 proof: ZeroKnowledgeProof,
302 session_nullifier: SessionNullifier,
303 expires_at_min: u64,
304 ) -> Self {
305 Self {
306 identifier,
307 issuer_schema_id,
308 proof,
309 nullifier: None,
310 session_nullifier: Some(session_nullifier),
311 expires_at_min,
312 }
313 }
314
315 #[must_use]
317 pub const fn is_session(&self) -> bool {
318 self.session_nullifier.is_some()
319 }
320
321 #[must_use]
323 pub const fn is_uniqueness(&self) -> bool {
324 self.nullifier.is_some()
325 }
326}
327
328impl ProofRequest {
329 #[must_use]
337 pub fn credentials_to_prove(&self, available: &HashSet<u64>) -> Option<Vec<&RequestItem>> {
338 let available_identifiers: HashSet<&str> = self
340 .requests
341 .iter()
342 .filter(|r| available.contains(&r.issuer_schema_id))
343 .map(|r| r.identifier.as_str())
344 .collect();
345
346 let is_selectable = |identifier: &str| available_identifiers.contains(identifier);
347
348 if self.constraints.is_none() {
350 return if self
351 .requests
352 .iter()
353 .all(|r| available.contains(&r.issuer_schema_id))
354 {
355 Some(self.requests.iter().collect())
356 } else {
357 None
358 };
359 }
360
361 let selected_identifiers = select_expr(self.constraints.as_ref().unwrap(), &is_selectable)?;
363 let selected_set: HashSet<&str> = selected_identifiers.into_iter().collect();
364
365 let result: Vec<&RequestItem> = self
367 .requests
368 .iter()
369 .filter(|r| selected_set.contains(r.identifier.as_str()))
370 .collect();
371 Some(result)
372 }
373
374 #[must_use]
376 pub fn find_request_by_issuer_schema_id(&self, issuer_schema_id: u64) -> Option<&RequestItem> {
377 self.requests
378 .iter()
379 .find(|r| r.issuer_schema_id == issuer_schema_id)
380 }
381
382 #[must_use]
384 pub const fn is_expired(&self, now: u64) -> bool {
385 now > self.expires_at
386 }
387
388 pub fn digest_hash(&self) -> Result<[u8; 32], PrimitiveError> {
402 use crate::rp::compute_rp_signature_msg;
403 use k256::sha2::{Digest, Sha256};
404
405 let msg = compute_rp_signature_msg(
406 *self.nonce,
407 self.created_at,
408 self.expires_at,
409 self.action.map(|v| *v),
410 );
411 let mut hasher = Sha256::new();
412 hasher.update(&msg);
413 Ok(hasher.finalize().into())
414 }
415
416 #[must_use]
418 pub const fn is_session_proof(&self) -> bool {
419 self.session_id.is_some()
420 }
421
422 pub fn validate_response(&self, response: &ProofResponse) -> Result<(), ValidationError> {
428 if self.id != response.id {
430 return Err(ValidationError::RequestIdMismatch);
431 }
432 if self.version != response.version {
433 return Err(ValidationError::VersionMismatch);
434 }
435
436 if let Some(error) = &response.error {
438 return Err(ValidationError::ProofGenerationFailed(error.clone()));
439 }
440
441 if self.session_id.is_some() && self.session_id != response.session_id {
443 return Err(ValidationError::SessionIdMismatch);
444 }
445
446 let mut provided: HashSet<&str> = HashSet::new();
448 for response_item in &response.responses {
449 if !provided.insert(response_item.identifier.as_str()) {
450 return Err(ValidationError::DuplicateCredential(
451 response_item.identifier.clone(),
452 ));
453 }
454
455 let request_item = self
456 .requests
457 .iter()
458 .find(|r| r.identifier == response_item.identifier)
459 .ok_or_else(|| {
460 ValidationError::UnexpectedCredential(response_item.identifier.clone())
461 })?;
462
463 if self.session_id.is_some() {
464 if response_item.session_nullifier.is_none() {
466 return Err(ValidationError::MissingSessionNullifier(
467 response_item.identifier.clone(),
468 ));
469 }
470 } else {
471 if response_item.nullifier.is_none() {
473 return Err(ValidationError::MissingNullifier(
474 response_item.identifier.clone(),
475 ));
476 }
477 }
478
479 let expected_expires_at_min = request_item.effective_expires_at_min(self.created_at);
480 if response_item.expires_at_min != expected_expires_at_min {
481 return Err(ValidationError::ExpiresAtMinMismatch(
482 response_item.identifier.clone(),
483 expected_expires_at_min,
484 response_item.expires_at_min,
485 ));
486 }
487 }
488
489 match &self.constraints {
490 None => {
492 for req in &self.requests {
493 if !provided.contains(req.identifier.as_str()) {
494 return Err(ValidationError::MissingCredential(req.identifier.clone()));
495 }
496 }
497 Ok(())
498 }
499 Some(expr) => {
500 if !expr.validate_max_depth(2) {
501 return Err(ValidationError::ConstraintTooDeep);
502 }
503 if !expr.validate_max_nodes(MAX_CONSTRAINT_NODES) {
504 return Err(ValidationError::ConstraintTooLarge);
505 }
506 if expr.evaluate(&|t| provided.contains(t)) {
507 Ok(())
508 } else {
509 Err(ValidationError::ConstraintNotSatisfied)
510 }
511 }
512 }
513 }
514
515 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
520 let v: Self = serde_json::from_str(json)?;
521 let mut seen: HashSet<String> = HashSet::new();
523 for r in &v.requests {
524 let t = r.issuer_schema_id.to_string();
525 if !seen.insert(t.clone()) {
526 return Err(serde_json::Error::custom(format!(
527 "duplicate issuer schema id: {t}"
528 )));
529 }
530 }
531 Ok(v)
532 }
533
534 pub fn to_json(&self) -> Result<String, serde_json::Error> {
539 serde_json::to_string(self)
540 }
541
542 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
547 serde_json::to_string_pretty(self)
548 }
549}
550
551impl ProofResponse {
552 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
557 serde_json::from_str(json)
558 }
559
560 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
565 serde_json::to_string_pretty(self)
566 }
567
568 #[must_use]
571 pub fn successful_credentials(&self) -> Vec<u64> {
572 if self.error.is_some() {
573 return vec![];
574 }
575 self.responses.iter().map(|r| r.issuer_schema_id).collect()
576 }
577}
578
579#[derive(Debug, thiserror::Error, PartialEq, Eq)]
581pub enum ValidationError {
582 #[error("Request ID mismatch")]
584 RequestIdMismatch,
585 #[error("Version mismatch")]
587 VersionMismatch,
588 #[error("Proof generation failed: {0}")]
590 ProofGenerationFailed(String),
591 #[error("Missing required credential: {0}")]
593 MissingCredential(String),
594 #[error("Unexpected credential in response: {0}")]
596 UnexpectedCredential(String),
597 #[error("Duplicate credential in response: {0}")]
599 DuplicateCredential(String),
600 #[error("Constraints not satisfied")]
602 ConstraintNotSatisfied,
603 #[error("Constraints nesting exceeds maximum allowed depth")]
605 ConstraintTooDeep,
606 #[error("Constraints exceed maximum allowed size")]
608 ConstraintTooLarge,
609 #[error("Invalid expires_at_min for credential '{0}': expected {1}, got {2}")]
611 ExpiresAtMinMismatch(String, u64, u64),
612 #[error("Session ID doesn't match between request and response")]
614 SessionIdMismatch,
615 #[error("Session nullifier missing for credential: {0}")]
617 MissingSessionNullifier(String),
618 #[error("Nullifier missing for credential: {0}")]
620 MissingNullifier(String),
621}
622
623fn select_node<'a, F>(node: &'a ConstraintNode<'a>, pred: &F) -> Option<Vec<&'a str>>
625where
626 F: Fn(&str) -> bool,
627{
628 match node {
629 ConstraintNode::Type(t) => pred(t.as_ref()).then(|| vec![t.as_ref()]),
630 ConstraintNode::Expr(e) => select_expr(e, pred),
631 }
632}
633
634fn select_expr<'a, F>(expr: &'a ConstraintExpr<'a>, pred: &F) -> Option<Vec<&'a str>>
635where
636 F: Fn(&str) -> bool,
637{
638 match expr {
639 ConstraintExpr::All { all } => {
640 let mut seen: std::collections::HashSet<&'a str> = std::collections::HashSet::new();
641 let mut out: Vec<&'a str> = Vec::new();
642 for n in all {
643 let sub = select_node(n, pred)?;
644 for s in sub {
645 if seen.insert(s) {
646 out.push(s);
647 }
648 }
649 }
650 Some(out)
651 }
652 ConstraintExpr::Any { any } => any.iter().find_map(|n| select_node(n, pred)),
653 ConstraintExpr::Enumerate { enumerate } => {
654 let mut seen: std::collections::HashSet<&'a str> = std::collections::HashSet::new();
656 let mut selected: Vec<&'a str> = Vec::new();
657
658 for child in enumerate {
660 let Some(child_selection) = select_node(child, pred) else {
661 continue;
662 };
663
664 for identifier in child_selection {
665 if seen.insert(identifier) {
666 selected.push(identifier);
667 }
668 }
669 }
670
671 if selected.is_empty() {
672 None
673 } else {
674 Some(selected)
675 }
676 }
677 }
678}
679
680#[cfg(test)]
681mod tests {
682 use super::*;
683 use crate::SessionNullifier;
684 use alloy::{
685 signers::{SignerSync, local::PrivateKeySigner},
686 uint,
687 };
688 use k256::ecdsa::SigningKey;
689
690 fn test_signature() -> alloy::signers::Signature {
692 let signer =
693 PrivateKeySigner::from_signing_key(SigningKey::from_bytes(&[1u8; 32].into()).unwrap());
694 signer.sign_message_sync(b"test").expect("can sign")
695 }
696
697 fn test_nonce() -> FieldElement {
698 FieldElement::from(1u64)
699 }
700
701 fn test_field_element(n: u64) -> FieldElement {
702 FieldElement::from(n)
703 }
704
705 fn test_action(n: u64) -> FieldElement {
707 use ruint::{aliases::U256, uint};
708 let v = U256::from(n)
709 | uint!(0x0200000000000000000000000000000000000000000000000000000000000000_U256);
710 FieldElement::try_from(v).expect("test value fits in field")
711 }
712
713 #[test]
714 fn constraints_all_any_nested() {
715 let response = ProofResponse {
717 id: "req_123".into(),
718 version: RequestVersion::V1,
719 session_id: None,
720 error: None,
721 responses: vec![
722 ResponseItem::new_uniqueness(
723 "test_req_1".into(),
724 1,
725 ZeroKnowledgeProof::default(),
726 test_field_element(1001).into(),
727 1_735_689_600,
728 ),
729 ResponseItem::new_uniqueness(
730 "test_req_2".into(),
731 2,
732 ZeroKnowledgeProof::default(),
733 test_field_element(1002).into(),
734 1_735_689_600,
735 ),
736 ],
737 };
738
739 let expr = ConstraintExpr::All {
741 all: vec![
742 ConstraintNode::Type("test_req_1".into()),
743 ConstraintNode::Expr(ConstraintExpr::Any {
744 any: vec![
745 ConstraintNode::Type("test_req_2".into()),
746 ConstraintNode::Type("test_req_4".into()),
747 ],
748 }),
749 ],
750 };
751
752 assert!(response.constraints_satisfied(&expr));
753
754 let fail_expr = ConstraintExpr::All {
756 all: vec![
757 ConstraintNode::Type("test_req_1".into()),
758 ConstraintNode::Type("test_req_3".into()),
759 ],
760 };
761 assert!(!response.constraints_satisfied(&fail_expr));
762 }
763
764 #[test]
765 fn constraints_enumerate_partial_and_empty() {
766 let response = ProofResponse {
768 id: "req_123".into(),
769 version: RequestVersion::V1,
770 session_id: None,
771 error: None,
772 responses: vec![
773 ResponseItem::new_uniqueness(
774 "orb".into(),
775 1,
776 ZeroKnowledgeProof::default(),
777 Nullifier::from(test_field_element(1001)),
778 1_735_689_600,
779 ),
780 ResponseItem::new_uniqueness(
781 "passport".into(),
782 2,
783 ZeroKnowledgeProof::default(),
784 Nullifier::from(test_field_element(1002)),
785 1_735_689_600,
786 ),
787 ],
788 };
789
790 let expr = ConstraintExpr::Enumerate {
792 enumerate: vec![
793 ConstraintNode::Type("passport".into()),
794 ConstraintNode::Type("national_id".into()),
795 ],
796 };
797 assert!(response.constraints_satisfied(&expr));
798
799 let fail_expr = ConstraintExpr::Enumerate {
801 enumerate: vec![
802 ConstraintNode::Type("national_id".into()),
803 ConstraintNode::Type("document".into()),
804 ],
805 };
806 assert!(!response.constraints_satisfied(&fail_expr));
807 }
808
809 #[test]
810 fn test_digest_hash() {
811 let request = ProofRequest {
812 id: "test_request".into(),
813 version: RequestVersion::V1,
814 created_at: 1_700_000_000,
815 expires_at: 1_700_100_000,
816 rp_id: RpId::new(1),
817 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
818 session_id: None,
819 action: Some(FieldElement::ZERO),
820 signature: test_signature(),
821 nonce: test_nonce(),
822 requests: vec![RequestItem {
823 identifier: "orb".into(),
824 issuer_schema_id: 1,
825 signal: Some("test_signal".into()),
826 genesis_issued_at_min: None,
827 expires_at_min: None,
828 }],
829 constraints: None,
830 };
831
832 let digest1 = request.digest_hash().unwrap();
833 assert_eq!(digest1.len(), 32);
835
836 let digest2 = request.digest_hash().unwrap();
838 assert_eq!(digest1, digest2);
839
840 let request2 = ProofRequest {
842 nonce: test_field_element(3),
843 ..request
844 };
845 let digest3 = request2.digest_hash().unwrap();
846 assert_ne!(digest1, digest3);
847 }
848
849 #[test]
850 fn proof_request_signature_serializes_as_hex_string() {
851 let request = ProofRequest {
852 id: "test".into(),
853 version: RequestVersion::V1,
854 created_at: 1_700_000_000,
855 expires_at: 1_700_100_000,
856 rp_id: RpId::new(1),
857 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
858 session_id: None,
859 action: None,
860 signature: test_signature(),
861 nonce: test_nonce(),
862 requests: vec![RequestItem {
863 identifier: "orb".into(),
864 issuer_schema_id: 1,
865 signal: None,
866 genesis_issued_at_min: None,
867 expires_at_min: None,
868 }],
869 constraints: None,
870 };
871
872 let json = request.to_json().unwrap();
873 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
874 let sig = value["signature"]
875 .as_str()
876 .expect("signature should be a string");
877 assert!(sig.starts_with("0x"));
878 assert_eq!(sig.len(), 132);
879
880 let roundtripped = ProofRequest::from_json(&json).unwrap();
881 assert_eq!(roundtripped.signature, request.signature);
882 }
883
884 #[test]
885 fn request_validate_response_none_constraints_means_all() {
886 let request = ProofRequest {
887 id: "req_1".into(),
888 version: RequestVersion::V1,
889 created_at: 1_735_689_600,
890 expires_at: 1_735_689_600, rp_id: RpId::new(1),
892 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
893 session_id: None,
894 action: Some(FieldElement::ZERO),
895 signature: test_signature(),
896 nonce: test_nonce(),
897 requests: vec![
898 RequestItem {
899 identifier: "orb".into(),
900 issuer_schema_id: 1,
901 signal: None,
902 genesis_issued_at_min: None,
903 expires_at_min: None,
904 },
905 RequestItem {
906 identifier: "document".into(),
907 issuer_schema_id: 2,
908 signal: None,
909 genesis_issued_at_min: None,
910 expires_at_min: None,
911 },
912 ],
913 constraints: None,
914 };
915
916 let ok = ProofResponse {
917 id: "req_1".into(),
918 version: RequestVersion::V1,
919 session_id: None,
920 error: None,
921 responses: vec![
922 ResponseItem::new_uniqueness(
923 "orb".into(),
924 1,
925 ZeroKnowledgeProof::default(),
926 Nullifier::from(test_field_element(1001)),
927 1_735_689_600,
928 ),
929 ResponseItem::new_uniqueness(
930 "document".into(),
931 2,
932 ZeroKnowledgeProof::default(),
933 Nullifier::from(test_field_element(1002)),
934 1_735_689_600,
935 ),
936 ],
937 };
938 assert!(request.validate_response(&ok).is_ok());
939
940 let missing = ProofResponse {
941 id: "req_1".into(),
942 version: RequestVersion::V1,
943 session_id: None,
944 error: None,
945 responses: vec![ResponseItem::new_uniqueness(
946 "orb".into(),
947 1,
948 ZeroKnowledgeProof::default(),
949 Nullifier::from(test_field_element(1001)),
950 1_735_689_600,
951 )],
952 };
953 let err = request.validate_response(&missing).unwrap_err();
954 assert!(matches!(err, ValidationError::MissingCredential(_)));
955
956 let unexpected = ProofResponse {
957 id: "req_1".into(),
958 version: RequestVersion::V1,
959 session_id: None,
960 error: None,
961 responses: vec![
962 ResponseItem::new_uniqueness(
963 "orb".into(),
964 1,
965 ZeroKnowledgeProof::default(),
966 Nullifier::from(test_field_element(1001)),
967 1_735_689_600,
968 ),
969 ResponseItem::new_uniqueness(
970 "document".into(),
971 2,
972 ZeroKnowledgeProof::default(),
973 Nullifier::from(test_field_element(1002)),
974 1_735_689_600,
975 ),
976 ResponseItem::new_uniqueness(
977 "passport".into(),
978 3,
979 ZeroKnowledgeProof::default(),
980 Nullifier::from(test_field_element(1003)),
981 1_735_689_600,
982 ),
983 ],
984 };
985 let err = request.validate_response(&unexpected).unwrap_err();
986 assert!(matches!(
987 err,
988 ValidationError::UnexpectedCredential(ref id) if id == "passport"
989 ));
990
991 let duplicate = ProofResponse {
992 id: "req_1".into(),
993 version: RequestVersion::V1,
994 session_id: None,
995 error: None,
996 responses: vec![
997 ResponseItem::new_uniqueness(
998 "orb".into(),
999 1,
1000 ZeroKnowledgeProof::default(),
1001 Nullifier::from(test_field_element(1001)),
1002 1_735_689_600,
1003 ),
1004 ResponseItem::new_uniqueness(
1005 "orb".into(),
1006 1,
1007 ZeroKnowledgeProof::default(),
1008 Nullifier::from(test_field_element(1001)),
1009 1_735_689_600,
1010 ),
1011 ],
1012 };
1013 let err = request.validate_response(&duplicate).unwrap_err();
1014 assert!(matches!(
1015 err,
1016 ValidationError::DuplicateCredential(ref id) if id == "orb"
1017 ));
1018 }
1019
1020 #[test]
1021 fn constraint_depth_enforced() {
1022 let deep = ConstraintExpr::All {
1024 all: vec![ConstraintNode::Expr(ConstraintExpr::Any {
1025 any: vec![ConstraintNode::Expr(ConstraintExpr::All {
1026 all: vec![ConstraintNode::Type("orb".into())],
1027 })],
1028 })],
1029 };
1030
1031 let request = ProofRequest {
1032 id: "req_2".into(),
1033 version: RequestVersion::V1,
1034 created_at: 1_735_689_600,
1035 expires_at: 1_735_689_600,
1036 rp_id: RpId::new(1),
1037 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1038 session_id: None,
1039 action: Some(test_field_element(1)),
1040 signature: test_signature(),
1041 nonce: test_nonce(),
1042 requests: vec![RequestItem {
1043 identifier: "orb".into(),
1044 issuer_schema_id: 1,
1045 signal: None,
1046 genesis_issued_at_min: None,
1047 expires_at_min: None,
1048 }],
1049 constraints: Some(deep),
1050 };
1051
1052 let response = ProofResponse {
1053 id: "req_2".into(),
1054 version: RequestVersion::V1,
1055 session_id: None,
1056 error: None,
1057 responses: vec![ResponseItem::new_uniqueness(
1058 "orb".into(),
1059 1,
1060 ZeroKnowledgeProof::default(),
1061 Nullifier::from(test_field_element(1001)),
1062 1_735_689_600,
1063 )],
1064 };
1065
1066 let err = request.validate_response(&response).unwrap_err();
1067 assert!(matches!(err, ValidationError::ConstraintTooDeep));
1068 }
1069
1070 #[test]
1071 #[allow(clippy::too_many_lines)]
1072 fn constraint_node_limit_boundary_passes() {
1073 let expr = ConstraintExpr::All {
1077 all: vec![
1078 ConstraintNode::Type("test_req_10".into()),
1079 ConstraintNode::Expr(ConstraintExpr::Any {
1080 any: vec![
1081 ConstraintNode::Type("test_req_11".into()),
1082 ConstraintNode::Type("test_req_12".into()),
1083 ConstraintNode::Type("test_req_13".into()),
1084 ConstraintNode::Type("test_req_14".into()),
1085 ],
1086 }),
1087 ConstraintNode::Expr(ConstraintExpr::Any {
1088 any: vec![
1089 ConstraintNode::Type("test_req_15".into()),
1090 ConstraintNode::Type("test_req_16".into()),
1091 ConstraintNode::Type("test_req_17".into()),
1092 ConstraintNode::Type("test_req_18".into()),
1093 ],
1094 }),
1095 ],
1096 };
1097
1098 let request = ProofRequest {
1099 id: "req_nodes_ok".into(),
1100 version: RequestVersion::V1,
1101 created_at: 1_735_689_600,
1102 expires_at: 1_735_689_600,
1103 rp_id: RpId::new(1),
1104 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1105 session_id: None,
1106 action: Some(test_field_element(5)),
1107 signature: test_signature(),
1108 nonce: test_nonce(),
1109 requests: vec![
1110 RequestItem {
1111 identifier: "test_req_10".into(),
1112 issuer_schema_id: 10,
1113 signal: None,
1114 genesis_issued_at_min: None,
1115 expires_at_min: None,
1116 },
1117 RequestItem {
1118 identifier: "test_req_11".into(),
1119 issuer_schema_id: 11,
1120 signal: None,
1121 genesis_issued_at_min: None,
1122 expires_at_min: None,
1123 },
1124 RequestItem {
1125 identifier: "test_req_12".into(),
1126 issuer_schema_id: 12,
1127 signal: None,
1128 genesis_issued_at_min: None,
1129 expires_at_min: None,
1130 },
1131 RequestItem {
1132 identifier: "test_req_13".into(),
1133 issuer_schema_id: 13,
1134 signal: None,
1135 genesis_issued_at_min: None,
1136 expires_at_min: None,
1137 },
1138 RequestItem {
1139 identifier: "test_req_14".into(),
1140 issuer_schema_id: 14,
1141 signal: None,
1142 genesis_issued_at_min: None,
1143 expires_at_min: None,
1144 },
1145 RequestItem {
1146 identifier: "test_req_15".into(),
1147 issuer_schema_id: 15,
1148 signal: None,
1149 genesis_issued_at_min: None,
1150 expires_at_min: None,
1151 },
1152 RequestItem {
1153 identifier: "test_req_16".into(),
1154 issuer_schema_id: 16,
1155 signal: None,
1156 genesis_issued_at_min: None,
1157 expires_at_min: None,
1158 },
1159 RequestItem {
1160 identifier: "test_req_17".into(),
1161 issuer_schema_id: 17,
1162 signal: None,
1163 genesis_issued_at_min: None,
1164 expires_at_min: None,
1165 },
1166 RequestItem {
1167 identifier: "test_req_18".into(),
1168 issuer_schema_id: 18,
1169 signal: None,
1170 genesis_issued_at_min: None,
1171 expires_at_min: None,
1172 },
1173 ],
1174 constraints: Some(expr),
1175 };
1176
1177 let response = ProofResponse {
1179 id: "req_nodes_ok".into(),
1180 version: RequestVersion::V1,
1181 session_id: None,
1182 error: None,
1183 responses: vec![
1184 ResponseItem::new_uniqueness(
1185 "test_req_10".into(),
1186 10,
1187 ZeroKnowledgeProof::default(),
1188 Nullifier::from(test_field_element(1010)),
1189 1_735_689_600,
1190 ),
1191 ResponseItem::new_uniqueness(
1192 "test_req_11".into(),
1193 11,
1194 ZeroKnowledgeProof::default(),
1195 Nullifier::from(test_field_element(1011)),
1196 1_735_689_600,
1197 ),
1198 ResponseItem::new_uniqueness(
1199 "test_req_15".into(),
1200 15,
1201 ZeroKnowledgeProof::default(),
1202 Nullifier::from(test_field_element(1015)),
1203 1_735_689_600,
1204 ),
1205 ],
1206 };
1207
1208 assert!(request.validate_response(&response).is_ok());
1210 }
1211
1212 #[test]
1213 #[allow(clippy::too_many_lines)]
1214 fn constraint_node_limit_exceeded_fails() {
1215 let expr = ConstraintExpr::All {
1218 all: vec![
1219 ConstraintNode::Type("t0".into()),
1220 ConstraintNode::Expr(ConstraintExpr::Any {
1221 any: vec![
1222 ConstraintNode::Type("t1".into()),
1223 ConstraintNode::Type("t2".into()),
1224 ConstraintNode::Type("t3".into()),
1225 ConstraintNode::Type("t4".into()),
1226 ],
1227 }),
1228 ConstraintNode::Expr(ConstraintExpr::Any {
1229 any: vec![
1230 ConstraintNode::Type("t5".into()),
1231 ConstraintNode::Type("t6".into()),
1232 ConstraintNode::Type("t7".into()),
1233 ConstraintNode::Type("t8".into()),
1234 ConstraintNode::Type("t9".into()),
1235 ],
1236 }),
1237 ],
1238 };
1239
1240 let request = ProofRequest {
1241 id: "req_nodes_too_many".into(),
1242 version: RequestVersion::V1,
1243 created_at: 1_735_689_600,
1244 expires_at: 1_735_689_600,
1245 rp_id: RpId::new(1),
1246 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1247 session_id: None,
1248 action: Some(test_field_element(1)),
1249 signature: test_signature(),
1250 nonce: test_nonce(),
1251 requests: vec![
1252 RequestItem {
1253 identifier: "test_req_20".into(),
1254 issuer_schema_id: 20,
1255 signal: None,
1256 genesis_issued_at_min: None,
1257 expires_at_min: None,
1258 },
1259 RequestItem {
1260 identifier: "test_req_21".into(),
1261 issuer_schema_id: 21,
1262 signal: None,
1263 genesis_issued_at_min: None,
1264 expires_at_min: None,
1265 },
1266 RequestItem {
1267 identifier: "test_req_22".into(),
1268 issuer_schema_id: 22,
1269 signal: None,
1270 genesis_issued_at_min: None,
1271 expires_at_min: None,
1272 },
1273 RequestItem {
1274 identifier: "test_req_23".into(),
1275 issuer_schema_id: 23,
1276 signal: None,
1277 genesis_issued_at_min: None,
1278 expires_at_min: None,
1279 },
1280 RequestItem {
1281 identifier: "test_req_24".into(),
1282 issuer_schema_id: 24,
1283 signal: None,
1284 genesis_issued_at_min: None,
1285 expires_at_min: None,
1286 },
1287 RequestItem {
1288 identifier: "test_req_25".into(),
1289 issuer_schema_id: 25,
1290 signal: None,
1291 genesis_issued_at_min: None,
1292 expires_at_min: None,
1293 },
1294 RequestItem {
1295 identifier: "test_req_26".into(),
1296 issuer_schema_id: 26,
1297 signal: None,
1298 genesis_issued_at_min: None,
1299 expires_at_min: None,
1300 },
1301 RequestItem {
1302 identifier: "test_req_27".into(),
1303 issuer_schema_id: 27,
1304 signal: None,
1305 genesis_issued_at_min: None,
1306 expires_at_min: None,
1307 },
1308 RequestItem {
1309 identifier: "test_req_28".into(),
1310 issuer_schema_id: 28,
1311 signal: None,
1312 genesis_issued_at_min: None,
1313 expires_at_min: None,
1314 },
1315 RequestItem {
1316 identifier: "test_req_29".into(),
1317 issuer_schema_id: 29,
1318 signal: None,
1319 genesis_issued_at_min: None,
1320 expires_at_min: None,
1321 },
1322 ],
1323 constraints: Some(expr),
1324 };
1325
1326 let response = ProofResponse {
1328 id: "req_nodes_too_many".into(),
1329 version: RequestVersion::V1,
1330 session_id: None,
1331 error: None,
1332 responses: vec![ResponseItem::new_uniqueness(
1333 "test_req_20".into(),
1334 20,
1335 ZeroKnowledgeProof::default(),
1336 Nullifier::from(test_field_element(1020)),
1337 1_735_689_600,
1338 )],
1339 };
1340
1341 let err = request.validate_response(&response).unwrap_err();
1342 assert!(matches!(err, ValidationError::ConstraintTooLarge));
1343 }
1344
1345 #[test]
1346 fn request_single_credential_parse_and_validate() {
1347 let req = ProofRequest {
1348 id: "req_18c0f7f03e7d".into(),
1349 version: RequestVersion::V1,
1350 created_at: 1_725_381_192,
1351 expires_at: 1_725_381_492,
1352 rp_id: RpId::new(1),
1353 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1354 session_id: Some(SessionId::default()),
1355 action: Some(test_field_element(1)),
1356 signature: test_signature(),
1357 nonce: test_nonce(),
1358 requests: vec![RequestItem {
1359 identifier: "test_req_1".into(),
1360 issuer_schema_id: 1,
1361 signal: Some("abcd-efgh-ijkl".into()),
1362 genesis_issued_at_min: Some(1_725_381_192),
1363 expires_at_min: None,
1364 }],
1365 constraints: None,
1366 };
1367
1368 assert_eq!(req.id, "req_18c0f7f03e7d");
1369 assert_eq!(req.requests.len(), 1);
1370
1371 let resp = ProofResponse {
1373 id: req.id.clone(),
1374 version: RequestVersion::V1,
1375 session_id: Some(SessionId::default()),
1376 error: None,
1377 responses: vec![ResponseItem::new_session(
1378 "test_req_1".into(),
1379 1,
1380 ZeroKnowledgeProof::default(),
1381 SessionNullifier::new(test_field_element(1001), test_action(1)).unwrap(),
1382 1_725_381_192,
1383 )],
1384 };
1385 assert!(req.validate_response(&resp).is_ok());
1386 }
1387
1388 #[test]
1389 fn request_multiple_credentials_all_constraint_and_missing() {
1390 let req = ProofRequest {
1391 id: "req_18c0f7f03e7d".into(),
1392 version: RequestVersion::V1,
1393 created_at: 1_725_381_192,
1394 expires_at: 1_725_381_492,
1395 rp_id: RpId::new(1),
1396 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1397 session_id: None,
1398 action: Some(test_field_element(1)),
1399 signature: test_signature(),
1400 nonce: test_nonce(),
1401 requests: vec![
1402 RequestItem {
1403 identifier: "test_req_1".into(),
1404 issuer_schema_id: 1,
1405 signal: Some("abcd-efgh-ijkl".into()),
1406 genesis_issued_at_min: Some(1_725_381_192),
1407 expires_at_min: None,
1408 },
1409 RequestItem {
1410 identifier: "test_req_2".into(),
1411 issuer_schema_id: 2,
1412 signal: Some("abcd-efgh-ijkl".into()),
1413 genesis_issued_at_min: Some(1_725_381_192),
1414 expires_at_min: None,
1415 },
1416 ],
1417 constraints: Some(ConstraintExpr::All {
1418 all: vec![
1419 ConstraintNode::Type("test_req_1".into()),
1420 ConstraintNode::Type("test_req_2".into()),
1421 ],
1422 }),
1423 };
1424
1425 let resp = ProofResponse {
1427 id: req.id.clone(),
1428 version: RequestVersion::V1,
1429 session_id: None,
1430 error: None,
1431 responses: vec![ResponseItem::new_uniqueness(
1432 "test_req_2".into(),
1433 2,
1434 ZeroKnowledgeProof::default(),
1435 Nullifier::from(test_field_element(1001)),
1436 1_725_381_192,
1437 )],
1438 };
1439
1440 let err = req.validate_response(&resp).unwrap_err();
1441 assert!(matches!(err, ValidationError::ConstraintNotSatisfied));
1442 }
1443
1444 #[test]
1445 fn request_more_complex_constraints_nested_success() {
1446 let req = ProofRequest {
1447 id: "req_18c0f7f03e7d".into(),
1448 version: RequestVersion::V1,
1449 created_at: 1_725_381_192,
1450 expires_at: 1_725_381_492,
1451 rp_id: RpId::new(1),
1452 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1453 session_id: None,
1454 action: Some(test_field_element(1)),
1455 signature: test_signature(),
1456 nonce: test_nonce(),
1457 requests: vec![
1458 RequestItem {
1459 identifier: "test_req_1".into(),
1460 issuer_schema_id: 1,
1461 signal: Some("abcd-efgh-ijkl".into()),
1462 genesis_issued_at_min: None,
1463 expires_at_min: None,
1464 },
1465 RequestItem {
1466 identifier: "test_req_2".into(),
1467 issuer_schema_id: 2,
1468 signal: Some("mnop-qrst-uvwx".into()),
1469 genesis_issued_at_min: None,
1470 expires_at_min: None,
1471 },
1472 RequestItem {
1473 identifier: "test_req_3".into(),
1474 issuer_schema_id: 3,
1475 signal: Some("abcd-efgh-ijkl".into()),
1476 genesis_issued_at_min: None,
1477 expires_at_min: None,
1478 },
1479 ],
1480 constraints: Some(ConstraintExpr::All {
1481 all: vec![
1482 ConstraintNode::Type("test_req_3".into()),
1483 ConstraintNode::Expr(ConstraintExpr::Any {
1484 any: vec![
1485 ConstraintNode::Type("test_req_1".into()),
1486 ConstraintNode::Type("test_req_2".into()),
1487 ],
1488 }),
1489 ],
1490 }),
1491 };
1492
1493 let resp = ProofResponse {
1495 id: req.id.clone(),
1496 version: RequestVersion::V1,
1497 session_id: None,
1498 error: None,
1499 responses: vec![
1500 ResponseItem::new_uniqueness(
1501 "test_req_3".into(),
1502 3,
1503 ZeroKnowledgeProof::default(),
1504 Nullifier::from(test_field_element(1001)),
1505 1_725_381_192,
1506 ),
1507 ResponseItem::new_uniqueness(
1508 "test_req_1".into(),
1509 1,
1510 ZeroKnowledgeProof::default(),
1511 Nullifier::from(test_field_element(1002)),
1512 1_725_381_192,
1513 ),
1514 ],
1515 };
1516
1517 assert!(req.validate_response(&resp).is_ok());
1518 }
1519
1520 #[test]
1521 fn request_validate_response_with_enumerate() {
1522 let req = ProofRequest {
1523 id: "req_enum".into(),
1524 version: RequestVersion::V1,
1525 created_at: 1_725_381_192,
1526 expires_at: 1_725_381_492,
1527 rp_id: RpId::new(1),
1528 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1529 session_id: None,
1530 action: Some(test_field_element(1)),
1531 signature: test_signature(),
1532 nonce: test_nonce(),
1533 requests: vec![
1534 RequestItem {
1535 identifier: "passport".into(),
1536 issuer_schema_id: 2,
1537 signal: None,
1538 genesis_issued_at_min: None,
1539 expires_at_min: None,
1540 },
1541 RequestItem {
1542 identifier: "national_id".into(),
1543 issuer_schema_id: 3,
1544 signal: None,
1545 genesis_issued_at_min: None,
1546 expires_at_min: None,
1547 },
1548 ],
1549 constraints: Some(ConstraintExpr::Enumerate {
1550 enumerate: vec![
1551 ConstraintNode::Type("passport".into()),
1552 ConstraintNode::Type("national_id".into()),
1553 ],
1554 }),
1555 };
1556
1557 let ok_resp = ProofResponse {
1559 id: req.id.clone(),
1560 version: RequestVersion::V1,
1561 session_id: None,
1562 error: None,
1563 responses: vec![ResponseItem::new_uniqueness(
1564 "passport".into(),
1565 2,
1566 ZeroKnowledgeProof::default(),
1567 Nullifier::from(test_field_element(2002)),
1568 1_725_381_192,
1569 )],
1570 };
1571 assert!(req.validate_response(&ok_resp).is_ok());
1572
1573 let fail_resp = ProofResponse {
1575 id: req.id.clone(),
1576 version: RequestVersion::V1,
1577 session_id: None,
1578 error: None,
1579 responses: vec![],
1580 };
1581 let err = req.validate_response(&fail_resp).unwrap_err();
1582 assert!(matches!(err, ValidationError::ConstraintNotSatisfied));
1583 }
1584
1585 #[test]
1586 fn request_json_parse() {
1587 let with_signal = r#"{
1589 "id": "req_abc123",
1590 "version": 1,
1591 "created_at": 1725381192,
1592 "expires_at": 1725381492,
1593 "rp_id": "rp_0000000000000001",
1594 "oprf_key_id": "0x1",
1595 "session_id": null,
1596 "action": "0x000000000000000000000000000000000000000000000000000000000000002a",
1597 "signature": "0xa1fd06f0d8ceb541f6096fe2e865063eac1ff085c9d2bac2eedcc9ed03804bfc18d956b38c5ac3a8f7e71fde43deff3bda254d369c699f3c7a3f8e6b8477a5f51c",
1598 "nonce": "0x0000000000000000000000000000000000000000000000000000000000000001",
1599 "proof_requests": [
1600 {
1601 "identifier": "orb",
1602 "issuer_schema_id": 1,
1603 "signal": "0xdeadbeef",
1604 "genesis_issued_at_min": 1725381192,
1605 "expires_at_min": 1725381492
1606 }
1607 ]
1608}"#;
1609
1610 let req = ProofRequest::from_json(with_signal).expect("parse with signal");
1611 assert_eq!(req.id, "req_abc123");
1612 assert_eq!(req.requests.len(), 1);
1613 assert_eq!(req.requests[0].signal, Some(b"\xde\xad\xbe\xef".to_vec()));
1614 assert_eq!(req.requests[0].genesis_issued_at_min, Some(1_725_381_192));
1615 assert_eq!(req.requests[0].expires_at_min, Some(1_725_381_492));
1616
1617 let without_signal = r#"{
1618 "id": "req_abc123",
1619 "version": 1,
1620 "created_at": 1725381192,
1621 "expires_at": 1725381492,
1622 "rp_id": "rp_0000000000000001",
1623 "oprf_key_id": "0x1",
1624 "session_id": null,
1625 "action": "0x000000000000000000000000000000000000000000000000000000000000002a",
1626 "signature": "0xa1fd06f0d8ceb541f6096fe2e865063eac1ff085c9d2bac2eedcc9ed03804bfc18d956b38c5ac3a8f7e71fde43deff3bda254d369c699f3c7a3f8e6b8477a5f51c",
1627 "nonce": "0x0000000000000000000000000000000000000000000000000000000000000001",
1628 "proof_requests": [
1629 {
1630 "identifier": "orb",
1631 "issuer_schema_id": 1
1632 }
1633 ]
1634}"#;
1635
1636 let req = ProofRequest::from_json(without_signal).expect("parse without signal");
1637 assert!(req.requests[0].signal.is_none());
1638 assert_eq!(req.requests[0].signal_hash(), FieldElement::ZERO);
1639 }
1640
1641 #[test]
1642 fn response_json_parse() {
1643 let ok_json = r#"{
1645 "id": "req_18c0f7f03e7d",
1646 "version": 1,
1647 "responses": [
1648 {
1649 "identifier": "orb",
1650 "issuer_schema_id": 100,
1651 "proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
1652 "nullifier": "nil_00000000000000000000000000000000000000000000000000000000000003e9",
1653 "expires_at_min": 1725381192
1654 }
1655 ]
1656}"#;
1657
1658 let ok = ProofResponse::from_json(ok_json).unwrap();
1659 assert_eq!(ok.successful_credentials(), vec![100]);
1660 assert!(ok.responses[0].is_uniqueness());
1661
1662 let canonical_session_nullifier = serde_json::to_string(
1664 &SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap(),
1665 )
1666 .unwrap();
1667 let sess_json_canonical = format!(
1668 r#"{{
1669 "id": "req_18c0f7f03e7d",
1670 "version": 1,
1671 "session_id": "session_00000000000000000000000000000000000000000000000000000000000003ea0100000000000000000000000000000000000000000000000000000000000001",
1672 "responses": [
1673 {{
1674 "identifier": "orb",
1675 "issuer_schema_id": 100,
1676 "proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
1677 "session_nullifier": {canonical_session_nullifier},
1678 "expires_at_min": 1725381192
1679 }}
1680 ]
1681}}"#
1682 );
1683 let sess_canonical = ProofResponse::from_json(&sess_json_canonical).unwrap();
1684 assert_eq!(sess_canonical.successful_credentials(), vec![100]);
1685 assert!(sess_canonical.responses[0].is_session());
1686 assert_eq!(
1687 sess_canonical.session_id.unwrap().oprf_seed.to_u256(),
1688 uint!(0x0100000000000000000000000000000000000000000000000000000000000001_U256)
1689 );
1690 }
1691 #[test]
1694 fn request_rejects_duplicate_issuer_schema_ids_on_parse() {
1695 let req = ProofRequest {
1696 id: "req_dup".into(),
1697 version: RequestVersion::V1,
1698 created_at: 1_725_381_192,
1699 expires_at: 1_725_381_492,
1700 rp_id: RpId::new(1),
1701 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1702 session_id: None,
1703 action: Some(test_field_element(5)),
1704 signature: test_signature(),
1705 nonce: test_nonce(),
1706 requests: vec![
1707 RequestItem {
1708 identifier: "test_req_1".into(),
1709 issuer_schema_id: 1,
1710 signal: None,
1711 genesis_issued_at_min: None,
1712 expires_at_min: None,
1713 },
1714 RequestItem {
1715 identifier: "test_req_2".into(),
1716 issuer_schema_id: 1, signal: None,
1718 genesis_issued_at_min: None,
1719 expires_at_min: None,
1720 },
1721 ],
1722 constraints: None,
1723 };
1724
1725 let json = req.to_json().unwrap();
1727 let err = ProofRequest::from_json(&json).unwrap_err();
1728 let msg = err.to_string();
1729 assert!(
1730 msg.contains("duplicate issuer schema id"),
1731 "Expected error message to contain 'duplicate issuer schema id', got: {msg}"
1732 );
1733 }
1734
1735 #[test]
1736 fn response_with_error_has_empty_responses_and_fails_validation() {
1737 let request = ProofRequest {
1738 id: "req_error".into(),
1739 version: RequestVersion::V1,
1740 created_at: 1_735_689_600,
1741 expires_at: 1_735_689_600,
1742 rp_id: RpId::new(1),
1743 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1744 session_id: None,
1745 action: Some(FieldElement::ZERO),
1746 signature: test_signature(),
1747 nonce: test_nonce(),
1748 requests: vec![RequestItem {
1749 identifier: "orb".into(),
1750 issuer_schema_id: 1,
1751 signal: None,
1752 genesis_issued_at_min: None,
1753 expires_at_min: None,
1754 }],
1755 constraints: None,
1756 };
1757
1758 let error_response = ProofResponse {
1760 id: "req_error".into(),
1761 version: RequestVersion::V1,
1762 session_id: None,
1763 error: Some("credential_not_available".into()),
1764 responses: vec![], };
1766
1767 let err = request.validate_response(&error_response).unwrap_err();
1769 assert!(matches!(err, ValidationError::ProofGenerationFailed(_)));
1770 if let ValidationError::ProofGenerationFailed(msg) = err {
1771 assert_eq!(msg, "credential_not_available");
1772 }
1773
1774 assert_eq!(error_response.successful_credentials(), Vec::<u64>::new());
1776
1777 let expr = ConstraintExpr::All {
1779 all: vec![ConstraintNode::Type("orb".into())],
1780 };
1781 assert!(!error_response.constraints_satisfied(&expr));
1782 }
1783
1784 #[test]
1785 fn response_error_json_parse() {
1786 let error_json = r#"{
1788 "id": "req_error",
1789 "version": 1,
1790 "error": "credential_not_available",
1791 "responses": []
1792}"#;
1793
1794 let error_resp = ProofResponse::from_json(error_json).unwrap();
1795 assert_eq!(error_resp.error, Some("credential_not_available".into()));
1796 assert_eq!(error_resp.responses.len(), 0);
1797 assert_eq!(error_resp.successful_credentials(), Vec::<u64>::new());
1798 }
1799
1800 #[test]
1801 fn credentials_to_prove_none_constraints_requires_all_and_drops_if_missing() {
1802 let req = ProofRequest {
1803 id: "req".into(),
1804 version: RequestVersion::V1,
1805 created_at: 1_735_689_600,
1806 expires_at: 1_735_689_600, rp_id: RpId::new(1),
1808 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1809 session_id: None,
1810 action: Some(test_field_element(5)),
1811 signature: test_signature(),
1812 nonce: test_nonce(),
1813 requests: vec![
1814 RequestItem {
1815 identifier: "orb".into(),
1816 issuer_schema_id: 100,
1817 signal: None,
1818 genesis_issued_at_min: None,
1819 expires_at_min: None,
1820 },
1821 RequestItem {
1822 identifier: "passport".into(),
1823 issuer_schema_id: 101,
1824 signal: None,
1825 genesis_issued_at_min: None,
1826 expires_at_min: None,
1827 },
1828 ],
1829 constraints: None,
1830 };
1831
1832 let available_ok: HashSet<u64> = [100, 101].into_iter().collect();
1833 let sel_ok = req.credentials_to_prove(&available_ok).unwrap();
1834 assert_eq!(sel_ok.len(), 2);
1835 assert_eq!(sel_ok[0].issuer_schema_id, 100);
1836 assert_eq!(sel_ok[1].issuer_schema_id, 101);
1837
1838 let available_missing: HashSet<u64> = std::iter::once(100).collect();
1839 assert!(req.credentials_to_prove(&available_missing).is_none());
1840 }
1841
1842 #[test]
1843 fn credentials_to_prove_with_constraints_all_and_any() {
1844 let orb_id = 100;
1846 let passport_id = 101;
1847 let national_id = 102;
1848
1849 let req = ProofRequest {
1850 id: "req".into(),
1851 version: RequestVersion::V1,
1852 created_at: 1_735_689_600,
1853 expires_at: 1_735_689_600, rp_id: RpId::new(1),
1855 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1856 session_id: None,
1857 action: Some(test_field_element(1)),
1858 signature: test_signature(),
1859 nonce: test_nonce(),
1860 requests: vec![
1861 RequestItem {
1862 identifier: "orb".into(),
1863 issuer_schema_id: orb_id,
1864 signal: None,
1865 genesis_issued_at_min: None,
1866 expires_at_min: None,
1867 },
1868 RequestItem {
1869 identifier: "passport".into(),
1870 issuer_schema_id: passport_id,
1871 signal: None,
1872 genesis_issued_at_min: None,
1873 expires_at_min: None,
1874 },
1875 RequestItem {
1876 identifier: "national_id".into(),
1877 issuer_schema_id: national_id,
1878 signal: None,
1879 genesis_issued_at_min: None,
1880 expires_at_min: None,
1881 },
1882 ],
1883 constraints: Some(ConstraintExpr::All {
1884 all: vec![
1885 ConstraintNode::Type("orb".into()),
1886 ConstraintNode::Expr(ConstraintExpr::Any {
1887 any: vec![
1888 ConstraintNode::Type("passport".into()),
1889 ConstraintNode::Type("national_id".into()),
1890 ],
1891 }),
1892 ],
1893 }),
1894 };
1895
1896 let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
1898 let sel1 = req.credentials_to_prove(&available1).unwrap();
1899 assert_eq!(sel1.len(), 2);
1900 assert_eq!(sel1[0].issuer_schema_id, orb_id);
1901 assert_eq!(sel1[1].issuer_schema_id, passport_id);
1902
1903 let available2: HashSet<u64> = [orb_id, national_id].into_iter().collect();
1905 let sel2 = req.credentials_to_prove(&available2).unwrap();
1906 assert_eq!(sel2.len(), 2);
1907 assert_eq!(sel2[0].issuer_schema_id, orb_id);
1908 assert_eq!(sel2[1].issuer_schema_id, national_id);
1909
1910 let available3: HashSet<u64> = std::iter::once(passport_id).collect();
1912 assert!(req.credentials_to_prove(&available3).is_none());
1913 }
1914
1915 #[test]
1916 fn credentials_to_prove_with_constraints_enumerate() {
1917 let orb_id = 100;
1918 let passport_id = 101;
1919 let national_id = 102;
1920
1921 let req = ProofRequest {
1922 id: "req".into(),
1923 version: RequestVersion::V1,
1924 created_at: 1_735_689_600,
1925 expires_at: 1_735_689_600,
1926 rp_id: RpId::new(1),
1927 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1928 session_id: None,
1929 action: Some(test_field_element(1)),
1930 signature: test_signature(),
1931 nonce: test_nonce(),
1932 requests: vec![
1933 RequestItem {
1934 identifier: "orb".into(),
1935 issuer_schema_id: orb_id,
1936 signal: None,
1937 genesis_issued_at_min: None,
1938 expires_at_min: None,
1939 },
1940 RequestItem {
1941 identifier: "passport".into(),
1942 issuer_schema_id: passport_id,
1943 signal: None,
1944 genesis_issued_at_min: None,
1945 expires_at_min: None,
1946 },
1947 RequestItem {
1948 identifier: "national_id".into(),
1949 issuer_schema_id: national_id,
1950 signal: None,
1951 genesis_issued_at_min: None,
1952 expires_at_min: None,
1953 },
1954 ],
1955 constraints: Some(ConstraintExpr::Enumerate {
1956 enumerate: vec![
1957 ConstraintNode::Type("passport".into()),
1958 ConstraintNode::Type("national_id".into()),
1959 ],
1960 }),
1961 };
1962
1963 let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
1965 let sel1 = req.credentials_to_prove(&available1).unwrap();
1966 assert_eq!(sel1.len(), 1);
1967 assert_eq!(sel1[0].issuer_schema_id, passport_id);
1968
1969 let available2: HashSet<u64> = [orb_id, passport_id, national_id].into_iter().collect();
1971 let sel2 = req.credentials_to_prove(&available2).unwrap();
1972 assert_eq!(sel2.len(), 2);
1973 assert_eq!(sel2[0].issuer_schema_id, passport_id);
1974 assert_eq!(sel2[1].issuer_schema_id, national_id);
1975
1976 let available3: HashSet<u64> = std::iter::once(orb_id).collect();
1978 assert!(req.credentials_to_prove(&available3).is_none());
1979 }
1980
1981 #[test]
1982 fn credentials_to_prove_with_constraints_all_and_enumerate() {
1983 let orb_id = 100;
1984 let passport_id = 101;
1985 let national_id = 102;
1986
1987 let req = ProofRequest {
1988 id: "req".into(),
1989 version: RequestVersion::V1,
1990 created_at: 1_735_689_600,
1991 expires_at: 1_735_689_600,
1992 rp_id: RpId::new(1),
1993 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1994 session_id: None,
1995 action: Some(test_field_element(1)),
1996 signature: test_signature(),
1997 nonce: test_nonce(),
1998 requests: vec![
1999 RequestItem {
2000 identifier: "orb".into(),
2001 issuer_schema_id: orb_id,
2002 signal: None,
2003 genesis_issued_at_min: None,
2004 expires_at_min: None,
2005 },
2006 RequestItem {
2007 identifier: "passport".into(),
2008 issuer_schema_id: passport_id,
2009 signal: None,
2010 genesis_issued_at_min: None,
2011 expires_at_min: None,
2012 },
2013 RequestItem {
2014 identifier: "national_id".into(),
2015 issuer_schema_id: national_id,
2016 signal: None,
2017 genesis_issued_at_min: None,
2018 expires_at_min: None,
2019 },
2020 ],
2021 constraints: Some(ConstraintExpr::All {
2022 all: vec![
2023 ConstraintNode::Type("orb".into()),
2024 ConstraintNode::Expr(ConstraintExpr::Enumerate {
2025 enumerate: vec![
2026 ConstraintNode::Type("passport".into()),
2027 ConstraintNode::Type("national_id".into()),
2028 ],
2029 }),
2030 ],
2031 }),
2032 };
2033
2034 let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
2036 let sel1 = req.credentials_to_prove(&available1).unwrap();
2037 assert_eq!(sel1.len(), 2);
2038 assert_eq!(sel1[0].issuer_schema_id, orb_id);
2039 assert_eq!(sel1[1].issuer_schema_id, passport_id);
2040
2041 let available2: HashSet<u64> = [orb_id, passport_id, national_id].into_iter().collect();
2043 let sel2 = req.credentials_to_prove(&available2).unwrap();
2044 assert_eq!(sel2.len(), 3);
2045 assert_eq!(sel2[0].issuer_schema_id, orb_id);
2046 assert_eq!(sel2[1].issuer_schema_id, passport_id);
2047 assert_eq!(sel2[2].issuer_schema_id, national_id);
2048
2049 let available3: HashSet<u64> = std::iter::once(orb_id).collect();
2051 assert!(req.credentials_to_prove(&available3).is_none());
2052 }
2053
2054 #[test]
2055 fn request_item_effective_expires_at_min_defaults_to_created_at() {
2056 let request_created_at = 1_735_689_600; let custom_expires_at = 1_735_862_400; let item_with_none = RequestItem {
2061 identifier: "test".into(),
2062 issuer_schema_id: 100,
2063 signal: None,
2064 genesis_issued_at_min: None,
2065 expires_at_min: None,
2066 };
2067 assert_eq!(
2068 item_with_none.effective_expires_at_min(request_created_at),
2069 request_created_at,
2070 "When expires_at_min is None, should default to request created_at"
2071 );
2072
2073 let item_with_custom = RequestItem {
2075 identifier: "test".into(),
2076 issuer_schema_id: 100,
2077 signal: None,
2078 genesis_issued_at_min: None,
2079 expires_at_min: Some(custom_expires_at),
2080 };
2081 assert_eq!(
2082 item_with_custom.effective_expires_at_min(request_created_at),
2083 custom_expires_at,
2084 "When expires_at_min is Some, should use that explicit value"
2085 );
2086 }
2087
2088 #[test]
2089 fn validate_response_checks_expires_at_min_matches() {
2090 let request_created_at = 1_735_689_600; let custom_expires_at = 1_735_862_400; let request = ProofRequest {
2096 id: "req_expires_test".into(),
2097 version: RequestVersion::V1,
2098 created_at: request_created_at,
2099 expires_at: request_created_at + 300,
2100 rp_id: RpId::new(1),
2101 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2102 session_id: None,
2103 action: Some(test_field_element(1)),
2104 signature: test_signature(),
2105 nonce: test_nonce(),
2106 requests: vec![
2107 RequestItem {
2108 identifier: "orb".into(),
2109 issuer_schema_id: 100,
2110 signal: None,
2111 genesis_issued_at_min: None,
2112 expires_at_min: None, },
2114 RequestItem {
2115 identifier: "document".into(),
2116 issuer_schema_id: 101,
2117 signal: None,
2118 genesis_issued_at_min: None,
2119 expires_at_min: Some(custom_expires_at), },
2121 ],
2122 constraints: None,
2123 };
2124
2125 let valid_response = ProofResponse {
2127 id: "req_expires_test".into(),
2128 version: RequestVersion::V1,
2129 session_id: None,
2130 error: None,
2131 responses: vec![
2132 ResponseItem::new_uniqueness(
2133 "orb".into(),
2134 100,
2135 ZeroKnowledgeProof::default(),
2136 Nullifier::from(test_field_element(1001)),
2137 request_created_at, ),
2139 ResponseItem::new_uniqueness(
2140 "document".into(),
2141 101,
2142 ZeroKnowledgeProof::default(),
2143 Nullifier::from(test_field_element(1002)),
2144 custom_expires_at, ),
2146 ],
2147 };
2148 assert!(request.validate_response(&valid_response).is_ok());
2149
2150 let invalid_response_1 = ProofResponse {
2152 id: "req_expires_test".into(),
2153 version: RequestVersion::V1,
2154 session_id: None,
2155 error: None,
2156 responses: vec![
2157 ResponseItem::new_uniqueness(
2158 "orb".into(),
2159 100,
2160 ZeroKnowledgeProof::default(),
2161 Nullifier::from(test_field_element(1001)),
2162 custom_expires_at, ),
2164 ResponseItem::new_uniqueness(
2165 "document".into(),
2166 101,
2167 ZeroKnowledgeProof::default(),
2168 Nullifier::from(test_field_element(1002)),
2169 custom_expires_at,
2170 ),
2171 ],
2172 };
2173 let err1 = request.validate_response(&invalid_response_1).unwrap_err();
2174 assert!(matches!(
2175 err1,
2176 ValidationError::ExpiresAtMinMismatch(_, _, _)
2177 ));
2178 if let ValidationError::ExpiresAtMinMismatch(identifier, expected, got) = err1 {
2179 assert_eq!(identifier, "orb");
2180 assert_eq!(expected, request_created_at);
2181 assert_eq!(got, custom_expires_at);
2182 }
2183
2184 let invalid_response_2 = ProofResponse {
2186 id: "req_expires_test".into(),
2187 version: RequestVersion::V1,
2188 session_id: None,
2189 error: None,
2190 responses: vec![
2191 ResponseItem::new_uniqueness(
2192 "orb".into(),
2193 100,
2194 ZeroKnowledgeProof::default(),
2195 Nullifier::from(test_field_element(1001)),
2196 request_created_at,
2197 ),
2198 ResponseItem::new_uniqueness(
2199 "document".into(),
2200 101,
2201 ZeroKnowledgeProof::default(),
2202 Nullifier::from(test_field_element(1002)),
2203 request_created_at, ),
2205 ],
2206 };
2207 let err2 = request.validate_response(&invalid_response_2).unwrap_err();
2208 assert!(matches!(
2209 err2,
2210 ValidationError::ExpiresAtMinMismatch(_, _, _)
2211 ));
2212 if let ValidationError::ExpiresAtMinMismatch(identifier, expected, got) = err2 {
2213 assert_eq!(identifier, "document");
2214 assert_eq!(expected, custom_expires_at);
2215 assert_eq!(got, request_created_at);
2216 }
2217 }
2218
2219 #[test]
2220 fn test_validate_response_requires_session_id_in_response() {
2221 let request = ProofRequest {
2223 id: "req_session".into(),
2224 version: RequestVersion::V1,
2225 created_at: 1_735_689_600,
2226 expires_at: 1_735_689_900,
2227 rp_id: RpId::new(1),
2228 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2229 session_id: Some(SessionId::default()), action: Some(test_field_element(42)),
2231 signature: test_signature(),
2232 nonce: test_nonce(),
2233 requests: vec![RequestItem {
2234 identifier: "orb".into(),
2235 issuer_schema_id: 1,
2236 signal: None,
2237 genesis_issued_at_min: None,
2238 expires_at_min: None,
2239 }],
2240 constraints: None,
2241 };
2242
2243 let response_missing_session_id = ProofResponse {
2245 id: "req_session".into(),
2246 version: RequestVersion::V1,
2247 session_id: None, error: None,
2249 responses: vec![ResponseItem::new_session(
2250 "orb".into(),
2251 1,
2252 ZeroKnowledgeProof::default(),
2253 SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap(),
2254 1_735_689_600,
2255 )],
2256 };
2257
2258 let err = request
2259 .validate_response(&response_missing_session_id)
2260 .unwrap_err();
2261 assert!(matches!(err, ValidationError::SessionIdMismatch));
2262 }
2263
2264 #[test]
2265 fn test_validate_response_requires_session_nullifier_for_session_proof() {
2266 let request = ProofRequest {
2268 id: "req_session".into(),
2269 version: RequestVersion::V1,
2270 created_at: 1_735_689_600,
2271 expires_at: 1_735_689_900,
2272 rp_id: RpId::new(1),
2273 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2274 session_id: Some(SessionId::default()), action: Some(test_field_element(42)),
2276 signature: test_signature(),
2277 nonce: test_nonce(),
2278 requests: vec![RequestItem {
2279 identifier: "orb".into(),
2280 issuer_schema_id: 1,
2281 signal: None,
2282 genesis_issued_at_min: None,
2283 expires_at_min: None,
2284 }],
2285 constraints: None,
2286 };
2287
2288 let response_wrong_nullifier_type = ProofResponse {
2290 id: "req_session".into(),
2291 version: RequestVersion::V1,
2292 session_id: Some(SessionId::default()),
2293 error: None,
2294 responses: vec![ResponseItem::new_uniqueness(
2295 "orb".into(),
2296 1,
2297 ZeroKnowledgeProof::default(),
2298 Nullifier::from(test_field_element(1001)), 1_735_689_600,
2300 )],
2301 };
2302
2303 let err = request
2304 .validate_response(&response_wrong_nullifier_type)
2305 .unwrap_err();
2306 assert!(matches!(
2307 err,
2308 ValidationError::MissingSessionNullifier(ref id) if id == "orb"
2309 ));
2310 }
2311
2312 #[test]
2313 fn test_validate_response_requires_nullifier_for_uniqueness_proof() {
2314 let request = ProofRequest {
2316 id: "req_uniqueness".into(),
2317 version: RequestVersion::V1,
2318 created_at: 1_735_689_600,
2319 expires_at: 1_735_689_900,
2320 rp_id: RpId::new(1),
2321 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2322 session_id: None, action: Some(test_field_element(42)),
2324 signature: test_signature(),
2325 nonce: test_nonce(),
2326 requests: vec![RequestItem {
2327 identifier: "orb".into(),
2328 issuer_schema_id: 1,
2329 signal: None,
2330 genesis_issued_at_min: None,
2331 expires_at_min: None,
2332 }],
2333 constraints: None,
2334 };
2335
2336 let response_wrong_nullifier_type = ProofResponse {
2338 id: "req_uniqueness".into(),
2339 version: RequestVersion::V1,
2340 session_id: None,
2341 error: None,
2342 responses: vec![ResponseItem::new_session(
2343 "orb".into(),
2344 1,
2345 ZeroKnowledgeProof::default(),
2346 SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap(), 1_735_689_600,
2348 )],
2349 };
2350
2351 let err = request
2352 .validate_response(&response_wrong_nullifier_type)
2353 .unwrap_err();
2354 assert!(matches!(
2355 err,
2356 ValidationError::MissingNullifier(ref id) if id == "orb"
2357 ));
2358 }
2359}