1mod constraints;
6pub use constraints::{ConstraintExpr, ConstraintKind, ConstraintNode, MAX_CONSTRAINT_NODES};
7
8use crate::{
9 FieldElement, Nullifier, PrimitiveError, SessionId, SessionNullifier, ZeroKnowledgeProof,
10 rp::RpId,
11};
12use serde::{Deserialize, Serialize, de::Error as _};
13use std::collections::HashSet;
14use taceo_oprf::types::OprfKeyId;
15use uuid as _;
17
18#[repr(u8)]
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum RequestVersion {
22 V1 = 1,
24}
25
26impl serde::Serialize for RequestVersion {
27 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
28 where
29 S: serde::Serializer,
30 {
31 let v = *self as u8;
32 serializer.serialize_u8(v)
33 }
34}
35
36impl<'de> serde::Deserialize<'de> for RequestVersion {
37 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
38 where
39 D: serde::Deserializer<'de>,
40 {
41 let v = u8::deserialize(deserializer)?;
42 match v {
43 1 => Ok(Self::V1),
44 _ => Err(serde::de::Error::custom("unsupported version")),
45 }
46 }
47}
48
49#[repr(u8)]
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum ProofType {
57 #[default]
59 Uniqueness = 0x00,
60 CreateSession = 0x01,
62 Session = 0x02,
64}
65
66impl ProofType {
67 #[must_use]
69 pub const fn is_uniqueness(&self) -> bool {
70 matches!(self, Self::Uniqueness)
71 }
72
73 #[must_use]
75 pub const fn is_session(&self) -> bool {
76 matches!(self, Self::CreateSession | Self::Session)
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct ProofRequest {
84 pub id: String,
86 pub version: RequestVersion,
88 #[serde(default)]
93 pub proof_type: ProofType,
94 pub created_at: u64,
96 pub expires_at: u64,
98 pub rp_id: RpId,
100 pub oprf_key_id: OprfKeyId,
102 pub session_id: Option<SessionId>,
108 pub action: Option<FieldElement>,
113 #[serde(with = "crate::serde_utils::hex_signature")]
115 pub signature: alloy::signers::Signature,
116 pub nonce: FieldElement,
118 #[serde(rename = "proof_requests")]
120 pub requests: Vec<RequestItem>,
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub constraints: Option<ConstraintExpr<'static>>,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(deny_unknown_fields)]
129pub struct RequestItem {
130 pub identifier: String,
134
135 pub issuer_schema_id: u64,
139
140 #[serde(default, skip_serializing_if = "Option::is_none")]
152 #[serde(with = "crate::serde_utils::hex_bytes_opt")]
153 pub signal: Option<Vec<u8>>,
154
155 pub genesis_issued_at_min: Option<u64>,
161
162 pub expires_at_min: Option<u64>,
176}
177
178impl RequestItem {
179 #[must_use]
181 pub const fn new(
182 identifier: String,
183 issuer_schema_id: u64,
184 signal: Option<Vec<u8>>,
185 genesis_issued_at_min: Option<u64>,
186 expires_at_min: Option<u64>,
187 ) -> Self {
188 Self {
189 identifier,
190 issuer_schema_id,
191 signal,
192 genesis_issued_at_min,
193 expires_at_min,
194 }
195 }
196
197 #[must_use]
199 pub fn signal_hash(&self) -> FieldElement {
200 if let Some(signal) = &self.signal {
201 FieldElement::from_arbitrary_raw_bytes(signal)
202 } else {
203 FieldElement::ZERO
204 }
205 }
206
207 #[must_use]
212 pub const fn effective_expires_at_min(&self, request_created_at: u64) -> u64 {
213 match self.expires_at_min {
214 Some(value) => value,
215 None => request_created_at,
216 }
217 }
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
222#[serde(deny_unknown_fields)]
223pub struct ProofResponse {
224 pub id: String,
226 pub version: RequestVersion,
228 #[serde(skip_serializing_if = "Option::is_none")]
237 pub session_id: Option<SessionId>,
238 #[serde(skip_serializing_if = "Option::is_none")]
241 pub error: Option<String>,
242 pub responses: Vec<ResponseItem>,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
262#[serde(deny_unknown_fields)]
263pub struct ResponseItem {
264 pub identifier: String,
268
269 pub issuer_schema_id: u64,
271
272 pub proof: ZeroKnowledgeProof,
274
275 #[serde(skip_serializing_if = "Option::is_none")]
279 pub nullifier: Option<Nullifier>,
280
281 #[serde(skip_serializing_if = "Option::is_none")]
285 pub session_nullifier: Option<SessionNullifier>,
286
287 pub expires_at_min: u64,
291}
292
293impl ProofResponse {
294 #[must_use]
297 pub fn constraints_satisfied(&self, constraints: &ConstraintExpr<'_>) -> bool {
298 if self.error.is_some() {
300 return false;
301 }
302
303 let provided: HashSet<&str> = self
304 .responses
305 .iter()
306 .map(|item| item.identifier.as_str())
307 .collect();
308
309 constraints.evaluate(&|t| provided.contains(t))
310 }
311}
312
313impl ResponseItem {
314 #[must_use]
316 pub const fn new_uniqueness(
317 identifier: String,
318 issuer_schema_id: u64,
319 proof: ZeroKnowledgeProof,
320 nullifier: Nullifier,
321 expires_at_min: u64,
322 ) -> Self {
323 Self {
324 identifier,
325 issuer_schema_id,
326 proof,
327 nullifier: Some(nullifier),
328 session_nullifier: None,
329 expires_at_min,
330 }
331 }
332
333 #[must_use]
335 pub const fn new_session(
336 identifier: String,
337 issuer_schema_id: u64,
338 proof: ZeroKnowledgeProof,
339 session_nullifier: SessionNullifier,
340 expires_at_min: u64,
341 ) -> Self {
342 Self {
343 identifier,
344 issuer_schema_id,
345 proof,
346 nullifier: None,
347 session_nullifier: Some(session_nullifier),
348 expires_at_min,
349 }
350 }
351
352 #[must_use]
354 pub const fn is_session(&self) -> bool {
355 self.session_nullifier.is_some()
356 }
357
358 #[must_use]
360 pub const fn is_uniqueness(&self) -> bool {
361 self.nullifier.is_some()
362 }
363}
364
365impl ProofRequest {
366 #[must_use]
374 pub fn credentials_to_prove(&self, available: &HashSet<u64>) -> Option<Vec<&RequestItem>> {
375 let available_identifiers: HashSet<&str> = self
377 .requests
378 .iter()
379 .filter(|r| available.contains(&r.issuer_schema_id))
380 .map(|r| r.identifier.as_str())
381 .collect();
382
383 let is_selectable = |identifier: &str| available_identifiers.contains(identifier);
384
385 if self.constraints.is_none() {
387 return if self
388 .requests
389 .iter()
390 .all(|r| available.contains(&r.issuer_schema_id))
391 {
392 Some(self.requests.iter().collect())
393 } else {
394 None
395 };
396 }
397
398 let selected_identifiers = select_expr(self.constraints.as_ref().unwrap(), &is_selectable)?;
400 let selected_set: HashSet<&str> = selected_identifiers.into_iter().collect();
401
402 let result: Vec<&RequestItem> = self
404 .requests
405 .iter()
406 .filter(|r| selected_set.contains(r.identifier.as_str()))
407 .collect();
408 Some(result)
409 }
410
411 #[must_use]
413 pub fn find_request_by_issuer_schema_id(&self, issuer_schema_id: u64) -> Option<&RequestItem> {
414 self.requests
415 .iter()
416 .find(|r| r.issuer_schema_id == issuer_schema_id)
417 }
418
419 #[must_use]
421 pub const fn is_expired(&self, now: u64) -> bool {
422 now > self.expires_at
423 }
424
425 pub fn digest_hash(&self) -> Result<[u8; 32], PrimitiveError> {
439 use crate::rp::compute_rp_signature_msg;
440 use sha2::{Digest, Sha256};
441
442 let msg = compute_rp_signature_msg(
443 *self.nonce,
444 self.created_at,
445 self.expires_at,
446 self.action.map(|v| *v),
447 );
448 let mut hasher = Sha256::new();
449 hasher.update(&msg);
450 Ok(hasher.finalize().into())
451 }
452
453 pub fn validate_proof_type(&self) -> Result<(), PrimitiveError> {
462 match self.proof_type {
463 ProofType::Uniqueness => {
464 if self.session_id.is_some() {
465 return Err(PrimitiveError::InvalidInput {
466 attribute: "session_id".to_string(),
467 reason: "must be omitted for uniqueness proofs".to_string(),
468 });
469 }
470 }
471 ProofType::CreateSession => {
472 if self.session_id.is_some() {
473 return Err(PrimitiveError::InvalidInput {
474 attribute: "session_id".to_string(),
475 reason: "must be omitted when creating a session".to_string(),
476 });
477 }
478 if self.action.is_some() {
479 return Err(PrimitiveError::InvalidInput {
480 attribute: "action".to_string(),
481 reason: "must be omitted for session proofs".to_string(),
482 });
483 }
484 }
485 ProofType::Session => {
486 if self.session_id.is_none() {
487 return Err(PrimitiveError::InvalidInput {
488 attribute: "session_id".to_string(),
489 reason: "must be provided when proving a session".to_string(),
490 });
491 }
492 if self.action.is_some() {
493 return Err(PrimitiveError::InvalidInput {
494 attribute: "action".to_string(),
495 reason: "must be omitted for session proofs".to_string(),
496 });
497 }
498 }
499 }
500 Ok(())
501 }
502
503 #[must_use]
505 pub const fn is_session_proof(&self) -> bool {
506 self.proof_type.is_session()
507 }
508
509 #[must_use]
511 pub const fn is_create_session(&self) -> bool {
512 matches!(self.proof_type, ProofType::CreateSession)
513 }
514
515 pub fn validate_constraints(&self) -> Result<(), ValidationError> {
528 if let Some(expr) = &self.constraints {
529 if !expr.validate_max_depth(2) {
530 return Err(ValidationError::ConstraintTooDeep);
531 }
532 if !expr.validate_max_nodes(MAX_CONSTRAINT_NODES) {
533 return Err(ValidationError::ConstraintTooLarge);
534 }
535 }
536 Ok(())
537 }
538
539 pub fn validate_response(&self, response: &ProofResponse) -> Result<(), ValidationError> {
545 self.validate_proof_type()
546 .map_err(|err| ValidationError::InvalidProofRequest(err.to_string()))?;
547
548 if self.id != response.id {
550 return Err(ValidationError::RequestIdMismatch);
551 }
552 if self.version != response.version {
553 return Err(ValidationError::VersionMismatch);
554 }
555
556 if let Some(error) = &response.error {
558 return Err(ValidationError::ProofGenerationFailed(error.clone()));
559 }
560
561 match self.proof_type {
562 ProofType::Uniqueness => {
563 if response.session_id.is_some() {
564 return Err(ValidationError::UnexpectedSessionId);
565 }
566 }
567 ProofType::CreateSession => {
568 if response.session_id.is_none() {
569 return Err(ValidationError::MissingSessionId);
570 }
571 }
572 ProofType::Session => {
573 if self.session_id != response.session_id {
574 return Err(ValidationError::SessionIdMismatch);
575 }
576 }
577 }
578
579 let mut provided: HashSet<&str> = HashSet::new();
581 for response_item in &response.responses {
582 if !provided.insert(response_item.identifier.as_str()) {
583 return Err(ValidationError::DuplicateCredential(
584 response_item.identifier.clone(),
585 ));
586 }
587
588 let request_item = self
589 .requests
590 .iter()
591 .find(|r| r.identifier == response_item.identifier)
592 .ok_or_else(|| {
593 ValidationError::UnexpectedCredential(response_item.identifier.clone())
594 })?;
595
596 if self.is_session_proof() {
597 if response_item.session_nullifier.is_none() {
599 return Err(ValidationError::MissingSessionNullifier(
600 response_item.identifier.clone(),
601 ));
602 }
603 } else {
604 if response_item.nullifier.is_none() {
606 return Err(ValidationError::MissingNullifier(
607 response_item.identifier.clone(),
608 ));
609 }
610 }
611
612 let expected_expires_at_min = request_item.effective_expires_at_min(self.created_at);
613 if response_item.expires_at_min != expected_expires_at_min {
614 return Err(ValidationError::ExpiresAtMinMismatch(
615 response_item.identifier.clone(),
616 expected_expires_at_min,
617 response_item.expires_at_min,
618 ));
619 }
620 }
621
622 match &self.constraints {
623 None => {
625 for req in &self.requests {
626 if !provided.contains(req.identifier.as_str()) {
627 return Err(ValidationError::MissingCredential(req.identifier.clone()));
628 }
629 }
630 Ok(())
631 }
632 Some(expr) => {
633 self.validate_constraints()?;
634 if expr.evaluate(&|t| provided.contains(t)) {
635 Ok(())
636 } else {
637 Err(ValidationError::ConstraintNotSatisfied)
638 }
639 }
640 }
641 }
642
643 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
648 let v: Self = serde_json::from_str(json)?;
649 v.validate_proof_type().map_err(serde_json::Error::custom)?;
650 let mut seen: HashSet<String> = HashSet::new();
652 for r in &v.requests {
653 let t = r.issuer_schema_id.to_string();
654 if !seen.insert(t.clone()) {
655 return Err(serde_json::Error::custom(format!(
656 "duplicate issuer schema id: {t}"
657 )));
658 }
659 }
660 Ok(v)
661 }
662
663 pub fn to_json(&self) -> Result<String, serde_json::Error> {
668 serde_json::to_string(self)
669 }
670
671 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
676 serde_json::to_string_pretty(self)
677 }
678}
679
680impl ProofResponse {
681 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
686 serde_json::from_str(json)
687 }
688
689 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
694 serde_json::to_string_pretty(self)
695 }
696
697 #[must_use]
700 pub fn successful_credentials(&self) -> Vec<u64> {
701 if self.error.is_some() {
702 return vec![];
703 }
704 self.responses.iter().map(|r| r.issuer_schema_id).collect()
705 }
706}
707
708#[derive(Debug, thiserror::Error, PartialEq, Eq)]
710pub enum ValidationError {
711 #[error("Invalid proof request: {0}")]
713 InvalidProofRequest(String),
714 #[error("Request ID mismatch")]
716 RequestIdMismatch,
717 #[error("Version mismatch")]
719 VersionMismatch,
720 #[error("Proof generation failed: {0}")]
722 ProofGenerationFailed(String),
723 #[error("Missing required credential: {0}")]
725 MissingCredential(String),
726 #[error("Unexpected credential in response: {0}")]
728 UnexpectedCredential(String),
729 #[error("Duplicate credential in response: {0}")]
731 DuplicateCredential(String),
732 #[error("Constraints not satisfied")]
734 ConstraintNotSatisfied,
735 #[error("Constraints nesting exceeds maximum allowed depth")]
737 ConstraintTooDeep,
738 #[error("Constraints exceed maximum allowed size")]
740 ConstraintTooLarge,
741 #[error("Invalid expires_at_min for credential '{0}': expected {1}, got {2}")]
743 ExpiresAtMinMismatch(String, u64, u64),
744 #[error("Session ID doesn't match between request and response")]
746 SessionIdMismatch,
747 #[error("Session ID missing from session response")]
749 MissingSessionId,
750 #[error("Session ID present in uniqueness response")]
752 UnexpectedSessionId,
753 #[error("Session nullifier missing for credential: {0}")]
755 MissingSessionNullifier(String),
756 #[error("Nullifier missing for credential: {0}")]
758 MissingNullifier(String),
759}
760
761fn select_node<'a, F>(node: &'a ConstraintNode<'a>, pred: &F) -> Option<Vec<&'a str>>
763where
764 F: Fn(&str) -> bool,
765{
766 match node {
767 ConstraintNode::Type(t) => pred(t.as_ref()).then(|| vec![t.as_ref()]),
768 ConstraintNode::Expr(e) => select_expr(e, pred),
769 }
770}
771
772fn select_expr<'a, F>(expr: &'a ConstraintExpr<'a>, pred: &F) -> Option<Vec<&'a str>>
773where
774 F: Fn(&str) -> bool,
775{
776 match expr {
777 ConstraintExpr::All { all } => {
778 let mut seen: std::collections::HashSet<&'a str> = std::collections::HashSet::new();
779 let mut out: Vec<&'a str> = Vec::new();
780 for n in all {
781 let sub = select_node(n, pred)?;
782 for s in sub {
783 if seen.insert(s) {
784 out.push(s);
785 }
786 }
787 }
788 Some(out)
789 }
790 ConstraintExpr::Any { any } => any.iter().find_map(|n| select_node(n, pred)),
791 ConstraintExpr::Enumerate { enumerate } => {
792 let mut seen: std::collections::HashSet<&'a str> = std::collections::HashSet::new();
794 let mut selected: Vec<&'a str> = Vec::new();
795
796 for child in enumerate {
798 let Some(child_selection) = select_node(child, pred) else {
799 continue;
800 };
801
802 for identifier in child_selection {
803 if seen.insert(identifier) {
804 selected.push(identifier);
805 }
806 }
807 }
808
809 if selected.is_empty() {
810 None
811 } else {
812 Some(selected)
813 }
814 }
815 }
816}
817
818#[cfg(test)]
819mod tests {
820 use super::*;
821 use crate::SessionNullifier;
822 use alloy::{
823 signers::{SignerSync, local::PrivateKeySigner},
824 uint,
825 };
826 use k256::ecdsa::SigningKey;
827
828 fn test_signature() -> alloy::signers::Signature {
830 let signer =
831 PrivateKeySigner::from_signing_key(SigningKey::from_bytes(&[1u8; 32].into()).unwrap());
832 signer.sign_message_sync(b"test").expect("can sign")
833 }
834
835 fn test_nonce() -> FieldElement {
836 FieldElement::from(1u64)
837 }
838
839 fn test_field_element(n: u64) -> FieldElement {
840 FieldElement::from(n)
841 }
842
843 fn test_action(n: u64) -> FieldElement {
845 use ruint::{aliases::U256, uint};
846 let v = U256::from(n)
847 | uint!(0x0200000000000000000000000000000000000000000000000000000000000000_U256);
848 FieldElement::try_from(v).expect("test value fits in field")
849 }
850
851 #[test]
852 fn constraints_all_any_nested() {
853 let response = ProofResponse {
855 id: "req_123".into(),
856 version: RequestVersion::V1,
857 session_id: None,
858 error: None,
859 responses: vec![
860 ResponseItem::new_uniqueness(
861 "test_req_1".into(),
862 1,
863 ZeroKnowledgeProof::default(),
864 test_field_element(1001).into(),
865 1_735_689_600,
866 ),
867 ResponseItem::new_uniqueness(
868 "test_req_2".into(),
869 2,
870 ZeroKnowledgeProof::default(),
871 test_field_element(1002).into(),
872 1_735_689_600,
873 ),
874 ],
875 };
876
877 let expr = ConstraintExpr::All {
879 all: vec![
880 ConstraintNode::Type("test_req_1".into()),
881 ConstraintNode::Expr(ConstraintExpr::Any {
882 any: vec![
883 ConstraintNode::Type("test_req_2".into()),
884 ConstraintNode::Type("test_req_4".into()),
885 ],
886 }),
887 ],
888 };
889
890 assert!(response.constraints_satisfied(&expr));
891
892 let fail_expr = ConstraintExpr::All {
894 all: vec![
895 ConstraintNode::Type("test_req_1".into()),
896 ConstraintNode::Type("test_req_3".into()),
897 ],
898 };
899 assert!(!response.constraints_satisfied(&fail_expr));
900 }
901
902 #[test]
903 fn constraints_enumerate_partial_and_empty() {
904 let response = ProofResponse {
906 id: "req_123".into(),
907 version: RequestVersion::V1,
908 session_id: None,
909 error: None,
910 responses: vec![
911 ResponseItem::new_uniqueness(
912 "orb".into(),
913 1,
914 ZeroKnowledgeProof::default(),
915 Nullifier::from(test_field_element(1001)),
916 1_735_689_600,
917 ),
918 ResponseItem::new_uniqueness(
919 "passport".into(),
920 2,
921 ZeroKnowledgeProof::default(),
922 Nullifier::from(test_field_element(1002)),
923 1_735_689_600,
924 ),
925 ],
926 };
927
928 let expr = ConstraintExpr::Enumerate {
930 enumerate: vec![
931 ConstraintNode::Type("passport".into()),
932 ConstraintNode::Type("national_id".into()),
933 ],
934 };
935 assert!(response.constraints_satisfied(&expr));
936
937 let fail_expr = ConstraintExpr::Enumerate {
939 enumerate: vec![
940 ConstraintNode::Type("national_id".into()),
941 ConstraintNode::Type("document".into()),
942 ],
943 };
944 assert!(!response.constraints_satisfied(&fail_expr));
945 }
946
947 #[test]
948 fn test_digest_hash() {
949 let request = ProofRequest {
950 id: "test_request".into(),
951 version: RequestVersion::V1,
952 proof_type: ProofType::Uniqueness,
953 session_id: None,
954 action: Some(FieldElement::ZERO),
955 created_at: 1_700_000_000,
956 expires_at: 1_700_100_000,
957 rp_id: RpId::new(1),
958 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
959 signature: test_signature(),
960 nonce: test_nonce(),
961 requests: vec![RequestItem {
962 identifier: "orb".into(),
963 issuer_schema_id: 1,
964 signal: Some("test_signal".into()),
965 genesis_issued_at_min: None,
966 expires_at_min: None,
967 }],
968 constraints: None,
969 };
970
971 let digest1 = request.digest_hash().unwrap();
972 assert_eq!(digest1.len(), 32);
974
975 let digest2 = request.digest_hash().unwrap();
977 assert_eq!(digest1, digest2);
978
979 let request2 = ProofRequest {
981 nonce: test_field_element(3),
982 ..request
983 };
984 let digest3 = request2.digest_hash().unwrap();
985 assert_ne!(digest1, digest3);
986 }
987
988 #[test]
989 fn proof_request_signature_serializes_as_hex_string() {
990 let request = ProofRequest {
991 id: "test".into(),
992 version: RequestVersion::V1,
993 proof_type: ProofType::Uniqueness,
994 session_id: None,
995 action: Some(FieldElement::ZERO),
996 created_at: 1_700_000_000,
997 expires_at: 1_700_100_000,
998 rp_id: RpId::new(1),
999 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1000 signature: test_signature(),
1001 nonce: test_nonce(),
1002 requests: vec![RequestItem {
1003 identifier: "orb".into(),
1004 issuer_schema_id: 1,
1005 signal: None,
1006 genesis_issued_at_min: None,
1007 expires_at_min: None,
1008 }],
1009 constraints: None,
1010 };
1011
1012 let json = request.to_json().unwrap();
1013 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1014 assert_eq!(value["proof_type"], "uniqueness");
1015 assert_eq!(
1016 value["action"],
1017 serde_json::to_value(FieldElement::ZERO).unwrap()
1018 );
1019 assert!(value.get("kind").is_none());
1020 let sig = value["signature"]
1021 .as_str()
1022 .expect("signature should be a string");
1023 assert!(sig.starts_with("0x"));
1024 assert_eq!(sig.len(), 132);
1025
1026 let roundtripped = ProofRequest::from_json(&json).unwrap();
1027 assert_eq!(roundtripped.signature, request.signature);
1028 }
1029
1030 #[test]
1031 fn request_validate_response_none_constraints_means_all() {
1032 let request = ProofRequest {
1033 id: "req_1".into(),
1034 version: RequestVersion::V1,
1035 proof_type: ProofType::Uniqueness,
1036 session_id: None,
1037 action: Some(FieldElement::ZERO),
1038 created_at: 1_735_689_600,
1039 expires_at: 1_735_689_600, rp_id: RpId::new(1),
1041 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1042 signature: test_signature(),
1043 nonce: test_nonce(),
1044 requests: vec![
1045 RequestItem {
1046 identifier: "orb".into(),
1047 issuer_schema_id: 1,
1048 signal: None,
1049 genesis_issued_at_min: None,
1050 expires_at_min: None,
1051 },
1052 RequestItem {
1053 identifier: "document".into(),
1054 issuer_schema_id: 2,
1055 signal: None,
1056 genesis_issued_at_min: None,
1057 expires_at_min: None,
1058 },
1059 ],
1060 constraints: None,
1061 };
1062
1063 let ok = ProofResponse {
1064 id: "req_1".into(),
1065 version: RequestVersion::V1,
1066 session_id: None,
1067 error: None,
1068 responses: vec![
1069 ResponseItem::new_uniqueness(
1070 "orb".into(),
1071 1,
1072 ZeroKnowledgeProof::default(),
1073 Nullifier::from(test_field_element(1001)),
1074 1_735_689_600,
1075 ),
1076 ResponseItem::new_uniqueness(
1077 "document".into(),
1078 2,
1079 ZeroKnowledgeProof::default(),
1080 Nullifier::from(test_field_element(1002)),
1081 1_735_689_600,
1082 ),
1083 ],
1084 };
1085 assert!(request.validate_response(&ok).is_ok());
1086
1087 let missing = ProofResponse {
1088 id: "req_1".into(),
1089 version: RequestVersion::V1,
1090 session_id: None,
1091 error: None,
1092 responses: vec![ResponseItem::new_uniqueness(
1093 "orb".into(),
1094 1,
1095 ZeroKnowledgeProof::default(),
1096 Nullifier::from(test_field_element(1001)),
1097 1_735_689_600,
1098 )],
1099 };
1100 let err = request.validate_response(&missing).unwrap_err();
1101 assert!(matches!(err, ValidationError::MissingCredential(_)));
1102
1103 let unexpected = ProofResponse {
1104 id: "req_1".into(),
1105 version: RequestVersion::V1,
1106 session_id: None,
1107 error: None,
1108 responses: vec![
1109 ResponseItem::new_uniqueness(
1110 "orb".into(),
1111 1,
1112 ZeroKnowledgeProof::default(),
1113 Nullifier::from(test_field_element(1001)),
1114 1_735_689_600,
1115 ),
1116 ResponseItem::new_uniqueness(
1117 "document".into(),
1118 2,
1119 ZeroKnowledgeProof::default(),
1120 Nullifier::from(test_field_element(1002)),
1121 1_735_689_600,
1122 ),
1123 ResponseItem::new_uniqueness(
1124 "passport".into(),
1125 3,
1126 ZeroKnowledgeProof::default(),
1127 Nullifier::from(test_field_element(1003)),
1128 1_735_689_600,
1129 ),
1130 ],
1131 };
1132 let err = request.validate_response(&unexpected).unwrap_err();
1133 assert!(matches!(
1134 err,
1135 ValidationError::UnexpectedCredential(ref id) if id == "passport"
1136 ));
1137
1138 let duplicate = ProofResponse {
1139 id: "req_1".into(),
1140 version: RequestVersion::V1,
1141 session_id: None,
1142 error: None,
1143 responses: vec![
1144 ResponseItem::new_uniqueness(
1145 "orb".into(),
1146 1,
1147 ZeroKnowledgeProof::default(),
1148 Nullifier::from(test_field_element(1001)),
1149 1_735_689_600,
1150 ),
1151 ResponseItem::new_uniqueness(
1152 "orb".into(),
1153 1,
1154 ZeroKnowledgeProof::default(),
1155 Nullifier::from(test_field_element(1001)),
1156 1_735_689_600,
1157 ),
1158 ],
1159 };
1160 let err = request.validate_response(&duplicate).unwrap_err();
1161 assert!(matches!(
1162 err,
1163 ValidationError::DuplicateCredential(ref id) if id == "orb"
1164 ));
1165 }
1166
1167 #[test]
1168 fn constraint_depth_enforced() {
1169 let deep = ConstraintExpr::All {
1171 all: vec![ConstraintNode::Expr(ConstraintExpr::Any {
1172 any: vec![ConstraintNode::Expr(ConstraintExpr::All {
1173 all: vec![ConstraintNode::Type("orb".into())],
1174 })],
1175 })],
1176 };
1177
1178 let request = ProofRequest {
1179 id: "req_2".into(),
1180 version: RequestVersion::V1,
1181 proof_type: ProofType::Uniqueness,
1182 session_id: None,
1183 action: Some(test_field_element(1)),
1184 created_at: 1_735_689_600,
1185 expires_at: 1_735_689_600,
1186 rp_id: RpId::new(1),
1187 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1188 signature: test_signature(),
1189 nonce: test_nonce(),
1190 requests: vec![RequestItem {
1191 identifier: "orb".into(),
1192 issuer_schema_id: 1,
1193 signal: None,
1194 genesis_issued_at_min: None,
1195 expires_at_min: None,
1196 }],
1197 constraints: Some(deep),
1198 };
1199
1200 let response = ProofResponse {
1201 id: "req_2".into(),
1202 version: RequestVersion::V1,
1203 session_id: None,
1204 error: None,
1205 responses: vec![ResponseItem::new_uniqueness(
1206 "orb".into(),
1207 1,
1208 ZeroKnowledgeProof::default(),
1209 Nullifier::from(test_field_element(1001)),
1210 1_735_689_600,
1211 )],
1212 };
1213
1214 let err = request.validate_response(&response).unwrap_err();
1215 assert!(matches!(err, ValidationError::ConstraintTooDeep));
1216 }
1217
1218 #[test]
1219 #[allow(clippy::too_many_lines)]
1220 fn constraint_node_limit_boundary_passes() {
1221 let expr = ConstraintExpr::All {
1225 all: vec![
1226 ConstraintNode::Type("test_req_10".into()),
1227 ConstraintNode::Expr(ConstraintExpr::Any {
1228 any: vec![
1229 ConstraintNode::Type("test_req_11".into()),
1230 ConstraintNode::Type("test_req_12".into()),
1231 ConstraintNode::Type("test_req_13".into()),
1232 ConstraintNode::Type("test_req_14".into()),
1233 ],
1234 }),
1235 ConstraintNode::Expr(ConstraintExpr::Any {
1236 any: vec![
1237 ConstraintNode::Type("test_req_15".into()),
1238 ConstraintNode::Type("test_req_16".into()),
1239 ConstraintNode::Type("test_req_17".into()),
1240 ConstraintNode::Type("test_req_18".into()),
1241 ],
1242 }),
1243 ],
1244 };
1245
1246 let request = ProofRequest {
1247 id: "req_nodes_ok".into(),
1248 version: RequestVersion::V1,
1249 proof_type: ProofType::Uniqueness,
1250 session_id: None,
1251 action: Some(test_field_element(5)),
1252 created_at: 1_735_689_600,
1253 expires_at: 1_735_689_600,
1254 rp_id: RpId::new(1),
1255 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1256 signature: test_signature(),
1257 nonce: test_nonce(),
1258 requests: vec![
1259 RequestItem {
1260 identifier: "test_req_10".into(),
1261 issuer_schema_id: 10,
1262 signal: None,
1263 genesis_issued_at_min: None,
1264 expires_at_min: None,
1265 },
1266 RequestItem {
1267 identifier: "test_req_11".into(),
1268 issuer_schema_id: 11,
1269 signal: None,
1270 genesis_issued_at_min: None,
1271 expires_at_min: None,
1272 },
1273 RequestItem {
1274 identifier: "test_req_12".into(),
1275 issuer_schema_id: 12,
1276 signal: None,
1277 genesis_issued_at_min: None,
1278 expires_at_min: None,
1279 },
1280 RequestItem {
1281 identifier: "test_req_13".into(),
1282 issuer_schema_id: 13,
1283 signal: None,
1284 genesis_issued_at_min: None,
1285 expires_at_min: None,
1286 },
1287 RequestItem {
1288 identifier: "test_req_14".into(),
1289 issuer_schema_id: 14,
1290 signal: None,
1291 genesis_issued_at_min: None,
1292 expires_at_min: None,
1293 },
1294 RequestItem {
1295 identifier: "test_req_15".into(),
1296 issuer_schema_id: 15,
1297 signal: None,
1298 genesis_issued_at_min: None,
1299 expires_at_min: None,
1300 },
1301 RequestItem {
1302 identifier: "test_req_16".into(),
1303 issuer_schema_id: 16,
1304 signal: None,
1305 genesis_issued_at_min: None,
1306 expires_at_min: None,
1307 },
1308 RequestItem {
1309 identifier: "test_req_17".into(),
1310 issuer_schema_id: 17,
1311 signal: None,
1312 genesis_issued_at_min: None,
1313 expires_at_min: None,
1314 },
1315 RequestItem {
1316 identifier: "test_req_18".into(),
1317 issuer_schema_id: 18,
1318 signal: None,
1319 genesis_issued_at_min: None,
1320 expires_at_min: None,
1321 },
1322 ],
1323 constraints: Some(expr),
1324 };
1325
1326 let response = ProofResponse {
1328 id: "req_nodes_ok".into(),
1329 version: RequestVersion::V1,
1330 session_id: None,
1331 error: None,
1332 responses: vec![
1333 ResponseItem::new_uniqueness(
1334 "test_req_10".into(),
1335 10,
1336 ZeroKnowledgeProof::default(),
1337 Nullifier::from(test_field_element(1010)),
1338 1_735_689_600,
1339 ),
1340 ResponseItem::new_uniqueness(
1341 "test_req_11".into(),
1342 11,
1343 ZeroKnowledgeProof::default(),
1344 Nullifier::from(test_field_element(1011)),
1345 1_735_689_600,
1346 ),
1347 ResponseItem::new_uniqueness(
1348 "test_req_15".into(),
1349 15,
1350 ZeroKnowledgeProof::default(),
1351 Nullifier::from(test_field_element(1015)),
1352 1_735_689_600,
1353 ),
1354 ],
1355 };
1356
1357 assert!(request.validate_response(&response).is_ok());
1359 }
1360
1361 #[test]
1362 #[allow(clippy::too_many_lines)]
1363 fn constraint_node_limit_exceeded_fails() {
1364 let expr = ConstraintExpr::All {
1367 all: vec![
1368 ConstraintNode::Type("t0".into()),
1369 ConstraintNode::Expr(ConstraintExpr::Any {
1370 any: vec![
1371 ConstraintNode::Type("t1".into()),
1372 ConstraintNode::Type("t2".into()),
1373 ConstraintNode::Type("t3".into()),
1374 ConstraintNode::Type("t4".into()),
1375 ],
1376 }),
1377 ConstraintNode::Expr(ConstraintExpr::Any {
1378 any: vec![
1379 ConstraintNode::Type("t5".into()),
1380 ConstraintNode::Type("t6".into()),
1381 ConstraintNode::Type("t7".into()),
1382 ConstraintNode::Type("t8".into()),
1383 ConstraintNode::Type("t9".into()),
1384 ],
1385 }),
1386 ],
1387 };
1388
1389 let request = ProofRequest {
1390 id: "req_nodes_too_many".into(),
1391 version: RequestVersion::V1,
1392 proof_type: ProofType::Uniqueness,
1393 session_id: None,
1394 action: Some(test_field_element(1)),
1395 created_at: 1_735_689_600,
1396 expires_at: 1_735_689_600,
1397 rp_id: RpId::new(1),
1398 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1399 signature: test_signature(),
1400 nonce: test_nonce(),
1401 requests: vec![
1402 RequestItem {
1403 identifier: "test_req_20".into(),
1404 issuer_schema_id: 20,
1405 signal: None,
1406 genesis_issued_at_min: None,
1407 expires_at_min: None,
1408 },
1409 RequestItem {
1410 identifier: "test_req_21".into(),
1411 issuer_schema_id: 21,
1412 signal: None,
1413 genesis_issued_at_min: None,
1414 expires_at_min: None,
1415 },
1416 RequestItem {
1417 identifier: "test_req_22".into(),
1418 issuer_schema_id: 22,
1419 signal: None,
1420 genesis_issued_at_min: None,
1421 expires_at_min: None,
1422 },
1423 RequestItem {
1424 identifier: "test_req_23".into(),
1425 issuer_schema_id: 23,
1426 signal: None,
1427 genesis_issued_at_min: None,
1428 expires_at_min: None,
1429 },
1430 RequestItem {
1431 identifier: "test_req_24".into(),
1432 issuer_schema_id: 24,
1433 signal: None,
1434 genesis_issued_at_min: None,
1435 expires_at_min: None,
1436 },
1437 RequestItem {
1438 identifier: "test_req_25".into(),
1439 issuer_schema_id: 25,
1440 signal: None,
1441 genesis_issued_at_min: None,
1442 expires_at_min: None,
1443 },
1444 RequestItem {
1445 identifier: "test_req_26".into(),
1446 issuer_schema_id: 26,
1447 signal: None,
1448 genesis_issued_at_min: None,
1449 expires_at_min: None,
1450 },
1451 RequestItem {
1452 identifier: "test_req_27".into(),
1453 issuer_schema_id: 27,
1454 signal: None,
1455 genesis_issued_at_min: None,
1456 expires_at_min: None,
1457 },
1458 RequestItem {
1459 identifier: "test_req_28".into(),
1460 issuer_schema_id: 28,
1461 signal: None,
1462 genesis_issued_at_min: None,
1463 expires_at_min: None,
1464 },
1465 RequestItem {
1466 identifier: "test_req_29".into(),
1467 issuer_schema_id: 29,
1468 signal: None,
1469 genesis_issued_at_min: None,
1470 expires_at_min: None,
1471 },
1472 ],
1473 constraints: Some(expr),
1474 };
1475
1476 let response = ProofResponse {
1478 id: "req_nodes_too_many".into(),
1479 version: RequestVersion::V1,
1480 session_id: None,
1481 error: None,
1482 responses: vec![ResponseItem::new_uniqueness(
1483 "test_req_20".into(),
1484 20,
1485 ZeroKnowledgeProof::default(),
1486 Nullifier::from(test_field_element(1020)),
1487 1_735_689_600,
1488 )],
1489 };
1490
1491 let err = request.validate_response(&response).unwrap_err();
1492 assert!(matches!(err, ValidationError::ConstraintTooLarge));
1493 }
1494
1495 #[test]
1496 fn request_single_credential_parse_and_validate() {
1497 let req = ProofRequest {
1498 id: "req_18c0f7f03e7d".into(),
1499 version: RequestVersion::V1,
1500 proof_type: ProofType::Session,
1501 session_id: Some(SessionId::default()),
1502 action: None,
1503 created_at: 1_725_381_192,
1504 expires_at: 1_725_381_492,
1505 rp_id: RpId::new(1),
1506 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1507 signature: test_signature(),
1508 nonce: test_nonce(),
1509 requests: vec![RequestItem {
1510 identifier: "test_req_1".into(),
1511 issuer_schema_id: 1,
1512 signal: Some("abcd-efgh-ijkl".into()),
1513 genesis_issued_at_min: Some(1_725_381_192),
1514 expires_at_min: None,
1515 }],
1516 constraints: None,
1517 };
1518
1519 assert_eq!(req.id, "req_18c0f7f03e7d");
1520 assert_eq!(req.requests.len(), 1);
1521
1522 let resp = ProofResponse {
1524 id: req.id.clone(),
1525 version: RequestVersion::V1,
1526 session_id: Some(SessionId::default()),
1527 error: None,
1528 responses: vec![ResponseItem::new_session(
1529 "test_req_1".into(),
1530 1,
1531 ZeroKnowledgeProof::default(),
1532 SessionNullifier::new(test_field_element(1001), test_action(1)).unwrap(),
1533 1_725_381_192,
1534 )],
1535 };
1536 assert!(req.validate_response(&resp).is_ok());
1537 }
1538
1539 #[test]
1540 fn request_multiple_credentials_all_constraint_and_missing() {
1541 let req = ProofRequest {
1542 id: "req_18c0f7f03e7d".into(),
1543 version: RequestVersion::V1,
1544 proof_type: ProofType::Uniqueness,
1545 session_id: None,
1546 action: Some(test_field_element(1)),
1547 created_at: 1_725_381_192,
1548 expires_at: 1_725_381_492,
1549 rp_id: RpId::new(1),
1550 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1551 signature: test_signature(),
1552 nonce: test_nonce(),
1553 requests: vec![
1554 RequestItem {
1555 identifier: "test_req_1".into(),
1556 issuer_schema_id: 1,
1557 signal: Some("abcd-efgh-ijkl".into()),
1558 genesis_issued_at_min: Some(1_725_381_192),
1559 expires_at_min: None,
1560 },
1561 RequestItem {
1562 identifier: "test_req_2".into(),
1563 issuer_schema_id: 2,
1564 signal: Some("abcd-efgh-ijkl".into()),
1565 genesis_issued_at_min: Some(1_725_381_192),
1566 expires_at_min: None,
1567 },
1568 ],
1569 constraints: Some(ConstraintExpr::All {
1570 all: vec![
1571 ConstraintNode::Type("test_req_1".into()),
1572 ConstraintNode::Type("test_req_2".into()),
1573 ],
1574 }),
1575 };
1576
1577 let resp = ProofResponse {
1579 id: req.id.clone(),
1580 version: RequestVersion::V1,
1581 session_id: None,
1582 error: None,
1583 responses: vec![ResponseItem::new_uniqueness(
1584 "test_req_2".into(),
1585 2,
1586 ZeroKnowledgeProof::default(),
1587 Nullifier::from(test_field_element(1001)),
1588 1_725_381_192,
1589 )],
1590 };
1591
1592 let err = req.validate_response(&resp).unwrap_err();
1593 assert!(matches!(err, ValidationError::ConstraintNotSatisfied));
1594 }
1595
1596 #[test]
1597 fn request_more_complex_constraints_nested_success() {
1598 let req = ProofRequest {
1599 id: "req_18c0f7f03e7d".into(),
1600 version: RequestVersion::V1,
1601 proof_type: ProofType::Uniqueness,
1602 session_id: None,
1603 action: Some(test_field_element(1)),
1604 created_at: 1_725_381_192,
1605 expires_at: 1_725_381_492,
1606 rp_id: RpId::new(1),
1607 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1608 signature: test_signature(),
1609 nonce: test_nonce(),
1610 requests: vec![
1611 RequestItem {
1612 identifier: "test_req_1".into(),
1613 issuer_schema_id: 1,
1614 signal: Some("abcd-efgh-ijkl".into()),
1615 genesis_issued_at_min: None,
1616 expires_at_min: None,
1617 },
1618 RequestItem {
1619 identifier: "test_req_2".into(),
1620 issuer_schema_id: 2,
1621 signal: Some("mnop-qrst-uvwx".into()),
1622 genesis_issued_at_min: None,
1623 expires_at_min: None,
1624 },
1625 RequestItem {
1626 identifier: "test_req_3".into(),
1627 issuer_schema_id: 3,
1628 signal: Some("abcd-efgh-ijkl".into()),
1629 genesis_issued_at_min: None,
1630 expires_at_min: None,
1631 },
1632 ],
1633 constraints: Some(ConstraintExpr::All {
1634 all: vec![
1635 ConstraintNode::Type("test_req_3".into()),
1636 ConstraintNode::Expr(ConstraintExpr::Any {
1637 any: vec![
1638 ConstraintNode::Type("test_req_1".into()),
1639 ConstraintNode::Type("test_req_2".into()),
1640 ],
1641 }),
1642 ],
1643 }),
1644 };
1645
1646 let resp = ProofResponse {
1648 id: req.id.clone(),
1649 version: RequestVersion::V1,
1650 session_id: None,
1651 error: None,
1652 responses: vec![
1653 ResponseItem::new_uniqueness(
1654 "test_req_3".into(),
1655 3,
1656 ZeroKnowledgeProof::default(),
1657 Nullifier::from(test_field_element(1001)),
1658 1_725_381_192,
1659 ),
1660 ResponseItem::new_uniqueness(
1661 "test_req_1".into(),
1662 1,
1663 ZeroKnowledgeProof::default(),
1664 Nullifier::from(test_field_element(1002)),
1665 1_725_381_192,
1666 ),
1667 ],
1668 };
1669
1670 assert!(req.validate_response(&resp).is_ok());
1671 }
1672
1673 #[test]
1674 fn request_validate_response_with_enumerate() {
1675 let req = ProofRequest {
1676 id: "req_enum".into(),
1677 version: RequestVersion::V1,
1678 proof_type: ProofType::Uniqueness,
1679 session_id: None,
1680 action: Some(test_field_element(1)),
1681 created_at: 1_725_381_192,
1682 expires_at: 1_725_381_492,
1683 rp_id: RpId::new(1),
1684 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1685 signature: test_signature(),
1686 nonce: test_nonce(),
1687 requests: vec![
1688 RequestItem {
1689 identifier: "passport".into(),
1690 issuer_schema_id: 2,
1691 signal: None,
1692 genesis_issued_at_min: None,
1693 expires_at_min: None,
1694 },
1695 RequestItem {
1696 identifier: "national_id".into(),
1697 issuer_schema_id: 3,
1698 signal: None,
1699 genesis_issued_at_min: None,
1700 expires_at_min: None,
1701 },
1702 ],
1703 constraints: Some(ConstraintExpr::Enumerate {
1704 enumerate: vec![
1705 ConstraintNode::Type("passport".into()),
1706 ConstraintNode::Type("national_id".into()),
1707 ],
1708 }),
1709 };
1710
1711 let ok_resp = ProofResponse {
1713 id: req.id.clone(),
1714 version: RequestVersion::V1,
1715 session_id: None,
1716 error: None,
1717 responses: vec![ResponseItem::new_uniqueness(
1718 "passport".into(),
1719 2,
1720 ZeroKnowledgeProof::default(),
1721 Nullifier::from(test_field_element(2002)),
1722 1_725_381_192,
1723 )],
1724 };
1725 assert!(req.validate_response(&ok_resp).is_ok());
1726
1727 let fail_resp = ProofResponse {
1729 id: req.id.clone(),
1730 version: RequestVersion::V1,
1731 session_id: None,
1732 error: None,
1733 responses: vec![],
1734 };
1735 let err = req.validate_response(&fail_resp).unwrap_err();
1736 assert!(matches!(err, ValidationError::ConstraintNotSatisfied));
1737 }
1738
1739 #[test]
1740 fn request_json_parse() {
1741 let with_signal = r#"{
1743 "id": "req_abc123",
1744 "version": 1,
1745 "created_at": 1725381192,
1746 "expires_at": 1725381492,
1747 "rp_id": "rp_0000000000000001",
1748 "oprf_key_id": "0x1",
1749 "session_id": null,
1750 "action": "0x000000000000000000000000000000000000000000000000000000000000002a",
1751 "signature": "0xa1fd06f0d8ceb541f6096fe2e865063eac1ff085c9d2bac2eedcc9ed03804bfc18d956b38c5ac3a8f7e71fde43deff3bda254d369c699f3c7a3f8e6b8477a5f51c",
1752 "nonce": "0x0000000000000000000000000000000000000000000000000000000000000001",
1753 "proof_requests": [
1754 {
1755 "identifier": "orb",
1756 "issuer_schema_id": 1,
1757 "signal": "0xdeadbeef",
1758 "genesis_issued_at_min": 1725381192,
1759 "expires_at_min": 1725381492
1760 }
1761 ]
1762}"#;
1763
1764 let req = ProofRequest::from_json(with_signal).expect("parse with signal");
1765 assert_eq!(req.id, "req_abc123");
1766 assert_eq!(req.requests.len(), 1);
1767 assert_eq!(req.requests[0].signal, Some(b"\xde\xad\xbe\xef".to_vec()));
1768 assert_eq!(req.requests[0].genesis_issued_at_min, Some(1_725_381_192));
1769 assert_eq!(req.requests[0].expires_at_min, Some(1_725_381_492));
1770
1771 let without_signal = r#"{
1772 "id": "req_abc123",
1773 "version": 1,
1774 "created_at": 1725381192,
1775 "expires_at": 1725381492,
1776 "rp_id": "rp_0000000000000001",
1777 "oprf_key_id": "0x1",
1778 "session_id": null,
1779 "action": "0x000000000000000000000000000000000000000000000000000000000000002a",
1780 "signature": "0xa1fd06f0d8ceb541f6096fe2e865063eac1ff085c9d2bac2eedcc9ed03804bfc18d956b38c5ac3a8f7e71fde43deff3bda254d369c699f3c7a3f8e6b8477a5f51c",
1781 "nonce": "0x0000000000000000000000000000000000000000000000000000000000000001",
1782 "proof_requests": [
1783 {
1784 "identifier": "orb",
1785 "issuer_schema_id": 1
1786 }
1787 ]
1788}"#;
1789
1790 let req = ProofRequest::from_json(without_signal).expect("parse without signal");
1791 assert!(req.requests[0].signal.is_none());
1792 assert_eq!(req.requests[0].signal_hash(), FieldElement::ZERO);
1793 }
1794
1795 #[test]
1796 fn response_json_parse() {
1797 let ok_json = r#"{
1799 "id": "req_18c0f7f03e7d",
1800 "version": 1,
1801 "responses": [
1802 {
1803 "identifier": "orb",
1804 "issuer_schema_id": 100,
1805 "proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
1806 "nullifier": "nil_00000000000000000000000000000000000000000000000000000000000003e9",
1807 "expires_at_min": 1725381192
1808 }
1809 ]
1810}"#;
1811
1812 let ok = ProofResponse::from_json(ok_json).unwrap();
1813 assert_eq!(ok.successful_credentials(), vec![100]);
1814 assert!(ok.responses[0].is_uniqueness());
1815
1816 let canonical_session_nullifier = serde_json::to_string(
1818 &SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap(),
1819 )
1820 .unwrap();
1821 let sess_json_canonical = format!(
1822 r#"{{
1823 "id": "req_18c0f7f03e7d",
1824 "version": 1,
1825 "session_id": "session_00000000000000000000000000000000000000000000000000000000000003ea0100000000000000000000000000000000000000000000000000000000000001",
1826 "responses": [
1827 {{
1828 "identifier": "orb",
1829 "issuer_schema_id": 100,
1830 "proof": "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
1831 "session_nullifier": {canonical_session_nullifier},
1832 "expires_at_min": 1725381192
1833 }}
1834 ]
1835}}"#
1836 );
1837 let sess_canonical = ProofResponse::from_json(&sess_json_canonical).unwrap();
1838 assert_eq!(sess_canonical.successful_credentials(), vec![100]);
1839 assert!(sess_canonical.responses[0].is_session());
1840 assert_eq!(
1841 sess_canonical.session_id.unwrap().oprf_seed.to_u256(),
1842 uint!(0x0100000000000000000000000000000000000000000000000000000000000001_U256)
1843 );
1844 }
1845 #[test]
1848 fn request_rejects_duplicate_issuer_schema_ids_on_parse() {
1849 let req = ProofRequest {
1850 id: "req_dup".into(),
1851 version: RequestVersion::V1,
1852 proof_type: ProofType::Uniqueness,
1853 session_id: None,
1854 action: Some(test_field_element(5)),
1855 created_at: 1_725_381_192,
1856 expires_at: 1_725_381_492,
1857 rp_id: RpId::new(1),
1858 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1859 signature: test_signature(),
1860 nonce: test_nonce(),
1861 requests: vec![
1862 RequestItem {
1863 identifier: "test_req_1".into(),
1864 issuer_schema_id: 1,
1865 signal: None,
1866 genesis_issued_at_min: None,
1867 expires_at_min: None,
1868 },
1869 RequestItem {
1870 identifier: "test_req_2".into(),
1871 issuer_schema_id: 1, signal: None,
1873 genesis_issued_at_min: None,
1874 expires_at_min: None,
1875 },
1876 ],
1877 constraints: None,
1878 };
1879
1880 let json = req.to_json().unwrap();
1882 let err = ProofRequest::from_json(&json).unwrap_err();
1883 let msg = err.to_string();
1884 assert!(
1885 msg.contains("duplicate issuer schema id"),
1886 "Expected error message to contain 'duplicate issuer schema id', got: {msg}"
1887 );
1888 }
1889
1890 #[test]
1891 fn response_with_error_has_empty_responses_and_fails_validation() {
1892 let request = ProofRequest {
1893 id: "req_error".into(),
1894 version: RequestVersion::V1,
1895 proof_type: ProofType::Uniqueness,
1896 session_id: None,
1897 action: Some(FieldElement::ZERO),
1898 created_at: 1_735_689_600,
1899 expires_at: 1_735_689_600,
1900 rp_id: RpId::new(1),
1901 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1902 signature: test_signature(),
1903 nonce: test_nonce(),
1904 requests: vec![RequestItem {
1905 identifier: "orb".into(),
1906 issuer_schema_id: 1,
1907 signal: None,
1908 genesis_issued_at_min: None,
1909 expires_at_min: None,
1910 }],
1911 constraints: None,
1912 };
1913
1914 let error_response = ProofResponse {
1916 id: "req_error".into(),
1917 version: RequestVersion::V1,
1918 session_id: None,
1919 error: Some("credential_not_available".into()),
1920 responses: vec![], };
1922
1923 let err = request.validate_response(&error_response).unwrap_err();
1925 assert!(matches!(err, ValidationError::ProofGenerationFailed(_)));
1926 if let ValidationError::ProofGenerationFailed(msg) = err {
1927 assert_eq!(msg, "credential_not_available");
1928 }
1929
1930 assert_eq!(error_response.successful_credentials(), Vec::<u64>::new());
1932
1933 let expr = ConstraintExpr::All {
1935 all: vec![ConstraintNode::Type("orb".into())],
1936 };
1937 assert!(!error_response.constraints_satisfied(&expr));
1938 }
1939
1940 #[test]
1941 fn response_error_json_parse() {
1942 let error_json = r#"{
1944 "id": "req_error",
1945 "version": 1,
1946 "error": "credential_not_available",
1947 "responses": []
1948}"#;
1949
1950 let error_resp = ProofResponse::from_json(error_json).unwrap();
1951 assert_eq!(error_resp.error, Some("credential_not_available".into()));
1952 assert_eq!(error_resp.responses.len(), 0);
1953 assert_eq!(error_resp.successful_credentials(), Vec::<u64>::new());
1954 }
1955
1956 #[test]
1957 fn credentials_to_prove_none_constraints_requires_all_and_drops_if_missing() {
1958 let req = ProofRequest {
1959 id: "req".into(),
1960 version: RequestVersion::V1,
1961 proof_type: ProofType::Uniqueness,
1962 session_id: None,
1963 action: Some(test_field_element(5)),
1964 created_at: 1_735_689_600,
1965 expires_at: 1_735_689_600, rp_id: RpId::new(1),
1967 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
1968 signature: test_signature(),
1969 nonce: test_nonce(),
1970 requests: vec![
1971 RequestItem {
1972 identifier: "orb".into(),
1973 issuer_schema_id: 100,
1974 signal: None,
1975 genesis_issued_at_min: None,
1976 expires_at_min: None,
1977 },
1978 RequestItem {
1979 identifier: "passport".into(),
1980 issuer_schema_id: 101,
1981 signal: None,
1982 genesis_issued_at_min: None,
1983 expires_at_min: None,
1984 },
1985 ],
1986 constraints: None,
1987 };
1988
1989 let available_ok: HashSet<u64> = [100, 101].into_iter().collect();
1990 let sel_ok = req.credentials_to_prove(&available_ok).unwrap();
1991 assert_eq!(sel_ok.len(), 2);
1992 assert_eq!(sel_ok[0].issuer_schema_id, 100);
1993 assert_eq!(sel_ok[1].issuer_schema_id, 101);
1994
1995 let available_missing: HashSet<u64> = std::iter::once(100).collect();
1996 assert!(req.credentials_to_prove(&available_missing).is_none());
1997 }
1998
1999 #[test]
2000 fn credentials_to_prove_with_constraints_all_and_any() {
2001 let orb_id = 100;
2003 let passport_id = 101;
2004 let national_id = 102;
2005
2006 let req = ProofRequest {
2007 id: "req".into(),
2008 version: RequestVersion::V1,
2009 proof_type: ProofType::Uniqueness,
2010 session_id: None,
2011 action: Some(test_field_element(1)),
2012 created_at: 1_735_689_600,
2013 expires_at: 1_735_689_600, rp_id: RpId::new(1),
2015 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2016 signature: test_signature(),
2017 nonce: test_nonce(),
2018 requests: vec![
2019 RequestItem {
2020 identifier: "orb".into(),
2021 issuer_schema_id: orb_id,
2022 signal: None,
2023 genesis_issued_at_min: None,
2024 expires_at_min: None,
2025 },
2026 RequestItem {
2027 identifier: "passport".into(),
2028 issuer_schema_id: passport_id,
2029 signal: None,
2030 genesis_issued_at_min: None,
2031 expires_at_min: None,
2032 },
2033 RequestItem {
2034 identifier: "national_id".into(),
2035 issuer_schema_id: national_id,
2036 signal: None,
2037 genesis_issued_at_min: None,
2038 expires_at_min: None,
2039 },
2040 ],
2041 constraints: Some(ConstraintExpr::All {
2042 all: vec![
2043 ConstraintNode::Type("orb".into()),
2044 ConstraintNode::Expr(ConstraintExpr::Any {
2045 any: vec![
2046 ConstraintNode::Type("passport".into()),
2047 ConstraintNode::Type("national_id".into()),
2048 ],
2049 }),
2050 ],
2051 }),
2052 };
2053
2054 let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
2056 let sel1 = req.credentials_to_prove(&available1).unwrap();
2057 assert_eq!(sel1.len(), 2);
2058 assert_eq!(sel1[0].issuer_schema_id, orb_id);
2059 assert_eq!(sel1[1].issuer_schema_id, passport_id);
2060
2061 let available2: HashSet<u64> = [orb_id, national_id].into_iter().collect();
2063 let sel2 = req.credentials_to_prove(&available2).unwrap();
2064 assert_eq!(sel2.len(), 2);
2065 assert_eq!(sel2[0].issuer_schema_id, orb_id);
2066 assert_eq!(sel2[1].issuer_schema_id, national_id);
2067
2068 let available3: HashSet<u64> = std::iter::once(passport_id).collect();
2070 assert!(req.credentials_to_prove(&available3).is_none());
2071 }
2072
2073 #[test]
2074 fn credentials_to_prove_with_constraints_enumerate() {
2075 let orb_id = 100;
2076 let passport_id = 101;
2077 let national_id = 102;
2078
2079 let req = ProofRequest {
2080 id: "req".into(),
2081 version: RequestVersion::V1,
2082 proof_type: ProofType::Uniqueness,
2083 session_id: None,
2084 action: Some(test_field_element(1)),
2085 created_at: 1_735_689_600,
2086 expires_at: 1_735_689_600,
2087 rp_id: RpId::new(1),
2088 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2089 signature: test_signature(),
2090 nonce: test_nonce(),
2091 requests: vec![
2092 RequestItem {
2093 identifier: "orb".into(),
2094 issuer_schema_id: orb_id,
2095 signal: None,
2096 genesis_issued_at_min: None,
2097 expires_at_min: None,
2098 },
2099 RequestItem {
2100 identifier: "passport".into(),
2101 issuer_schema_id: passport_id,
2102 signal: None,
2103 genesis_issued_at_min: None,
2104 expires_at_min: None,
2105 },
2106 RequestItem {
2107 identifier: "national_id".into(),
2108 issuer_schema_id: national_id,
2109 signal: None,
2110 genesis_issued_at_min: None,
2111 expires_at_min: None,
2112 },
2113 ],
2114 constraints: Some(ConstraintExpr::Enumerate {
2115 enumerate: vec![
2116 ConstraintNode::Type("passport".into()),
2117 ConstraintNode::Type("national_id".into()),
2118 ],
2119 }),
2120 };
2121
2122 let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
2124 let sel1 = req.credentials_to_prove(&available1).unwrap();
2125 assert_eq!(sel1.len(), 1);
2126 assert_eq!(sel1[0].issuer_schema_id, passport_id);
2127
2128 let available2: HashSet<u64> = [orb_id, passport_id, national_id].into_iter().collect();
2130 let sel2 = req.credentials_to_prove(&available2).unwrap();
2131 assert_eq!(sel2.len(), 2);
2132 assert_eq!(sel2[0].issuer_schema_id, passport_id);
2133 assert_eq!(sel2[1].issuer_schema_id, national_id);
2134
2135 let available3: HashSet<u64> = std::iter::once(orb_id).collect();
2137 assert!(req.credentials_to_prove(&available3).is_none());
2138 }
2139
2140 #[test]
2141 fn credentials_to_prove_with_constraints_all_and_enumerate() {
2142 let orb_id = 100;
2143 let passport_id = 101;
2144 let national_id = 102;
2145
2146 let req = ProofRequest {
2147 id: "req".into(),
2148 version: RequestVersion::V1,
2149 proof_type: ProofType::Uniqueness,
2150 session_id: None,
2151 action: Some(test_field_element(1)),
2152 created_at: 1_735_689_600,
2153 expires_at: 1_735_689_600,
2154 rp_id: RpId::new(1),
2155 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2156 signature: test_signature(),
2157 nonce: test_nonce(),
2158 requests: vec![
2159 RequestItem {
2160 identifier: "orb".into(),
2161 issuer_schema_id: orb_id,
2162 signal: None,
2163 genesis_issued_at_min: None,
2164 expires_at_min: None,
2165 },
2166 RequestItem {
2167 identifier: "passport".into(),
2168 issuer_schema_id: passport_id,
2169 signal: None,
2170 genesis_issued_at_min: None,
2171 expires_at_min: None,
2172 },
2173 RequestItem {
2174 identifier: "national_id".into(),
2175 issuer_schema_id: national_id,
2176 signal: None,
2177 genesis_issued_at_min: None,
2178 expires_at_min: None,
2179 },
2180 ],
2181 constraints: Some(ConstraintExpr::All {
2182 all: vec![
2183 ConstraintNode::Type("orb".into()),
2184 ConstraintNode::Expr(ConstraintExpr::Enumerate {
2185 enumerate: vec![
2186 ConstraintNode::Type("passport".into()),
2187 ConstraintNode::Type("national_id".into()),
2188 ],
2189 }),
2190 ],
2191 }),
2192 };
2193
2194 let available1: HashSet<u64> = [orb_id, passport_id].into_iter().collect();
2196 let sel1 = req.credentials_to_prove(&available1).unwrap();
2197 assert_eq!(sel1.len(), 2);
2198 assert_eq!(sel1[0].issuer_schema_id, orb_id);
2199 assert_eq!(sel1[1].issuer_schema_id, passport_id);
2200
2201 let available2: HashSet<u64> = [orb_id, passport_id, national_id].into_iter().collect();
2203 let sel2 = req.credentials_to_prove(&available2).unwrap();
2204 assert_eq!(sel2.len(), 3);
2205 assert_eq!(sel2[0].issuer_schema_id, orb_id);
2206 assert_eq!(sel2[1].issuer_schema_id, passport_id);
2207 assert_eq!(sel2[2].issuer_schema_id, national_id);
2208
2209 let available3: HashSet<u64> = std::iter::once(orb_id).collect();
2211 assert!(req.credentials_to_prove(&available3).is_none());
2212 }
2213
2214 #[test]
2215 fn request_item_effective_expires_at_min_defaults_to_created_at() {
2216 let request_created_at = 1_735_689_600; let custom_expires_at = 1_735_862_400; let item_with_none = RequestItem {
2221 identifier: "test".into(),
2222 issuer_schema_id: 100,
2223 signal: None,
2224 genesis_issued_at_min: None,
2225 expires_at_min: None,
2226 };
2227 assert_eq!(
2228 item_with_none.effective_expires_at_min(request_created_at),
2229 request_created_at,
2230 "When expires_at_min is None, should default to request created_at"
2231 );
2232
2233 let item_with_custom = RequestItem {
2235 identifier: "test".into(),
2236 issuer_schema_id: 100,
2237 signal: None,
2238 genesis_issued_at_min: None,
2239 expires_at_min: Some(custom_expires_at),
2240 };
2241 assert_eq!(
2242 item_with_custom.effective_expires_at_min(request_created_at),
2243 custom_expires_at,
2244 "When expires_at_min is Some, should use that explicit value"
2245 );
2246 }
2247
2248 #[test]
2249 fn validate_response_checks_expires_at_min_matches() {
2250 let request_created_at = 1_735_689_600; let custom_expires_at = 1_735_862_400; let request = ProofRequest {
2256 id: "req_expires_test".into(),
2257 version: RequestVersion::V1,
2258 proof_type: ProofType::Uniqueness,
2259 session_id: None,
2260 action: Some(test_field_element(1)),
2261 created_at: request_created_at,
2262 expires_at: request_created_at + 300,
2263 rp_id: RpId::new(1),
2264 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2265 signature: test_signature(),
2266 nonce: test_nonce(),
2267 requests: vec![
2268 RequestItem {
2269 identifier: "orb".into(),
2270 issuer_schema_id: 100,
2271 signal: None,
2272 genesis_issued_at_min: None,
2273 expires_at_min: None, },
2275 RequestItem {
2276 identifier: "document".into(),
2277 issuer_schema_id: 101,
2278 signal: None,
2279 genesis_issued_at_min: None,
2280 expires_at_min: Some(custom_expires_at), },
2282 ],
2283 constraints: None,
2284 };
2285
2286 let valid_response = ProofResponse {
2288 id: "req_expires_test".into(),
2289 version: RequestVersion::V1,
2290 session_id: None,
2291 error: None,
2292 responses: vec![
2293 ResponseItem::new_uniqueness(
2294 "orb".into(),
2295 100,
2296 ZeroKnowledgeProof::default(),
2297 Nullifier::from(test_field_element(1001)),
2298 request_created_at, ),
2300 ResponseItem::new_uniqueness(
2301 "document".into(),
2302 101,
2303 ZeroKnowledgeProof::default(),
2304 Nullifier::from(test_field_element(1002)),
2305 custom_expires_at, ),
2307 ],
2308 };
2309 assert!(request.validate_response(&valid_response).is_ok());
2310
2311 let invalid_response_1 = ProofResponse {
2313 id: "req_expires_test".into(),
2314 version: RequestVersion::V1,
2315 session_id: None,
2316 error: None,
2317 responses: vec![
2318 ResponseItem::new_uniqueness(
2319 "orb".into(),
2320 100,
2321 ZeroKnowledgeProof::default(),
2322 Nullifier::from(test_field_element(1001)),
2323 custom_expires_at, ),
2325 ResponseItem::new_uniqueness(
2326 "document".into(),
2327 101,
2328 ZeroKnowledgeProof::default(),
2329 Nullifier::from(test_field_element(1002)),
2330 custom_expires_at,
2331 ),
2332 ],
2333 };
2334 let err1 = request.validate_response(&invalid_response_1).unwrap_err();
2335 assert!(matches!(
2336 err1,
2337 ValidationError::ExpiresAtMinMismatch(_, _, _)
2338 ));
2339 if let ValidationError::ExpiresAtMinMismatch(identifier, expected, got) = err1 {
2340 assert_eq!(identifier, "orb");
2341 assert_eq!(expected, request_created_at);
2342 assert_eq!(got, custom_expires_at);
2343 }
2344
2345 let invalid_response_2 = ProofResponse {
2347 id: "req_expires_test".into(),
2348 version: RequestVersion::V1,
2349 session_id: None,
2350 error: None,
2351 responses: vec![
2352 ResponseItem::new_uniqueness(
2353 "orb".into(),
2354 100,
2355 ZeroKnowledgeProof::default(),
2356 Nullifier::from(test_field_element(1001)),
2357 request_created_at,
2358 ),
2359 ResponseItem::new_uniqueness(
2360 "document".into(),
2361 101,
2362 ZeroKnowledgeProof::default(),
2363 Nullifier::from(test_field_element(1002)),
2364 request_created_at, ),
2366 ],
2367 };
2368 let err2 = request.validate_response(&invalid_response_2).unwrap_err();
2369 assert!(matches!(
2370 err2,
2371 ValidationError::ExpiresAtMinMismatch(_, _, _)
2372 ));
2373 if let ValidationError::ExpiresAtMinMismatch(identifier, expected, got) = err2 {
2374 assert_eq!(identifier, "document");
2375 assert_eq!(expected, custom_expires_at);
2376 assert_eq!(got, request_created_at);
2377 }
2378 }
2379
2380 #[test]
2381 fn test_validate_proof_type_is_strict() {
2382 let uniqueness_with_session = ProofRequest {
2383 id: "req_legacy_session".into(),
2384 version: RequestVersion::V1,
2385 proof_type: ProofType::Uniqueness,
2386 session_id: Some(SessionId::default()),
2387 action: None,
2388 created_at: 1_735_689_600,
2389 expires_at: 1_735_689_900,
2390 rp_id: RpId::new(1),
2391 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2392 signature: test_signature(),
2393 nonce: test_nonce(),
2394 requests: vec![RequestItem {
2395 identifier: "orb".into(),
2396 issuer_schema_id: 1,
2397 signal: None,
2398 genesis_issued_at_min: None,
2399 expires_at_min: None,
2400 }],
2401 constraints: None,
2402 };
2403
2404 assert!(matches!(
2405 uniqueness_with_session.validate_proof_type(),
2406 Err(PrimitiveError::InvalidInput { attribute, .. }) if attribute == "session_id"
2407 ));
2408
2409 let session_without_session = ProofRequest {
2410 proof_type: ProofType::Session,
2411 session_id: None,
2412 ..uniqueness_with_session
2413 };
2414
2415 assert!(matches!(
2416 session_without_session.validate_proof_type(),
2417 Err(PrimitiveError::InvalidInput { attribute, .. }) if attribute == "session_id"
2418 ));
2419 }
2420
2421 #[test]
2422 fn proof_type_protocol_encoding_is_stable() {
2423 assert_eq!(ProofType::Uniqueness as u8, 0x00);
2424 assert_eq!(ProofType::CreateSession as u8, 0x01);
2425 assert_eq!(ProofType::Session as u8, 0x02);
2426 }
2427
2428 #[test]
2429 fn test_validate_response_accepts_create_session_response() {
2430 let request = ProofRequest {
2431 id: "req_create_session".into(),
2432 version: RequestVersion::V1,
2433 proof_type: ProofType::CreateSession,
2434 session_id: None,
2435 action: None,
2436 created_at: 1_735_689_600,
2437 expires_at: 1_735_689_900,
2438 rp_id: RpId::new(1),
2439 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2440 signature: test_signature(),
2441 nonce: test_nonce(),
2442 requests: vec![RequestItem {
2443 identifier: "orb".into(),
2444 issuer_schema_id: 1,
2445 signal: None,
2446 genesis_issued_at_min: None,
2447 expires_at_min: None,
2448 }],
2449 constraints: None,
2450 };
2451
2452 let missing_session = ProofResponse {
2453 id: request.id.clone(),
2454 version: RequestVersion::V1,
2455 session_id: None,
2456 error: None,
2457 responses: vec![ResponseItem::new_session(
2458 "orb".into(),
2459 1,
2460 ZeroKnowledgeProof::default(),
2461 SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap(),
2462 1_735_689_600,
2463 )],
2464 };
2465 assert!(matches!(
2466 request.validate_response(&missing_session),
2467 Err(ValidationError::MissingSessionId)
2468 ));
2469
2470 let valid_response = ProofResponse {
2471 session_id: Some(SessionId::default()),
2472 ..missing_session
2473 };
2474 assert!(request.validate_response(&valid_response).is_ok());
2475 }
2476
2477 #[test]
2478 fn test_validate_response_requires_session_id_in_response() {
2479 let request = ProofRequest {
2481 id: "req_session".into(),
2482 version: RequestVersion::V1,
2483 proof_type: ProofType::Session,
2484 session_id: Some(SessionId::default()),
2485 action: None,
2486 created_at: 1_735_689_600,
2487 expires_at: 1_735_689_900,
2488 rp_id: RpId::new(1),
2489 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2490 signature: test_signature(),
2491 nonce: test_nonce(),
2492 requests: vec![RequestItem {
2493 identifier: "orb".into(),
2494 issuer_schema_id: 1,
2495 signal: None,
2496 genesis_issued_at_min: None,
2497 expires_at_min: None,
2498 }],
2499 constraints: None,
2500 };
2501
2502 let response_missing_session_id = ProofResponse {
2504 id: "req_session".into(),
2505 version: RequestVersion::V1,
2506 session_id: None, error: None,
2508 responses: vec![ResponseItem::new_session(
2509 "orb".into(),
2510 1,
2511 ZeroKnowledgeProof::default(),
2512 SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap(),
2513 1_735_689_600,
2514 )],
2515 };
2516
2517 let err = request
2518 .validate_response(&response_missing_session_id)
2519 .unwrap_err();
2520 assert!(matches!(err, ValidationError::SessionIdMismatch));
2521 }
2522
2523 #[test]
2524 fn test_validate_response_requires_session_nullifier_for_session_proof() {
2525 let request = ProofRequest {
2527 id: "req_session".into(),
2528 version: RequestVersion::V1,
2529 proof_type: ProofType::Session,
2530 session_id: Some(SessionId::default()),
2531 action: None,
2532 created_at: 1_735_689_600,
2533 expires_at: 1_735_689_900,
2534 rp_id: RpId::new(1),
2535 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2536 signature: test_signature(),
2537 nonce: test_nonce(),
2538 requests: vec![RequestItem {
2539 identifier: "orb".into(),
2540 issuer_schema_id: 1,
2541 signal: None,
2542 genesis_issued_at_min: None,
2543 expires_at_min: None,
2544 }],
2545 constraints: None,
2546 };
2547
2548 let response_wrong_nullifier_type = ProofResponse {
2550 id: "req_session".into(),
2551 version: RequestVersion::V1,
2552 session_id: Some(SessionId::default()),
2553 error: None,
2554 responses: vec![ResponseItem::new_uniqueness(
2555 "orb".into(),
2556 1,
2557 ZeroKnowledgeProof::default(),
2558 Nullifier::from(test_field_element(1001)), 1_735_689_600,
2560 )],
2561 };
2562
2563 let err = request
2564 .validate_response(&response_wrong_nullifier_type)
2565 .unwrap_err();
2566 assert!(matches!(
2567 err,
2568 ValidationError::MissingSessionNullifier(ref id) if id == "orb"
2569 ));
2570 }
2571
2572 #[test]
2573 fn test_validate_response_requires_nullifier_for_uniqueness_proof() {
2574 let request = ProofRequest {
2576 id: "req_uniqueness".into(),
2577 version: RequestVersion::V1,
2578 proof_type: ProofType::Uniqueness,
2579 session_id: None,
2580 action: Some(test_field_element(42)),
2581 created_at: 1_735_689_600,
2582 expires_at: 1_735_689_900,
2583 rp_id: RpId::new(1),
2584 oprf_key_id: OprfKeyId::new(uint!(1_U160)),
2585 signature: test_signature(),
2586 nonce: test_nonce(),
2587 requests: vec![RequestItem {
2588 identifier: "orb".into(),
2589 issuer_schema_id: 1,
2590 signal: None,
2591 genesis_issued_at_min: None,
2592 expires_at_min: None,
2593 }],
2594 constraints: None,
2595 };
2596
2597 let response_wrong_nullifier_type = ProofResponse {
2599 id: "req_uniqueness".into(),
2600 version: RequestVersion::V1,
2601 session_id: None,
2602 error: None,
2603 responses: vec![ResponseItem::new_session(
2604 "orb".into(),
2605 1,
2606 ZeroKnowledgeProof::default(),
2607 SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap(), 1_735_689_600,
2609 )],
2610 };
2611
2612 let err = request
2613 .validate_response(&response_wrong_nullifier_type)
2614 .unwrap_err();
2615 assert!(matches!(
2616 err,
2617 ValidationError::MissingNullifier(ref id) if id == "orb"
2618 ));
2619 }
2620}