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 #[test]
706 fn constraints_all_any_nested() {
707 let response = ProofResponse {
709 id: "req_123".into(),
710 version: RequestVersion::V1,
711 session_id: None,
712 error: None,
713 responses: vec![
714 ResponseItem::new_uniqueness(
715 "test_req_1".into(),
716 1,
717 ZeroKnowledgeProof::default(),
718 test_field_element(1001).into(),
719 1_735_689_600,
720 ),
721 ResponseItem::new_uniqueness(
722 "test_req_2".into(),
723 2,
724 ZeroKnowledgeProof::default(),
725 test_field_element(1002).into(),
726 1_735_689_600,
727 ),
728 ],
729 };
730
731 let expr = ConstraintExpr::All {
733 all: vec![
734 ConstraintNode::Type("test_req_1".into()),
735 ConstraintNode::Expr(ConstraintExpr::Any {
736 any: vec![
737 ConstraintNode::Type("test_req_2".into()),
738 ConstraintNode::Type("test_req_4".into()),
739 ],
740 }),
741 ],
742 };
743
744 assert!(response.constraints_satisfied(&expr));
745
746 let fail_expr = ConstraintExpr::All {
748 all: vec![
749 ConstraintNode::Type("test_req_1".into()),
750 ConstraintNode::Type("test_req_3".into()),
751 ],
752 };
753 assert!(!response.constraints_satisfied(&fail_expr));
754 }
755
756 #[test]
757 fn constraints_enumerate_partial_and_empty() {
758 let response = ProofResponse {
760 id: "req_123".into(),
761 version: RequestVersion::V1,
762 session_id: None,
763 error: None,
764 responses: vec![
765 ResponseItem::new_uniqueness(
766 "orb".into(),
767 1,
768 ZeroKnowledgeProof::default(),
769 Nullifier::from(test_field_element(1001)),
770 1_735_689_600,
771 ),
772 ResponseItem::new_uniqueness(
773 "passport".into(),
774 2,
775 ZeroKnowledgeProof::default(),
776 Nullifier::from(test_field_element(1002)),
777 1_735_689_600,
778 ),
779 ],
780 };
781
782 let expr = ConstraintExpr::Enumerate {
784 enumerate: vec![
785 ConstraintNode::Type("passport".into()),
786 ConstraintNode::Type("national_id".into()),
787 ],
788 };
789 assert!(response.constraints_satisfied(&expr));
790
791 let fail_expr = ConstraintExpr::Enumerate {
793 enumerate: vec![
794 ConstraintNode::Type("national_id".into()),
795 ConstraintNode::Type("document".into()),
796 ],
797 };
798 assert!(!response.constraints_satisfied(&fail_expr));
799 }
800
801 #[test]
802 fn test_digest_hash() {
803 let request = ProofRequest {
804 id: "test_request".into(),
805 version: RequestVersion::V1,
806 created_at: 1_700_000_000,
807 expires_at: 1_700_100_000,
808 rp_id: RpId::new(1),
809 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
810 session_id: None,
811 action: Some(FieldElement::ZERO),
812 signature: test_signature(),
813 nonce: test_nonce(),
814 requests: vec![RequestItem {
815 identifier: "orb".into(),
816 issuer_schema_id: 1,
817 signal: Some("test_signal".into()),
818 genesis_issued_at_min: None,
819 expires_at_min: None,
820 }],
821 constraints: None,
822 };
823
824 let digest1 = request.digest_hash().unwrap();
825 assert_eq!(digest1.len(), 32);
827
828 let digest2 = request.digest_hash().unwrap();
830 assert_eq!(digest1, digest2);
831
832 let request2 = ProofRequest {
834 nonce: test_field_element(3),
835 ..request
836 };
837 let digest3 = request2.digest_hash().unwrap();
838 assert_ne!(digest1, digest3);
839 }
840
841 #[test]
842 fn proof_request_signature_serializes_as_hex_string() {
843 let request = ProofRequest {
844 id: "test".into(),
845 version: RequestVersion::V1,
846 created_at: 1_700_000_000,
847 expires_at: 1_700_100_000,
848 rp_id: RpId::new(1),
849 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
850 session_id: None,
851 action: None,
852 signature: test_signature(),
853 nonce: test_nonce(),
854 requests: vec![RequestItem {
855 identifier: "orb".into(),
856 issuer_schema_id: 1,
857 signal: None,
858 genesis_issued_at_min: None,
859 expires_at_min: None,
860 }],
861 constraints: None,
862 };
863
864 let json = request.to_json().unwrap();
865 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
866 let sig = value["signature"]
867 .as_str()
868 .expect("signature should be a string");
869 assert!(sig.starts_with("0x"));
870 assert_eq!(sig.len(), 132);
871
872 let roundtripped = ProofRequest::from_json(&json).unwrap();
873 assert_eq!(roundtripped.signature, request.signature);
874 }
875
876 #[test]
877 fn request_validate_response_none_constraints_means_all() {
878 let request = ProofRequest {
879 id: "req_1".into(),
880 version: RequestVersion::V1,
881 created_at: 1_735_689_600,
882 expires_at: 1_735_689_600, rp_id: RpId::new(1),
884 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
885 session_id: None,
886 action: Some(FieldElement::ZERO),
887 signature: test_signature(),
888 nonce: test_nonce(),
889 requests: vec![
890 RequestItem {
891 identifier: "orb".into(),
892 issuer_schema_id: 1,
893 signal: None,
894 genesis_issued_at_min: None,
895 expires_at_min: None,
896 },
897 RequestItem {
898 identifier: "document".into(),
899 issuer_schema_id: 2,
900 signal: None,
901 genesis_issued_at_min: None,
902 expires_at_min: None,
903 },
904 ],
905 constraints: None,
906 };
907
908 let ok = ProofResponse {
909 id: "req_1".into(),
910 version: RequestVersion::V1,
911 session_id: None,
912 error: None,
913 responses: vec![
914 ResponseItem::new_uniqueness(
915 "orb".into(),
916 1,
917 ZeroKnowledgeProof::default(),
918 Nullifier::from(test_field_element(1001)),
919 1_735_689_600,
920 ),
921 ResponseItem::new_uniqueness(
922 "document".into(),
923 2,
924 ZeroKnowledgeProof::default(),
925 Nullifier::from(test_field_element(1002)),
926 1_735_689_600,
927 ),
928 ],
929 };
930 assert!(request.validate_response(&ok).is_ok());
931
932 let missing = ProofResponse {
933 id: "req_1".into(),
934 version: RequestVersion::V1,
935 session_id: None,
936 error: None,
937 responses: vec![ResponseItem::new_uniqueness(
938 "orb".into(),
939 1,
940 ZeroKnowledgeProof::default(),
941 Nullifier::from(test_field_element(1001)),
942 1_735_689_600,
943 )],
944 };
945 let err = request.validate_response(&missing).unwrap_err();
946 assert!(matches!(err, ValidationError::MissingCredential(_)));
947
948 let unexpected = ProofResponse {
949 id: "req_1".into(),
950 version: RequestVersion::V1,
951 session_id: None,
952 error: None,
953 responses: vec![
954 ResponseItem::new_uniqueness(
955 "orb".into(),
956 1,
957 ZeroKnowledgeProof::default(),
958 Nullifier::from(test_field_element(1001)),
959 1_735_689_600,
960 ),
961 ResponseItem::new_uniqueness(
962 "document".into(),
963 2,
964 ZeroKnowledgeProof::default(),
965 Nullifier::from(test_field_element(1002)),
966 1_735_689_600,
967 ),
968 ResponseItem::new_uniqueness(
969 "passport".into(),
970 3,
971 ZeroKnowledgeProof::default(),
972 Nullifier::from(test_field_element(1003)),
973 1_735_689_600,
974 ),
975 ],
976 };
977 let err = request.validate_response(&unexpected).unwrap_err();
978 assert!(matches!(
979 err,
980 ValidationError::UnexpectedCredential(ref id) if id == "passport"
981 ));
982
983 let duplicate = ProofResponse {
984 id: "req_1".into(),
985 version: RequestVersion::V1,
986 session_id: None,
987 error: None,
988 responses: vec![
989 ResponseItem::new_uniqueness(
990 "orb".into(),
991 1,
992 ZeroKnowledgeProof::default(),
993 Nullifier::from(test_field_element(1001)),
994 1_735_689_600,
995 ),
996 ResponseItem::new_uniqueness(
997 "orb".into(),
998 1,
999 ZeroKnowledgeProof::default(),
1000 Nullifier::from(test_field_element(1001)),
1001 1_735_689_600,
1002 ),
1003 ],
1004 };
1005 let err = request.validate_response(&duplicate).unwrap_err();
1006 assert!(matches!(
1007 err,
1008 ValidationError::DuplicateCredential(ref id) if id == "orb"
1009 ));
1010 }
1011
1012 #[test]
1013 fn constraint_depth_enforced() {
1014 let deep = ConstraintExpr::All {
1016 all: vec![ConstraintNode::Expr(ConstraintExpr::Any {
1017 any: vec![ConstraintNode::Expr(ConstraintExpr::All {
1018 all: vec![ConstraintNode::Type("orb".into())],
1019 })],
1020 })],
1021 };
1022
1023 let request = ProofRequest {
1024 id: "req_2".into(),
1025 version: RequestVersion::V1,
1026 created_at: 1_735_689_600,
1027 expires_at: 1_735_689_600,
1028 rp_id: RpId::new(1),
1029 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1030 session_id: None,
1031 action: Some(test_field_element(1)),
1032 signature: test_signature(),
1033 nonce: test_nonce(),
1034 requests: vec![RequestItem {
1035 identifier: "orb".into(),
1036 issuer_schema_id: 1,
1037 signal: None,
1038 genesis_issued_at_min: None,
1039 expires_at_min: None,
1040 }],
1041 constraints: Some(deep),
1042 };
1043
1044 let response = ProofResponse {
1045 id: "req_2".into(),
1046 version: RequestVersion::V1,
1047 session_id: None,
1048 error: None,
1049 responses: vec![ResponseItem::new_uniqueness(
1050 "orb".into(),
1051 1,
1052 ZeroKnowledgeProof::default(),
1053 Nullifier::from(test_field_element(1001)),
1054 1_735_689_600,
1055 )],
1056 };
1057
1058 let err = request.validate_response(&response).unwrap_err();
1059 assert!(matches!(err, ValidationError::ConstraintTooDeep));
1060 }
1061
1062 #[test]
1063 #[allow(clippy::too_many_lines)]
1064 fn constraint_node_limit_boundary_passes() {
1065 let expr = ConstraintExpr::All {
1069 all: vec![
1070 ConstraintNode::Type("test_req_10".into()),
1071 ConstraintNode::Expr(ConstraintExpr::Any {
1072 any: vec![
1073 ConstraintNode::Type("test_req_11".into()),
1074 ConstraintNode::Type("test_req_12".into()),
1075 ConstraintNode::Type("test_req_13".into()),
1076 ConstraintNode::Type("test_req_14".into()),
1077 ],
1078 }),
1079 ConstraintNode::Expr(ConstraintExpr::Any {
1080 any: vec![
1081 ConstraintNode::Type("test_req_15".into()),
1082 ConstraintNode::Type("test_req_16".into()),
1083 ConstraintNode::Type("test_req_17".into()),
1084 ConstraintNode::Type("test_req_18".into()),
1085 ],
1086 }),
1087 ],
1088 };
1089
1090 let request = ProofRequest {
1091 id: "req_nodes_ok".into(),
1092 version: RequestVersion::V1,
1093 created_at: 1_735_689_600,
1094 expires_at: 1_735_689_600,
1095 rp_id: RpId::new(1),
1096 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1097 session_id: None,
1098 action: Some(test_field_element(5)),
1099 signature: test_signature(),
1100 nonce: test_nonce(),
1101 requests: vec![
1102 RequestItem {
1103 identifier: "test_req_10".into(),
1104 issuer_schema_id: 10,
1105 signal: None,
1106 genesis_issued_at_min: None,
1107 expires_at_min: None,
1108 },
1109 RequestItem {
1110 identifier: "test_req_11".into(),
1111 issuer_schema_id: 11,
1112 signal: None,
1113 genesis_issued_at_min: None,
1114 expires_at_min: None,
1115 },
1116 RequestItem {
1117 identifier: "test_req_12".into(),
1118 issuer_schema_id: 12,
1119 signal: None,
1120 genesis_issued_at_min: None,
1121 expires_at_min: None,
1122 },
1123 RequestItem {
1124 identifier: "test_req_13".into(),
1125 issuer_schema_id: 13,
1126 signal: None,
1127 genesis_issued_at_min: None,
1128 expires_at_min: None,
1129 },
1130 RequestItem {
1131 identifier: "test_req_14".into(),
1132 issuer_schema_id: 14,
1133 signal: None,
1134 genesis_issued_at_min: None,
1135 expires_at_min: None,
1136 },
1137 RequestItem {
1138 identifier: "test_req_15".into(),
1139 issuer_schema_id: 15,
1140 signal: None,
1141 genesis_issued_at_min: None,
1142 expires_at_min: None,
1143 },
1144 RequestItem {
1145 identifier: "test_req_16".into(),
1146 issuer_schema_id: 16,
1147 signal: None,
1148 genesis_issued_at_min: None,
1149 expires_at_min: None,
1150 },
1151 RequestItem {
1152 identifier: "test_req_17".into(),
1153 issuer_schema_id: 17,
1154 signal: None,
1155 genesis_issued_at_min: None,
1156 expires_at_min: None,
1157 },
1158 RequestItem {
1159 identifier: "test_req_18".into(),
1160 issuer_schema_id: 18,
1161 signal: None,
1162 genesis_issued_at_min: None,
1163 expires_at_min: None,
1164 },
1165 ],
1166 constraints: Some(expr),
1167 };
1168
1169 let response = ProofResponse {
1171 id: "req_nodes_ok".into(),
1172 version: RequestVersion::V1,
1173 session_id: None,
1174 error: None,
1175 responses: vec![
1176 ResponseItem::new_uniqueness(
1177 "test_req_10".into(),
1178 10,
1179 ZeroKnowledgeProof::default(),
1180 Nullifier::from(test_field_element(1010)),
1181 1_735_689_600,
1182 ),
1183 ResponseItem::new_uniqueness(
1184 "test_req_11".into(),
1185 11,
1186 ZeroKnowledgeProof::default(),
1187 Nullifier::from(test_field_element(1011)),
1188 1_735_689_600,
1189 ),
1190 ResponseItem::new_uniqueness(
1191 "test_req_15".into(),
1192 15,
1193 ZeroKnowledgeProof::default(),
1194 Nullifier::from(test_field_element(1015)),
1195 1_735_689_600,
1196 ),
1197 ],
1198 };
1199
1200 assert!(request.validate_response(&response).is_ok());
1202 }
1203
1204 #[test]
1205 #[allow(clippy::too_many_lines)]
1206 fn constraint_node_limit_exceeded_fails() {
1207 let expr = ConstraintExpr::All {
1210 all: vec![
1211 ConstraintNode::Type("t0".into()),
1212 ConstraintNode::Expr(ConstraintExpr::Any {
1213 any: vec![
1214 ConstraintNode::Type("t1".into()),
1215 ConstraintNode::Type("t2".into()),
1216 ConstraintNode::Type("t3".into()),
1217 ConstraintNode::Type("t4".into()),
1218 ],
1219 }),
1220 ConstraintNode::Expr(ConstraintExpr::Any {
1221 any: vec![
1222 ConstraintNode::Type("t5".into()),
1223 ConstraintNode::Type("t6".into()),
1224 ConstraintNode::Type("t7".into()),
1225 ConstraintNode::Type("t8".into()),
1226 ConstraintNode::Type("t9".into()),
1227 ],
1228 }),
1229 ],
1230 };
1231
1232 let request = ProofRequest {
1233 id: "req_nodes_too_many".into(),
1234 version: RequestVersion::V1,
1235 created_at: 1_735_689_600,
1236 expires_at: 1_735_689_600,
1237 rp_id: RpId::new(1),
1238 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1239 session_id: None,
1240 action: Some(test_field_element(1)),
1241 signature: test_signature(),
1242 nonce: test_nonce(),
1243 requests: vec![
1244 RequestItem {
1245 identifier: "test_req_20".into(),
1246 issuer_schema_id: 20,
1247 signal: None,
1248 genesis_issued_at_min: None,
1249 expires_at_min: None,
1250 },
1251 RequestItem {
1252 identifier: "test_req_21".into(),
1253 issuer_schema_id: 21,
1254 signal: None,
1255 genesis_issued_at_min: None,
1256 expires_at_min: None,
1257 },
1258 RequestItem {
1259 identifier: "test_req_22".into(),
1260 issuer_schema_id: 22,
1261 signal: None,
1262 genesis_issued_at_min: None,
1263 expires_at_min: None,
1264 },
1265 RequestItem {
1266 identifier: "test_req_23".into(),
1267 issuer_schema_id: 23,
1268 signal: None,
1269 genesis_issued_at_min: None,
1270 expires_at_min: None,
1271 },
1272 RequestItem {
1273 identifier: "test_req_24".into(),
1274 issuer_schema_id: 24,
1275 signal: None,
1276 genesis_issued_at_min: None,
1277 expires_at_min: None,
1278 },
1279 RequestItem {
1280 identifier: "test_req_25".into(),
1281 issuer_schema_id: 25,
1282 signal: None,
1283 genesis_issued_at_min: None,
1284 expires_at_min: None,
1285 },
1286 RequestItem {
1287 identifier: "test_req_26".into(),
1288 issuer_schema_id: 26,
1289 signal: None,
1290 genesis_issued_at_min: None,
1291 expires_at_min: None,
1292 },
1293 RequestItem {
1294 identifier: "test_req_27".into(),
1295 issuer_schema_id: 27,
1296 signal: None,
1297 genesis_issued_at_min: None,
1298 expires_at_min: None,
1299 },
1300 RequestItem {
1301 identifier: "test_req_28".into(),
1302 issuer_schema_id: 28,
1303 signal: None,
1304 genesis_issued_at_min: None,
1305 expires_at_min: None,
1306 },
1307 RequestItem {
1308 identifier: "test_req_29".into(),
1309 issuer_schema_id: 29,
1310 signal: None,
1311 genesis_issued_at_min: None,
1312 expires_at_min: None,
1313 },
1314 ],
1315 constraints: Some(expr),
1316 };
1317
1318 let response = ProofResponse {
1320 id: "req_nodes_too_many".into(),
1321 version: RequestVersion::V1,
1322 session_id: None,
1323 error: None,
1324 responses: vec![ResponseItem::new_uniqueness(
1325 "test_req_20".into(),
1326 20,
1327 ZeroKnowledgeProof::default(),
1328 Nullifier::from(test_field_element(1020)),
1329 1_735_689_600,
1330 )],
1331 };
1332
1333 let err = request.validate_response(&response).unwrap_err();
1334 assert!(matches!(err, ValidationError::ConstraintTooLarge));
1335 }
1336
1337 #[test]
1338 fn request_single_credential_parse_and_validate() {
1339 let req = ProofRequest {
1340 id: "req_18c0f7f03e7d".into(),
1341 version: RequestVersion::V1,
1342 created_at: 1_725_381_192,
1343 expires_at: 1_725_381_492,
1344 rp_id: RpId::new(1),
1345 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1346 session_id: Some(SessionId::default()),
1347 action: Some(test_field_element(1)),
1348 signature: test_signature(),
1349 nonce: test_nonce(),
1350 requests: vec![RequestItem {
1351 identifier: "test_req_1".into(),
1352 issuer_schema_id: 1,
1353 signal: Some("abcd-efgh-ijkl".into()),
1354 genesis_issued_at_min: Some(1_725_381_192),
1355 expires_at_min: None,
1356 }],
1357 constraints: None,
1358 };
1359
1360 assert_eq!(req.id, "req_18c0f7f03e7d");
1361 assert_eq!(req.requests.len(), 1);
1362
1363 let resp = ProofResponse {
1365 id: req.id.clone(),
1366 version: RequestVersion::V1,
1367 session_id: Some(SessionId::default()),
1368 error: None,
1369 responses: vec![ResponseItem::new_session(
1370 "test_req_1".into(),
1371 1,
1372 ZeroKnowledgeProof::default(),
1373 SessionNullifier::new(test_field_element(1001), test_field_element(1)),
1374 1_725_381_192,
1375 )],
1376 };
1377 assert!(req.validate_response(&resp).is_ok());
1378 }
1379
1380 #[test]
1381 fn request_multiple_credentials_all_constraint_and_missing() {
1382 let req = ProofRequest {
1383 id: "req_18c0f7f03e7d".into(),
1384 version: RequestVersion::V1,
1385 created_at: 1_725_381_192,
1386 expires_at: 1_725_381_492,
1387 rp_id: RpId::new(1),
1388 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1389 session_id: None,
1390 action: Some(test_field_element(1)),
1391 signature: test_signature(),
1392 nonce: test_nonce(),
1393 requests: vec![
1394 RequestItem {
1395 identifier: "test_req_1".into(),
1396 issuer_schema_id: 1,
1397 signal: Some("abcd-efgh-ijkl".into()),
1398 genesis_issued_at_min: Some(1_725_381_192),
1399 expires_at_min: None,
1400 },
1401 RequestItem {
1402 identifier: "test_req_2".into(),
1403 issuer_schema_id: 2,
1404 signal: Some("abcd-efgh-ijkl".into()),
1405 genesis_issued_at_min: Some(1_725_381_192),
1406 expires_at_min: None,
1407 },
1408 ],
1409 constraints: Some(ConstraintExpr::All {
1410 all: vec![
1411 ConstraintNode::Type("test_req_1".into()),
1412 ConstraintNode::Type("test_req_2".into()),
1413 ],
1414 }),
1415 };
1416
1417 let resp = ProofResponse {
1419 id: req.id.clone(),
1420 version: RequestVersion::V1,
1421 session_id: None,
1422 error: None,
1423 responses: vec![ResponseItem::new_uniqueness(
1424 "test_req_2".into(),
1425 2,
1426 ZeroKnowledgeProof::default(),
1427 Nullifier::from(test_field_element(1001)),
1428 1_725_381_192,
1429 )],
1430 };
1431
1432 let err = req.validate_response(&resp).unwrap_err();
1433 assert!(matches!(err, ValidationError::ConstraintNotSatisfied));
1434 }
1435
1436 #[test]
1437 fn request_more_complex_constraints_nested_success() {
1438 let req = ProofRequest {
1439 id: "req_18c0f7f03e7d".into(),
1440 version: RequestVersion::V1,
1441 created_at: 1_725_381_192,
1442 expires_at: 1_725_381_492,
1443 rp_id: RpId::new(1),
1444 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1445 session_id: None,
1446 action: Some(test_field_element(1)),
1447 signature: test_signature(),
1448 nonce: test_nonce(),
1449 requests: vec![
1450 RequestItem {
1451 identifier: "test_req_1".into(),
1452 issuer_schema_id: 1,
1453 signal: Some("abcd-efgh-ijkl".into()),
1454 genesis_issued_at_min: None,
1455 expires_at_min: None,
1456 },
1457 RequestItem {
1458 identifier: "test_req_2".into(),
1459 issuer_schema_id: 2,
1460 signal: Some("mnop-qrst-uvwx".into()),
1461 genesis_issued_at_min: None,
1462 expires_at_min: None,
1463 },
1464 RequestItem {
1465 identifier: "test_req_3".into(),
1466 issuer_schema_id: 3,
1467 signal: Some("abcd-efgh-ijkl".into()),
1468 genesis_issued_at_min: None,
1469 expires_at_min: None,
1470 },
1471 ],
1472 constraints: Some(ConstraintExpr::All {
1473 all: vec![
1474 ConstraintNode::Type("test_req_3".into()),
1475 ConstraintNode::Expr(ConstraintExpr::Any {
1476 any: vec![
1477 ConstraintNode::Type("test_req_1".into()),
1478 ConstraintNode::Type("test_req_2".into()),
1479 ],
1480 }),
1481 ],
1482 }),
1483 };
1484
1485 let resp = ProofResponse {
1487 id: req.id.clone(),
1488 version: RequestVersion::V1,
1489 session_id: None,
1490 error: None,
1491 responses: vec![
1492 ResponseItem::new_uniqueness(
1493 "test_req_3".into(),
1494 3,
1495 ZeroKnowledgeProof::default(),
1496 Nullifier::from(test_field_element(1001)),
1497 1_725_381_192,
1498 ),
1499 ResponseItem::new_uniqueness(
1500 "test_req_1".into(),
1501 1,
1502 ZeroKnowledgeProof::default(),
1503 Nullifier::from(test_field_element(1002)),
1504 1_725_381_192,
1505 ),
1506 ],
1507 };
1508
1509 assert!(req.validate_response(&resp).is_ok());
1510 }
1511
1512 #[test]
1513 fn request_validate_response_with_enumerate() {
1514 let req = ProofRequest {
1515 id: "req_enum".into(),
1516 version: RequestVersion::V1,
1517 created_at: 1_725_381_192,
1518 expires_at: 1_725_381_492,
1519 rp_id: RpId::new(1),
1520 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1521 session_id: None,
1522 action: Some(test_field_element(1)),
1523 signature: test_signature(),
1524 nonce: test_nonce(),
1525 requests: vec![
1526 RequestItem {
1527 identifier: "passport".into(),
1528 issuer_schema_id: 2,
1529 signal: None,
1530 genesis_issued_at_min: None,
1531 expires_at_min: None,
1532 },
1533 RequestItem {
1534 identifier: "national_id".into(),
1535 issuer_schema_id: 3,
1536 signal: None,
1537 genesis_issued_at_min: None,
1538 expires_at_min: None,
1539 },
1540 ],
1541 constraints: Some(ConstraintExpr::Enumerate {
1542 enumerate: vec![
1543 ConstraintNode::Type("passport".into()),
1544 ConstraintNode::Type("national_id".into()),
1545 ],
1546 }),
1547 };
1548
1549 let ok_resp = ProofResponse {
1551 id: req.id.clone(),
1552 version: RequestVersion::V1,
1553 session_id: None,
1554 error: None,
1555 responses: vec![ResponseItem::new_uniqueness(
1556 "passport".into(),
1557 2,
1558 ZeroKnowledgeProof::default(),
1559 Nullifier::from(test_field_element(2002)),
1560 1_725_381_192,
1561 )],
1562 };
1563 assert!(req.validate_response(&ok_resp).is_ok());
1564
1565 let fail_resp = ProofResponse {
1567 id: req.id.clone(),
1568 version: RequestVersion::V1,
1569 session_id: None,
1570 error: None,
1571 responses: vec![],
1572 };
1573 let err = req.validate_response(&fail_resp).unwrap_err();
1574 assert!(matches!(err, ValidationError::ConstraintNotSatisfied));
1575 }
1576
1577 #[test]
1578 fn request_json_parse() {
1579 let with_signal = r#"{
1581 "id": "req_abc123",
1582 "version": 1,
1583 "created_at": 1725381192,
1584 "expires_at": 1725381492,
1585 "rp_id": "rp_0000000000000001",
1586 "oprf_key_id": "0x1",
1587 "session_id": null,
1588 "action": "0x000000000000000000000000000000000000000000000000000000000000002a",
1589 "signature": "0xa1fd06f0d8ceb541f6096fe2e865063eac1ff085c9d2bac2eedcc9ed03804bfc18d956b38c5ac3a8f7e71fde43deff3bda254d369c699f3c7a3f8e6b8477a5f51c",
1590 "nonce": "0x0000000000000000000000000000000000000000000000000000000000000001",
1591 "proof_requests": [
1592 {
1593 "identifier": "orb",
1594 "issuer_schema_id": 1,
1595 "signal": "0xdeadbeef",
1596 "genesis_issued_at_min": 1725381192,
1597 "expires_at_min": 1725381492
1598 }
1599 ]
1600}"#;
1601
1602 let req = ProofRequest::from_json(with_signal).expect("parse with signal");
1603 assert_eq!(req.id, "req_abc123");
1604 assert_eq!(req.requests.len(), 1);
1605 assert_eq!(req.requests[0].signal, Some(b"\xde\xad\xbe\xef".to_vec()));
1606 assert_eq!(req.requests[0].genesis_issued_at_min, Some(1_725_381_192));
1607 assert_eq!(req.requests[0].expires_at_min, Some(1_725_381_492));
1608
1609 let without_signal = r#"{
1610 "id": "req_abc123",
1611 "version": 1,
1612 "created_at": 1725381192,
1613 "expires_at": 1725381492,
1614 "rp_id": "rp_0000000000000001",
1615 "oprf_key_id": "0x1",
1616 "session_id": null,
1617 "action": "0x000000000000000000000000000000000000000000000000000000000000002a",
1618 "signature": "0xa1fd06f0d8ceb541f6096fe2e865063eac1ff085c9d2bac2eedcc9ed03804bfc18d956b38c5ac3a8f7e71fde43deff3bda254d369c699f3c7a3f8e6b8477a5f51c",
1619 "nonce": "0x0000000000000000000000000000000000000000000000000000000000000001",
1620 "proof_requests": [
1621 {
1622 "identifier": "orb",
1623 "issuer_schema_id": 1
1624 }
1625 ]
1626}"#;
1627
1628 let req = ProofRequest::from_json(without_signal).expect("parse without signal");
1629 assert!(req.requests[0].signal.is_none());
1630 assert_eq!(req.requests[0].signal_hash(), FieldElement::ZERO);
1631 }
1632
1633 #[test]
1634 fn response_json_parse() {
1635 let ok_json = r#"{
1637 "id": "req_18c0f7f03e7d",
1638 "version": 1,
1639 "responses": [
1640 {
1641 "identifier": "orb",
1642 "issuer_schema_id": 100,
1643 "proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
1644 "nullifier": "nil_00000000000000000000000000000000000000000000000000000000000003e9",
1645 "expires_at_min": 1725381192
1646 }
1647 ]
1648}"#;
1649
1650 let ok = ProofResponse::from_json(ok_json).unwrap();
1651 assert_eq!(ok.successful_credentials(), vec![100]);
1652 assert!(ok.responses[0].is_uniqueness());
1653
1654 let canonical_session_nullifier = serde_json::to_string(&SessionNullifier::new(
1656 test_field_element(1001),
1657 test_field_element(42),
1658 ))
1659 .unwrap();
1660 let sess_json_canonical = format!(
1661 r#"{{
1662 "id": "req_18c0f7f03e7d",
1663 "version": 1,
1664 "session_id": "session_00000000000000000000000000000000000000000000000000000000000003ea0100000000000000000000000000000000000000000000000000000000000001",
1665 "responses": [
1666 {{
1667 "identifier": "orb",
1668 "issuer_schema_id": 100,
1669 "proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
1670 "session_nullifier": {canonical_session_nullifier},
1671 "expires_at_min": 1725381192
1672 }}
1673 ]
1674}}"#
1675 );
1676 let sess_canonical = ProofResponse::from_json(&sess_json_canonical).unwrap();
1677 assert_eq!(sess_canonical.successful_credentials(), vec![100]);
1678 assert!(sess_canonical.responses[0].is_session());
1679 assert_eq!(
1680 sess_canonical.session_id.unwrap().oprf_seed().to_u256(),
1681 uint!(0x0100000000000000000000000000000000000000000000000000000000000001_U256)
1682 );
1683 }
1684 #[test]
1687 fn request_rejects_duplicate_issuer_schema_ids_on_parse() {
1688 let req = ProofRequest {
1689 id: "req_dup".into(),
1690 version: RequestVersion::V1,
1691 created_at: 1_725_381_192,
1692 expires_at: 1_725_381_492,
1693 rp_id: RpId::new(1),
1694 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1695 session_id: None,
1696 action: Some(test_field_element(5)),
1697 signature: test_signature(),
1698 nonce: test_nonce(),
1699 requests: vec![
1700 RequestItem {
1701 identifier: "test_req_1".into(),
1702 issuer_schema_id: 1,
1703 signal: None,
1704 genesis_issued_at_min: None,
1705 expires_at_min: None,
1706 },
1707 RequestItem {
1708 identifier: "test_req_2".into(),
1709 issuer_schema_id: 1, signal: None,
1711 genesis_issued_at_min: None,
1712 expires_at_min: None,
1713 },
1714 ],
1715 constraints: None,
1716 };
1717
1718 let json = req.to_json().unwrap();
1720 let err = ProofRequest::from_json(&json).unwrap_err();
1721 let msg = err.to_string();
1722 assert!(
1723 msg.contains("duplicate issuer schema id"),
1724 "Expected error message to contain 'duplicate issuer schema id', got: {msg}"
1725 );
1726 }
1727
1728 #[test]
1729 fn response_with_error_has_empty_responses_and_fails_validation() {
1730 let request = ProofRequest {
1731 id: "req_error".into(),
1732 version: RequestVersion::V1,
1733 created_at: 1_735_689_600,
1734 expires_at: 1_735_689_600,
1735 rp_id: RpId::new(1),
1736 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1737 session_id: None,
1738 action: Some(FieldElement::ZERO),
1739 signature: test_signature(),
1740 nonce: test_nonce(),
1741 requests: vec![RequestItem {
1742 identifier: "orb".into(),
1743 issuer_schema_id: 1,
1744 signal: None,
1745 genesis_issued_at_min: None,
1746 expires_at_min: None,
1747 }],
1748 constraints: None,
1749 };
1750
1751 let error_response = ProofResponse {
1753 id: "req_error".into(),
1754 version: RequestVersion::V1,
1755 session_id: None,
1756 error: Some("credential_not_available".into()),
1757 responses: vec![], };
1759
1760 let err = request.validate_response(&error_response).unwrap_err();
1762 assert!(matches!(err, ValidationError::ProofGenerationFailed(_)));
1763 if let ValidationError::ProofGenerationFailed(msg) = err {
1764 assert_eq!(msg, "credential_not_available");
1765 }
1766
1767 assert_eq!(error_response.successful_credentials(), Vec::<u64>::new());
1769
1770 let expr = ConstraintExpr::All {
1772 all: vec![ConstraintNode::Type("orb".into())],
1773 };
1774 assert!(!error_response.constraints_satisfied(&expr));
1775 }
1776
1777 #[test]
1778 fn response_error_json_parse() {
1779 let error_json = r#"{
1781 "id": "req_error",
1782 "version": 1,
1783 "error": "credential_not_available",
1784 "responses": []
1785}"#;
1786
1787 let error_resp = ProofResponse::from_json(error_json).unwrap();
1788 assert_eq!(error_resp.error, Some("credential_not_available".into()));
1789 assert_eq!(error_resp.responses.len(), 0);
1790 assert_eq!(error_resp.successful_credentials(), Vec::<u64>::new());
1791 }
1792
1793 #[test]
1794 fn credentials_to_prove_none_constraints_requires_all_and_drops_if_missing() {
1795 let req = ProofRequest {
1796 id: "req".into(),
1797 version: RequestVersion::V1,
1798 created_at: 1_735_689_600,
1799 expires_at: 1_735_689_600, rp_id: RpId::new(1),
1801 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1802 session_id: None,
1803 action: Some(test_field_element(5)),
1804 signature: test_signature(),
1805 nonce: test_nonce(),
1806 requests: vec![
1807 RequestItem {
1808 identifier: "orb".into(),
1809 issuer_schema_id: 100,
1810 signal: None,
1811 genesis_issued_at_min: None,
1812 expires_at_min: None,
1813 },
1814 RequestItem {
1815 identifier: "passport".into(),
1816 issuer_schema_id: 101,
1817 signal: None,
1818 genesis_issued_at_min: None,
1819 expires_at_min: None,
1820 },
1821 ],
1822 constraints: None,
1823 };
1824
1825 let available_ok: HashSet<u64> = [100, 101].into_iter().collect();
1826 let sel_ok = req.credentials_to_prove(&available_ok).unwrap();
1827 assert_eq!(sel_ok.len(), 2);
1828 assert_eq!(sel_ok[0].issuer_schema_id, 100);
1829 assert_eq!(sel_ok[1].issuer_schema_id, 101);
1830
1831 let available_missing: HashSet<u64> = std::iter::once(100).collect();
1832 assert!(req.credentials_to_prove(&available_missing).is_none());
1833 }
1834
1835 #[test]
1836 fn credentials_to_prove_with_constraints_all_and_any() {
1837 let orb_id = 100;
1839 let passport_id = 101;
1840 let national_id = 102;
1841
1842 let req = ProofRequest {
1843 id: "req".into(),
1844 version: RequestVersion::V1,
1845 created_at: 1_735_689_600,
1846 expires_at: 1_735_689_600, rp_id: RpId::new(1),
1848 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1849 session_id: None,
1850 action: Some(test_field_element(1)),
1851 signature: test_signature(),
1852 nonce: test_nonce(),
1853 requests: vec![
1854 RequestItem {
1855 identifier: "orb".into(),
1856 issuer_schema_id: orb_id,
1857 signal: None,
1858 genesis_issued_at_min: None,
1859 expires_at_min: None,
1860 },
1861 RequestItem {
1862 identifier: "passport".into(),
1863 issuer_schema_id: passport_id,
1864 signal: None,
1865 genesis_issued_at_min: None,
1866 expires_at_min: None,
1867 },
1868 RequestItem {
1869 identifier: "national_id".into(),
1870 issuer_schema_id: national_id,
1871 signal: None,
1872 genesis_issued_at_min: None,
1873 expires_at_min: None,
1874 },
1875 ],
1876 constraints: Some(ConstraintExpr::All {
1877 all: vec![
1878 ConstraintNode::Type("orb".into()),
1879 ConstraintNode::Expr(ConstraintExpr::Any {
1880 any: vec![
1881 ConstraintNode::Type("passport".into()),
1882 ConstraintNode::Type("national_id".into()),
1883 ],
1884 }),
1885 ],
1886 }),
1887 };
1888
1889 let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
1891 let sel1 = req.credentials_to_prove(&available1).unwrap();
1892 assert_eq!(sel1.len(), 2);
1893 assert_eq!(sel1[0].issuer_schema_id, orb_id);
1894 assert_eq!(sel1[1].issuer_schema_id, passport_id);
1895
1896 let available2: HashSet<u64> = [orb_id, national_id].into_iter().collect();
1898 let sel2 = req.credentials_to_prove(&available2).unwrap();
1899 assert_eq!(sel2.len(), 2);
1900 assert_eq!(sel2[0].issuer_schema_id, orb_id);
1901 assert_eq!(sel2[1].issuer_schema_id, national_id);
1902
1903 let available3: HashSet<u64> = std::iter::once(passport_id).collect();
1905 assert!(req.credentials_to_prove(&available3).is_none());
1906 }
1907
1908 #[test]
1909 fn credentials_to_prove_with_constraints_enumerate() {
1910 let orb_id = 100;
1911 let passport_id = 101;
1912 let national_id = 102;
1913
1914 let req = ProofRequest {
1915 id: "req".into(),
1916 version: RequestVersion::V1,
1917 created_at: 1_735_689_600,
1918 expires_at: 1_735_689_600,
1919 rp_id: RpId::new(1),
1920 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1921 session_id: None,
1922 action: Some(test_field_element(1)),
1923 signature: test_signature(),
1924 nonce: test_nonce(),
1925 requests: vec![
1926 RequestItem {
1927 identifier: "orb".into(),
1928 issuer_schema_id: orb_id,
1929 signal: None,
1930 genesis_issued_at_min: None,
1931 expires_at_min: None,
1932 },
1933 RequestItem {
1934 identifier: "passport".into(),
1935 issuer_schema_id: passport_id,
1936 signal: None,
1937 genesis_issued_at_min: None,
1938 expires_at_min: None,
1939 },
1940 RequestItem {
1941 identifier: "national_id".into(),
1942 issuer_schema_id: national_id,
1943 signal: None,
1944 genesis_issued_at_min: None,
1945 expires_at_min: None,
1946 },
1947 ],
1948 constraints: Some(ConstraintExpr::Enumerate {
1949 enumerate: vec![
1950 ConstraintNode::Type("passport".into()),
1951 ConstraintNode::Type("national_id".into()),
1952 ],
1953 }),
1954 };
1955
1956 let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
1958 let sel1 = req.credentials_to_prove(&available1).unwrap();
1959 assert_eq!(sel1.len(), 1);
1960 assert_eq!(sel1[0].issuer_schema_id, passport_id);
1961
1962 let available2: HashSet<u64> = [orb_id, passport_id, national_id].into_iter().collect();
1964 let sel2 = req.credentials_to_prove(&available2).unwrap();
1965 assert_eq!(sel2.len(), 2);
1966 assert_eq!(sel2[0].issuer_schema_id, passport_id);
1967 assert_eq!(sel2[1].issuer_schema_id, national_id);
1968
1969 let available3: HashSet<u64> = std::iter::once(orb_id).collect();
1971 assert!(req.credentials_to_prove(&available3).is_none());
1972 }
1973
1974 #[test]
1975 fn credentials_to_prove_with_constraints_all_and_enumerate() {
1976 let orb_id = 100;
1977 let passport_id = 101;
1978 let national_id = 102;
1979
1980 let req = ProofRequest {
1981 id: "req".into(),
1982 version: RequestVersion::V1,
1983 created_at: 1_735_689_600,
1984 expires_at: 1_735_689_600,
1985 rp_id: RpId::new(1),
1986 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1987 session_id: None,
1988 action: Some(test_field_element(1)),
1989 signature: test_signature(),
1990 nonce: test_nonce(),
1991 requests: vec![
1992 RequestItem {
1993 identifier: "orb".into(),
1994 issuer_schema_id: orb_id,
1995 signal: None,
1996 genesis_issued_at_min: None,
1997 expires_at_min: None,
1998 },
1999 RequestItem {
2000 identifier: "passport".into(),
2001 issuer_schema_id: passport_id,
2002 signal: None,
2003 genesis_issued_at_min: None,
2004 expires_at_min: None,
2005 },
2006 RequestItem {
2007 identifier: "national_id".into(),
2008 issuer_schema_id: national_id,
2009 signal: None,
2010 genesis_issued_at_min: None,
2011 expires_at_min: None,
2012 },
2013 ],
2014 constraints: Some(ConstraintExpr::All {
2015 all: vec![
2016 ConstraintNode::Type("orb".into()),
2017 ConstraintNode::Expr(ConstraintExpr::Enumerate {
2018 enumerate: vec![
2019 ConstraintNode::Type("passport".into()),
2020 ConstraintNode::Type("national_id".into()),
2021 ],
2022 }),
2023 ],
2024 }),
2025 };
2026
2027 let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
2029 let sel1 = req.credentials_to_prove(&available1).unwrap();
2030 assert_eq!(sel1.len(), 2);
2031 assert_eq!(sel1[0].issuer_schema_id, orb_id);
2032 assert_eq!(sel1[1].issuer_schema_id, passport_id);
2033
2034 let available2: HashSet<u64> = [orb_id, passport_id, national_id].into_iter().collect();
2036 let sel2 = req.credentials_to_prove(&available2).unwrap();
2037 assert_eq!(sel2.len(), 3);
2038 assert_eq!(sel2[0].issuer_schema_id, orb_id);
2039 assert_eq!(sel2[1].issuer_schema_id, passport_id);
2040 assert_eq!(sel2[2].issuer_schema_id, national_id);
2041
2042 let available3: HashSet<u64> = std::iter::once(orb_id).collect();
2044 assert!(req.credentials_to_prove(&available3).is_none());
2045 }
2046
2047 #[test]
2048 fn request_item_effective_expires_at_min_defaults_to_created_at() {
2049 let request_created_at = 1_735_689_600; let custom_expires_at = 1_735_862_400; let item_with_none = RequestItem {
2054 identifier: "test".into(),
2055 issuer_schema_id: 100,
2056 signal: None,
2057 genesis_issued_at_min: None,
2058 expires_at_min: None,
2059 };
2060 assert_eq!(
2061 item_with_none.effective_expires_at_min(request_created_at),
2062 request_created_at,
2063 "When expires_at_min is None, should default to request created_at"
2064 );
2065
2066 let item_with_custom = RequestItem {
2068 identifier: "test".into(),
2069 issuer_schema_id: 100,
2070 signal: None,
2071 genesis_issued_at_min: None,
2072 expires_at_min: Some(custom_expires_at),
2073 };
2074 assert_eq!(
2075 item_with_custom.effective_expires_at_min(request_created_at),
2076 custom_expires_at,
2077 "When expires_at_min is Some, should use that explicit value"
2078 );
2079 }
2080
2081 #[test]
2082 fn validate_response_checks_expires_at_min_matches() {
2083 let request_created_at = 1_735_689_600; let custom_expires_at = 1_735_862_400; let request = ProofRequest {
2089 id: "req_expires_test".into(),
2090 version: RequestVersion::V1,
2091 created_at: request_created_at,
2092 expires_at: request_created_at + 300,
2093 rp_id: RpId::new(1),
2094 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2095 session_id: None,
2096 action: Some(test_field_element(1)),
2097 signature: test_signature(),
2098 nonce: test_nonce(),
2099 requests: vec![
2100 RequestItem {
2101 identifier: "orb".into(),
2102 issuer_schema_id: 100,
2103 signal: None,
2104 genesis_issued_at_min: None,
2105 expires_at_min: None, },
2107 RequestItem {
2108 identifier: "document".into(),
2109 issuer_schema_id: 101,
2110 signal: None,
2111 genesis_issued_at_min: None,
2112 expires_at_min: Some(custom_expires_at), },
2114 ],
2115 constraints: None,
2116 };
2117
2118 let valid_response = ProofResponse {
2120 id: "req_expires_test".into(),
2121 version: RequestVersion::V1,
2122 session_id: None,
2123 error: None,
2124 responses: vec![
2125 ResponseItem::new_uniqueness(
2126 "orb".into(),
2127 100,
2128 ZeroKnowledgeProof::default(),
2129 Nullifier::from(test_field_element(1001)),
2130 request_created_at, ),
2132 ResponseItem::new_uniqueness(
2133 "document".into(),
2134 101,
2135 ZeroKnowledgeProof::default(),
2136 Nullifier::from(test_field_element(1002)),
2137 custom_expires_at, ),
2139 ],
2140 };
2141 assert!(request.validate_response(&valid_response).is_ok());
2142
2143 let invalid_response_1 = ProofResponse {
2145 id: "req_expires_test".into(),
2146 version: RequestVersion::V1,
2147 session_id: None,
2148 error: None,
2149 responses: vec![
2150 ResponseItem::new_uniqueness(
2151 "orb".into(),
2152 100,
2153 ZeroKnowledgeProof::default(),
2154 Nullifier::from(test_field_element(1001)),
2155 custom_expires_at, ),
2157 ResponseItem::new_uniqueness(
2158 "document".into(),
2159 101,
2160 ZeroKnowledgeProof::default(),
2161 Nullifier::from(test_field_element(1002)),
2162 custom_expires_at,
2163 ),
2164 ],
2165 };
2166 let err1 = request.validate_response(&invalid_response_1).unwrap_err();
2167 assert!(matches!(
2168 err1,
2169 ValidationError::ExpiresAtMinMismatch(_, _, _)
2170 ));
2171 if let ValidationError::ExpiresAtMinMismatch(identifier, expected, got) = err1 {
2172 assert_eq!(identifier, "orb");
2173 assert_eq!(expected, request_created_at);
2174 assert_eq!(got, custom_expires_at);
2175 }
2176
2177 let invalid_response_2 = ProofResponse {
2179 id: "req_expires_test".into(),
2180 version: RequestVersion::V1,
2181 session_id: None,
2182 error: None,
2183 responses: vec![
2184 ResponseItem::new_uniqueness(
2185 "orb".into(),
2186 100,
2187 ZeroKnowledgeProof::default(),
2188 Nullifier::from(test_field_element(1001)),
2189 request_created_at,
2190 ),
2191 ResponseItem::new_uniqueness(
2192 "document".into(),
2193 101,
2194 ZeroKnowledgeProof::default(),
2195 Nullifier::from(test_field_element(1002)),
2196 request_created_at, ),
2198 ],
2199 };
2200 let err2 = request.validate_response(&invalid_response_2).unwrap_err();
2201 assert!(matches!(
2202 err2,
2203 ValidationError::ExpiresAtMinMismatch(_, _, _)
2204 ));
2205 if let ValidationError::ExpiresAtMinMismatch(identifier, expected, got) = err2 {
2206 assert_eq!(identifier, "document");
2207 assert_eq!(expected, custom_expires_at);
2208 assert_eq!(got, request_created_at);
2209 }
2210 }
2211
2212 #[test]
2213 fn test_validate_response_requires_session_id_in_response() {
2214 let request = ProofRequest {
2216 id: "req_session".into(),
2217 version: RequestVersion::V1,
2218 created_at: 1_735_689_600,
2219 expires_at: 1_735_689_900,
2220 rp_id: RpId::new(1),
2221 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2222 session_id: Some(SessionId::default()), action: Some(test_field_element(42)),
2224 signature: test_signature(),
2225 nonce: test_nonce(),
2226 requests: vec![RequestItem {
2227 identifier: "orb".into(),
2228 issuer_schema_id: 1,
2229 signal: None,
2230 genesis_issued_at_min: None,
2231 expires_at_min: None,
2232 }],
2233 constraints: None,
2234 };
2235
2236 let response_missing_session_id = ProofResponse {
2238 id: "req_session".into(),
2239 version: RequestVersion::V1,
2240 session_id: None, error: None,
2242 responses: vec![ResponseItem::new_session(
2243 "orb".into(),
2244 1,
2245 ZeroKnowledgeProof::default(),
2246 SessionNullifier::new(test_field_element(1001), test_field_element(42)),
2247 1_735_689_600,
2248 )],
2249 };
2250
2251 let err = request
2252 .validate_response(&response_missing_session_id)
2253 .unwrap_err();
2254 assert!(matches!(err, ValidationError::SessionIdMismatch));
2255 }
2256
2257 #[test]
2258 fn test_validate_response_requires_session_nullifier_for_session_proof() {
2259 let request = ProofRequest {
2261 id: "req_session".into(),
2262 version: RequestVersion::V1,
2263 created_at: 1_735_689_600,
2264 expires_at: 1_735_689_900,
2265 rp_id: RpId::new(1),
2266 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2267 session_id: Some(SessionId::default()), action: Some(test_field_element(42)),
2269 signature: test_signature(),
2270 nonce: test_nonce(),
2271 requests: vec![RequestItem {
2272 identifier: "orb".into(),
2273 issuer_schema_id: 1,
2274 signal: None,
2275 genesis_issued_at_min: None,
2276 expires_at_min: None,
2277 }],
2278 constraints: None,
2279 };
2280
2281 let response_wrong_nullifier_type = ProofResponse {
2283 id: "req_session".into(),
2284 version: RequestVersion::V1,
2285 session_id: Some(SessionId::default()),
2286 error: None,
2287 responses: vec![ResponseItem::new_uniqueness(
2288 "orb".into(),
2289 1,
2290 ZeroKnowledgeProof::default(),
2291 Nullifier::from(test_field_element(1001)), 1_735_689_600,
2293 )],
2294 };
2295
2296 let err = request
2297 .validate_response(&response_wrong_nullifier_type)
2298 .unwrap_err();
2299 assert!(matches!(
2300 err,
2301 ValidationError::MissingSessionNullifier(ref id) if id == "orb"
2302 ));
2303 }
2304
2305 #[test]
2306 fn test_validate_response_requires_nullifier_for_uniqueness_proof() {
2307 let request = ProofRequest {
2309 id: "req_uniqueness".into(),
2310 version: RequestVersion::V1,
2311 created_at: 1_735_689_600,
2312 expires_at: 1_735_689_900,
2313 rp_id: RpId::new(1),
2314 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2315 session_id: None, action: Some(test_field_element(42)),
2317 signature: test_signature(),
2318 nonce: test_nonce(),
2319 requests: vec![RequestItem {
2320 identifier: "orb".into(),
2321 issuer_schema_id: 1,
2322 signal: None,
2323 genesis_issued_at_min: None,
2324 expires_at_min: None,
2325 }],
2326 constraints: None,
2327 };
2328
2329 let response_wrong_nullifier_type = ProofResponse {
2331 id: "req_uniqueness".into(),
2332 version: RequestVersion::V1,
2333 session_id: None,
2334 error: None,
2335 responses: vec![ResponseItem::new_session(
2336 "orb".into(),
2337 1,
2338 ZeroKnowledgeProof::default(),
2339 SessionNullifier::new(test_field_element(1001), test_field_element(42)), 1_735_689_600,
2341 )],
2342 };
2343
2344 let err = request
2345 .validate_response(&response_wrong_nullifier_type)
2346 .unwrap_err();
2347 assert!(matches!(
2348 err,
2349 ValidationError::MissingNullifier(ref id) if id == "orb"
2350 ));
2351 }
2352}