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