1mod constraints;
6pub use constraints::{ConstraintExpr, ConstraintKind, ConstraintNode, MAX_CONSTRAINT_NODES};
7
8use crate::{
9 FieldElement, PrimitiveError, SessionNullifier, ZeroKnowledgeProof, nullifier::Nullifier,
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<FieldElement>,
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")]
197 pub session_id: Option<FieldElement>,
198 #[serde(skip_serializing_if = "Option::is_none")]
201 pub error: Option<String>,
202 pub responses: Vec<ResponseItem>,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
222#[serde(deny_unknown_fields)]
223pub struct ResponseItem {
224 pub identifier: String,
228
229 pub issuer_schema_id: u64,
231
232 pub proof: ZeroKnowledgeProof,
234
235 #[serde(skip_serializing_if = "Option::is_none")]
239 pub nullifier: Option<Nullifier>,
240
241 #[serde(skip_serializing_if = "Option::is_none")]
245 pub session_nullifier: Option<SessionNullifier>,
246
247 pub expires_at_min: u64,
251}
252
253impl ProofResponse {
254 #[must_use]
257 pub fn constraints_satisfied(&self, constraints: &ConstraintExpr<'_>) -> bool {
258 if self.error.is_some() {
260 return false;
261 }
262
263 let provided: HashSet<&str> = self
264 .responses
265 .iter()
266 .map(|item| item.identifier.as_str())
267 .collect();
268
269 constraints.evaluate(&|t| provided.contains(t))
270 }
271}
272
273impl ResponseItem {
274 #[must_use]
276 pub const fn new_uniqueness(
277 identifier: String,
278 issuer_schema_id: u64,
279 proof: ZeroKnowledgeProof,
280 nullifier: Nullifier,
281 expires_at_min: u64,
282 ) -> Self {
283 Self {
284 identifier,
285 issuer_schema_id,
286 proof,
287 nullifier: Some(nullifier),
288 session_nullifier: None,
289 expires_at_min,
290 }
291 }
292
293 #[must_use]
295 pub const fn new_session(
296 identifier: String,
297 issuer_schema_id: u64,
298 proof: ZeroKnowledgeProof,
299 session_nullifier: SessionNullifier,
300 expires_at_min: u64,
301 ) -> Self {
302 Self {
303 identifier,
304 issuer_schema_id,
305 proof,
306 nullifier: None,
307 session_nullifier: Some(session_nullifier),
308 expires_at_min,
309 }
310 }
311
312 #[must_use]
314 pub const fn is_session(&self) -> bool {
315 self.session_nullifier.is_some()
316 }
317
318 #[must_use]
320 pub const fn is_uniqueness(&self) -> bool {
321 self.nullifier.is_some()
322 }
323}
324
325impl ProofRequest {
326 #[must_use]
334 pub fn credentials_to_prove(&self, available: &HashSet<u64>) -> Option<Vec<&RequestItem>> {
335 let available_identifiers: HashSet<&str> = self
337 .requests
338 .iter()
339 .filter(|r| available.contains(&r.issuer_schema_id))
340 .map(|r| r.identifier.as_str())
341 .collect();
342
343 let is_selectable = |identifier: &str| available_identifiers.contains(identifier);
344
345 if self.constraints.is_none() {
347 return if self
348 .requests
349 .iter()
350 .all(|r| available.contains(&r.issuer_schema_id))
351 {
352 Some(self.requests.iter().collect())
353 } else {
354 None
355 };
356 }
357
358 let selected_identifiers = select_expr(self.constraints.as_ref().unwrap(), &is_selectable)?;
360 let selected_set: HashSet<&str> = selected_identifiers.into_iter().collect();
361
362 let result: Vec<&RequestItem> = self
364 .requests
365 .iter()
366 .filter(|r| selected_set.contains(r.identifier.as_str()))
367 .collect();
368 Some(result)
369 }
370
371 #[must_use]
373 pub fn find_request_by_issuer_schema_id(&self, issuer_schema_id: u64) -> Option<&RequestItem> {
374 self.requests
375 .iter()
376 .find(|r| r.issuer_schema_id == issuer_schema_id)
377 }
378
379 #[must_use]
381 pub const fn is_expired(&self, now: u64) -> bool {
382 now > self.expires_at
383 }
384
385 pub fn digest_hash(&self) -> Result<[u8; 32], PrimitiveError> {
399 use crate::rp::compute_rp_signature_msg;
400 use k256::sha2::{Digest, Sha256};
401
402 let msg = compute_rp_signature_msg(*self.nonce, self.created_at, self.expires_at);
403 let mut hasher = Sha256::new();
404 hasher.update(&msg);
405 Ok(hasher.finalize().into())
406 }
407
408 #[must_use]
415 pub fn computed_action<R: rand::CryptoRng + rand::RngCore>(&self, rng: &mut R) -> FieldElement {
416 match self.action {
417 Some(action) => action,
418 None => FieldElement::random(rng),
419 }
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(test_field_element(55)),
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(test_field_element(55)),
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": "0x00000000000000000000000000000000000000000000000000000000000003ea",
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 }
1680 #[test]
1683 fn request_rejects_duplicate_issuer_schema_ids_on_parse() {
1684 let req = ProofRequest {
1685 id: "req_dup".into(),
1686 version: RequestVersion::V1,
1687 created_at: 1_725_381_192,
1688 expires_at: 1_725_381_492,
1689 rp_id: RpId::new(1),
1690 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1691 session_id: None,
1692 action: Some(test_field_element(5)),
1693 signature: test_signature(),
1694 nonce: test_nonce(),
1695 requests: vec![
1696 RequestItem {
1697 identifier: "test_req_1".into(),
1698 issuer_schema_id: 1,
1699 signal: None,
1700 genesis_issued_at_min: None,
1701 expires_at_min: None,
1702 },
1703 RequestItem {
1704 identifier: "test_req_2".into(),
1705 issuer_schema_id: 1, signal: None,
1707 genesis_issued_at_min: None,
1708 expires_at_min: None,
1709 },
1710 ],
1711 constraints: None,
1712 };
1713
1714 let json = req.to_json().unwrap();
1716 let err = ProofRequest::from_json(&json).unwrap_err();
1717 let msg = err.to_string();
1718 assert!(
1719 msg.contains("duplicate issuer schema id"),
1720 "Expected error message to contain 'duplicate issuer schema id', got: {msg}"
1721 );
1722 }
1723
1724 #[test]
1725 fn response_with_error_has_empty_responses_and_fails_validation() {
1726 let request = ProofRequest {
1727 id: "req_error".into(),
1728 version: RequestVersion::V1,
1729 created_at: 1_735_689_600,
1730 expires_at: 1_735_689_600,
1731 rp_id: RpId::new(1),
1732 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1733 session_id: None,
1734 action: Some(FieldElement::ZERO),
1735 signature: test_signature(),
1736 nonce: test_nonce(),
1737 requests: vec![RequestItem {
1738 identifier: "orb".into(),
1739 issuer_schema_id: 1,
1740 signal: None,
1741 genesis_issued_at_min: None,
1742 expires_at_min: None,
1743 }],
1744 constraints: None,
1745 };
1746
1747 let error_response = ProofResponse {
1749 id: "req_error".into(),
1750 version: RequestVersion::V1,
1751 session_id: None,
1752 error: Some("credential_not_available".into()),
1753 responses: vec![], };
1755
1756 let err = request.validate_response(&error_response).unwrap_err();
1758 assert!(matches!(err, ValidationError::ProofGenerationFailed(_)));
1759 if let ValidationError::ProofGenerationFailed(msg) = err {
1760 assert_eq!(msg, "credential_not_available");
1761 }
1762
1763 assert_eq!(error_response.successful_credentials(), Vec::<u64>::new());
1765
1766 let expr = ConstraintExpr::All {
1768 all: vec![ConstraintNode::Type("orb".into())],
1769 };
1770 assert!(!error_response.constraints_satisfied(&expr));
1771 }
1772
1773 #[test]
1774 fn response_error_json_parse() {
1775 let error_json = r#"{
1777 "id": "req_error",
1778 "version": 1,
1779 "error": "credential_not_available",
1780 "responses": []
1781}"#;
1782
1783 let error_resp = ProofResponse::from_json(error_json).unwrap();
1784 assert_eq!(error_resp.error, Some("credential_not_available".into()));
1785 assert_eq!(error_resp.responses.len(), 0);
1786 assert_eq!(error_resp.successful_credentials(), Vec::<u64>::new());
1787 }
1788
1789 #[test]
1790 fn credentials_to_prove_none_constraints_requires_all_and_drops_if_missing() {
1791 let req = ProofRequest {
1792 id: "req".into(),
1793 version: RequestVersion::V1,
1794 created_at: 1_735_689_600,
1795 expires_at: 1_735_689_600, rp_id: RpId::new(1),
1797 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1798 session_id: None,
1799 action: Some(test_field_element(5)),
1800 signature: test_signature(),
1801 nonce: test_nonce(),
1802 requests: vec![
1803 RequestItem {
1804 identifier: "orb".into(),
1805 issuer_schema_id: 100,
1806 signal: None,
1807 genesis_issued_at_min: None,
1808 expires_at_min: None,
1809 },
1810 RequestItem {
1811 identifier: "passport".into(),
1812 issuer_schema_id: 101,
1813 signal: None,
1814 genesis_issued_at_min: None,
1815 expires_at_min: None,
1816 },
1817 ],
1818 constraints: None,
1819 };
1820
1821 let available_ok: HashSet<u64> = [100, 101].into_iter().collect();
1822 let sel_ok = req.credentials_to_prove(&available_ok).unwrap();
1823 assert_eq!(sel_ok.len(), 2);
1824 assert_eq!(sel_ok[0].issuer_schema_id, 100);
1825 assert_eq!(sel_ok[1].issuer_schema_id, 101);
1826
1827 let available_missing: HashSet<u64> = std::iter::once(100).collect();
1828 assert!(req.credentials_to_prove(&available_missing).is_none());
1829 }
1830
1831 #[test]
1832 fn credentials_to_prove_with_constraints_all_and_any() {
1833 let orb_id = 100;
1835 let passport_id = 101;
1836 let national_id = 102;
1837
1838 let req = ProofRequest {
1839 id: "req".into(),
1840 version: RequestVersion::V1,
1841 created_at: 1_735_689_600,
1842 expires_at: 1_735_689_600, rp_id: RpId::new(1),
1844 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1845 session_id: None,
1846 action: Some(test_field_element(1)),
1847 signature: test_signature(),
1848 nonce: test_nonce(),
1849 requests: vec![
1850 RequestItem {
1851 identifier: "orb".into(),
1852 issuer_schema_id: orb_id,
1853 signal: None,
1854 genesis_issued_at_min: None,
1855 expires_at_min: None,
1856 },
1857 RequestItem {
1858 identifier: "passport".into(),
1859 issuer_schema_id: passport_id,
1860 signal: None,
1861 genesis_issued_at_min: None,
1862 expires_at_min: None,
1863 },
1864 RequestItem {
1865 identifier: "national_id".into(),
1866 issuer_schema_id: national_id,
1867 signal: None,
1868 genesis_issued_at_min: None,
1869 expires_at_min: None,
1870 },
1871 ],
1872 constraints: Some(ConstraintExpr::All {
1873 all: vec![
1874 ConstraintNode::Type("orb".into()),
1875 ConstraintNode::Expr(ConstraintExpr::Any {
1876 any: vec![
1877 ConstraintNode::Type("passport".into()),
1878 ConstraintNode::Type("national_id".into()),
1879 ],
1880 }),
1881 ],
1882 }),
1883 };
1884
1885 let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
1887 let sel1 = req.credentials_to_prove(&available1).unwrap();
1888 assert_eq!(sel1.len(), 2);
1889 assert_eq!(sel1[0].issuer_schema_id, orb_id);
1890 assert_eq!(sel1[1].issuer_schema_id, passport_id);
1891
1892 let available2: HashSet<u64> = [orb_id, national_id].into_iter().collect();
1894 let sel2 = req.credentials_to_prove(&available2).unwrap();
1895 assert_eq!(sel2.len(), 2);
1896 assert_eq!(sel2[0].issuer_schema_id, orb_id);
1897 assert_eq!(sel2[1].issuer_schema_id, national_id);
1898
1899 let available3: HashSet<u64> = std::iter::once(passport_id).collect();
1901 assert!(req.credentials_to_prove(&available3).is_none());
1902 }
1903
1904 #[test]
1905 fn credentials_to_prove_with_constraints_enumerate() {
1906 let orb_id = 100;
1907 let passport_id = 101;
1908 let national_id = 102;
1909
1910 let req = ProofRequest {
1911 id: "req".into(),
1912 version: RequestVersion::V1,
1913 created_at: 1_735_689_600,
1914 expires_at: 1_735_689_600,
1915 rp_id: RpId::new(1),
1916 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1917 session_id: None,
1918 action: Some(test_field_element(1)),
1919 signature: test_signature(),
1920 nonce: test_nonce(),
1921 requests: vec![
1922 RequestItem {
1923 identifier: "orb".into(),
1924 issuer_schema_id: orb_id,
1925 signal: None,
1926 genesis_issued_at_min: None,
1927 expires_at_min: None,
1928 },
1929 RequestItem {
1930 identifier: "passport".into(),
1931 issuer_schema_id: passport_id,
1932 signal: None,
1933 genesis_issued_at_min: None,
1934 expires_at_min: None,
1935 },
1936 RequestItem {
1937 identifier: "national_id".into(),
1938 issuer_schema_id: national_id,
1939 signal: None,
1940 genesis_issued_at_min: None,
1941 expires_at_min: None,
1942 },
1943 ],
1944 constraints: Some(ConstraintExpr::Enumerate {
1945 enumerate: vec![
1946 ConstraintNode::Type("passport".into()),
1947 ConstraintNode::Type("national_id".into()),
1948 ],
1949 }),
1950 };
1951
1952 let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
1954 let sel1 = req.credentials_to_prove(&available1).unwrap();
1955 assert_eq!(sel1.len(), 1);
1956 assert_eq!(sel1[0].issuer_schema_id, passport_id);
1957
1958 let available2: HashSet<u64> = [orb_id, passport_id, national_id].into_iter().collect();
1960 let sel2 = req.credentials_to_prove(&available2).unwrap();
1961 assert_eq!(sel2.len(), 2);
1962 assert_eq!(sel2[0].issuer_schema_id, passport_id);
1963 assert_eq!(sel2[1].issuer_schema_id, national_id);
1964
1965 let available3: HashSet<u64> = std::iter::once(orb_id).collect();
1967 assert!(req.credentials_to_prove(&available3).is_none());
1968 }
1969
1970 #[test]
1971 fn credentials_to_prove_with_constraints_all_and_enumerate() {
1972 let orb_id = 100;
1973 let passport_id = 101;
1974 let national_id = 102;
1975
1976 let req = ProofRequest {
1977 id: "req".into(),
1978 version: RequestVersion::V1,
1979 created_at: 1_735_689_600,
1980 expires_at: 1_735_689_600,
1981 rp_id: RpId::new(1),
1982 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1983 session_id: None,
1984 action: Some(test_field_element(1)),
1985 signature: test_signature(),
1986 nonce: test_nonce(),
1987 requests: vec![
1988 RequestItem {
1989 identifier: "orb".into(),
1990 issuer_schema_id: orb_id,
1991 signal: None,
1992 genesis_issued_at_min: None,
1993 expires_at_min: None,
1994 },
1995 RequestItem {
1996 identifier: "passport".into(),
1997 issuer_schema_id: passport_id,
1998 signal: None,
1999 genesis_issued_at_min: None,
2000 expires_at_min: None,
2001 },
2002 RequestItem {
2003 identifier: "national_id".into(),
2004 issuer_schema_id: national_id,
2005 signal: None,
2006 genesis_issued_at_min: None,
2007 expires_at_min: None,
2008 },
2009 ],
2010 constraints: Some(ConstraintExpr::All {
2011 all: vec![
2012 ConstraintNode::Type("orb".into()),
2013 ConstraintNode::Expr(ConstraintExpr::Enumerate {
2014 enumerate: vec![
2015 ConstraintNode::Type("passport".into()),
2016 ConstraintNode::Type("national_id".into()),
2017 ],
2018 }),
2019 ],
2020 }),
2021 };
2022
2023 let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
2025 let sel1 = req.credentials_to_prove(&available1).unwrap();
2026 assert_eq!(sel1.len(), 2);
2027 assert_eq!(sel1[0].issuer_schema_id, orb_id);
2028 assert_eq!(sel1[1].issuer_schema_id, passport_id);
2029
2030 let available2: HashSet<u64> = [orb_id, passport_id, national_id].into_iter().collect();
2032 let sel2 = req.credentials_to_prove(&available2).unwrap();
2033 assert_eq!(sel2.len(), 3);
2034 assert_eq!(sel2[0].issuer_schema_id, orb_id);
2035 assert_eq!(sel2[1].issuer_schema_id, passport_id);
2036 assert_eq!(sel2[2].issuer_schema_id, national_id);
2037
2038 let available3: HashSet<u64> = std::iter::once(orb_id).collect();
2040 assert!(req.credentials_to_prove(&available3).is_none());
2041 }
2042
2043 #[test]
2044 fn request_item_effective_expires_at_min_defaults_to_created_at() {
2045 let request_created_at = 1_735_689_600; let custom_expires_at = 1_735_862_400; let item_with_none = RequestItem {
2050 identifier: "test".into(),
2051 issuer_schema_id: 100,
2052 signal: None,
2053 genesis_issued_at_min: None,
2054 expires_at_min: None,
2055 };
2056 assert_eq!(
2057 item_with_none.effective_expires_at_min(request_created_at),
2058 request_created_at,
2059 "When expires_at_min is None, should default to request created_at"
2060 );
2061
2062 let item_with_custom = RequestItem {
2064 identifier: "test".into(),
2065 issuer_schema_id: 100,
2066 signal: None,
2067 genesis_issued_at_min: None,
2068 expires_at_min: Some(custom_expires_at),
2069 };
2070 assert_eq!(
2071 item_with_custom.effective_expires_at_min(request_created_at),
2072 custom_expires_at,
2073 "When expires_at_min is Some, should use that explicit value"
2074 );
2075 }
2076
2077 #[test]
2078 fn validate_response_checks_expires_at_min_matches() {
2079 let request_created_at = 1_735_689_600; let custom_expires_at = 1_735_862_400; let request = ProofRequest {
2085 id: "req_expires_test".into(),
2086 version: RequestVersion::V1,
2087 created_at: request_created_at,
2088 expires_at: request_created_at + 300,
2089 rp_id: RpId::new(1),
2090 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2091 session_id: None,
2092 action: Some(test_field_element(1)),
2093 signature: test_signature(),
2094 nonce: test_nonce(),
2095 requests: vec![
2096 RequestItem {
2097 identifier: "orb".into(),
2098 issuer_schema_id: 100,
2099 signal: None,
2100 genesis_issued_at_min: None,
2101 expires_at_min: None, },
2103 RequestItem {
2104 identifier: "document".into(),
2105 issuer_schema_id: 101,
2106 signal: None,
2107 genesis_issued_at_min: None,
2108 expires_at_min: Some(custom_expires_at), },
2110 ],
2111 constraints: None,
2112 };
2113
2114 let valid_response = ProofResponse {
2116 id: "req_expires_test".into(),
2117 version: RequestVersion::V1,
2118 session_id: None,
2119 error: None,
2120 responses: vec![
2121 ResponseItem::new_uniqueness(
2122 "orb".into(),
2123 100,
2124 ZeroKnowledgeProof::default(),
2125 Nullifier::from(test_field_element(1001)),
2126 request_created_at, ),
2128 ResponseItem::new_uniqueness(
2129 "document".into(),
2130 101,
2131 ZeroKnowledgeProof::default(),
2132 Nullifier::from(test_field_element(1002)),
2133 custom_expires_at, ),
2135 ],
2136 };
2137 assert!(request.validate_response(&valid_response).is_ok());
2138
2139 let invalid_response_1 = ProofResponse {
2141 id: "req_expires_test".into(),
2142 version: RequestVersion::V1,
2143 session_id: None,
2144 error: None,
2145 responses: vec![
2146 ResponseItem::new_uniqueness(
2147 "orb".into(),
2148 100,
2149 ZeroKnowledgeProof::default(),
2150 Nullifier::from(test_field_element(1001)),
2151 custom_expires_at, ),
2153 ResponseItem::new_uniqueness(
2154 "document".into(),
2155 101,
2156 ZeroKnowledgeProof::default(),
2157 Nullifier::from(test_field_element(1002)),
2158 custom_expires_at,
2159 ),
2160 ],
2161 };
2162 let err1 = request.validate_response(&invalid_response_1).unwrap_err();
2163 assert!(matches!(
2164 err1,
2165 ValidationError::ExpiresAtMinMismatch(_, _, _)
2166 ));
2167 if let ValidationError::ExpiresAtMinMismatch(identifier, expected, got) = err1 {
2168 assert_eq!(identifier, "orb");
2169 assert_eq!(expected, request_created_at);
2170 assert_eq!(got, custom_expires_at);
2171 }
2172
2173 let invalid_response_2 = ProofResponse {
2175 id: "req_expires_test".into(),
2176 version: RequestVersion::V1,
2177 session_id: None,
2178 error: None,
2179 responses: vec![
2180 ResponseItem::new_uniqueness(
2181 "orb".into(),
2182 100,
2183 ZeroKnowledgeProof::default(),
2184 Nullifier::from(test_field_element(1001)),
2185 request_created_at,
2186 ),
2187 ResponseItem::new_uniqueness(
2188 "document".into(),
2189 101,
2190 ZeroKnowledgeProof::default(),
2191 Nullifier::from(test_field_element(1002)),
2192 request_created_at, ),
2194 ],
2195 };
2196 let err2 = request.validate_response(&invalid_response_2).unwrap_err();
2197 assert!(matches!(
2198 err2,
2199 ValidationError::ExpiresAtMinMismatch(_, _, _)
2200 ));
2201 if let ValidationError::ExpiresAtMinMismatch(identifier, expected, got) = err2 {
2202 assert_eq!(identifier, "document");
2203 assert_eq!(expected, custom_expires_at);
2204 assert_eq!(got, request_created_at);
2205 }
2206 }
2207
2208 #[test]
2209 fn computed_action_returns_explicit_action() {
2210 let action = test_field_element(42);
2211 let request = ProofRequest {
2212 id: "req".into(),
2213 version: RequestVersion::V1,
2214 created_at: 1_700_000_000,
2215 expires_at: 1_700_100_000,
2216 rp_id: RpId::new(1),
2217 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2218 session_id: None,
2219 action: Some(action),
2220 signature: test_signature(),
2221 nonce: test_nonce(),
2222 requests: vec![],
2223 constraints: None,
2224 };
2225 assert_eq!(request.computed_action(&mut rand::rngs::OsRng), action);
2226 }
2227
2228 #[test]
2229 fn computed_action_generates_random_when_none() {
2230 let request = ProofRequest {
2231 id: "req".into(),
2232 version: RequestVersion::V1,
2233 created_at: 1_700_000_000,
2234 expires_at: 1_700_100_000,
2235 rp_id: RpId::new(1),
2236 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2237 session_id: Some(test_field_element(99)),
2238 action: None,
2239 signature: test_signature(),
2240 nonce: test_nonce(),
2241 requests: vec![],
2242 constraints: None,
2243 };
2244
2245 let action1 = request.computed_action(&mut rand::rngs::OsRng);
2246 let action2 = request.computed_action(&mut rand::rngs::OsRng);
2247 assert_ne!(action1, action2);
2249 }
2250
2251 #[test]
2252 fn test_validate_response_requires_session_id_in_response() {
2253 let request = ProofRequest {
2255 id: "req_session".into(),
2256 version: RequestVersion::V1,
2257 created_at: 1_735_689_600,
2258 expires_at: 1_735_689_900,
2259 rp_id: RpId::new(1),
2260 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2261 session_id: Some(test_field_element(123)), action: Some(test_field_element(42)),
2263 signature: test_signature(),
2264 nonce: test_nonce(),
2265 requests: vec![RequestItem {
2266 identifier: "orb".into(),
2267 issuer_schema_id: 1,
2268 signal: None,
2269 genesis_issued_at_min: None,
2270 expires_at_min: None,
2271 }],
2272 constraints: None,
2273 };
2274
2275 let response_missing_session_id = ProofResponse {
2277 id: "req_session".into(),
2278 version: RequestVersion::V1,
2279 session_id: None, error: None,
2281 responses: vec![ResponseItem::new_session(
2282 "orb".into(),
2283 1,
2284 ZeroKnowledgeProof::default(),
2285 SessionNullifier::new(test_field_element(1001), test_field_element(42)),
2286 1_735_689_600,
2287 )],
2288 };
2289
2290 let err = request
2291 .validate_response(&response_missing_session_id)
2292 .unwrap_err();
2293 assert!(matches!(err, ValidationError::SessionIdMismatch));
2294 }
2295
2296 #[test]
2297 fn test_validate_response_requires_session_nullifier_for_session_proof() {
2298 let request = ProofRequest {
2300 id: "req_session".into(),
2301 version: RequestVersion::V1,
2302 created_at: 1_735_689_600,
2303 expires_at: 1_735_689_900,
2304 rp_id: RpId::new(1),
2305 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2306 session_id: Some(test_field_element(123)), action: Some(test_field_element(42)),
2308 signature: test_signature(),
2309 nonce: test_nonce(),
2310 requests: vec![RequestItem {
2311 identifier: "orb".into(),
2312 issuer_schema_id: 1,
2313 signal: None,
2314 genesis_issued_at_min: None,
2315 expires_at_min: None,
2316 }],
2317 constraints: None,
2318 };
2319
2320 let response_wrong_nullifier_type = ProofResponse {
2322 id: "req_session".into(),
2323 version: RequestVersion::V1,
2324 session_id: Some(test_field_element(123)),
2325 error: None,
2326 responses: vec![ResponseItem::new_uniqueness(
2327 "orb".into(),
2328 1,
2329 ZeroKnowledgeProof::default(),
2330 Nullifier::from(test_field_element(1001)), 1_735_689_600,
2332 )],
2333 };
2334
2335 let err = request
2336 .validate_response(&response_wrong_nullifier_type)
2337 .unwrap_err();
2338 assert!(matches!(
2339 err,
2340 ValidationError::MissingSessionNullifier(ref id) if id == "orb"
2341 ));
2342 }
2343
2344 #[test]
2345 fn test_validate_response_requires_nullifier_for_uniqueness_proof() {
2346 let request = ProofRequest {
2348 id: "req_uniqueness".into(),
2349 version: RequestVersion::V1,
2350 created_at: 1_735_689_600,
2351 expires_at: 1_735_689_900,
2352 rp_id: RpId::new(1),
2353 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2354 session_id: None, action: Some(test_field_element(42)),
2356 signature: test_signature(),
2357 nonce: test_nonce(),
2358 requests: vec![RequestItem {
2359 identifier: "orb".into(),
2360 issuer_schema_id: 1,
2361 signal: None,
2362 genesis_issued_at_min: None,
2363 expires_at_min: None,
2364 }],
2365 constraints: None,
2366 };
2367
2368 let response_wrong_nullifier_type = ProofResponse {
2370 id: "req_uniqueness".into(),
2371 version: RequestVersion::V1,
2372 session_id: None,
2373 error: None,
2374 responses: vec![ResponseItem::new_session(
2375 "orb".into(),
2376 1,
2377 ZeroKnowledgeProof::default(),
2378 SessionNullifier::new(test_field_element(1001), test_field_element(42)), 1_735_689_600,
2380 )],
2381 };
2382
2383 let err = request
2384 .validate_response(&response_wrong_nullifier_type)
2385 .unwrap_err();
2386 assert!(matches!(
2387 err,
2388 ValidationError::MissingNullifier(ref id) if id == "orb"
2389 ));
2390 }
2391}