chie_crypto/
anonymous_credentials.rs

1//! Anonymous Credentials (Idemix-style) for privacy-preserving authentication
2//!
3//! This module implements a simplified anonymous credential system inspired by IBM's
4//! Idemix (Identity Mixer). It provides:
5//! - Credential issuance without revealing user identity
6//! - Selective disclosure of attributes
7//! - Unlinkable presentations (same credential, different presentations are unlinkable)
8//! - Revocation support
9//!
10//! # Example
11//!
12//! ```
13//! use chie_crypto::anonymous_credentials::*;
14//!
15//! // Setup issuer
16//! let issuer = Issuer::new();
17//!
18//! // User creates credential request
19//! let user = User::new();
20//! let request = user.create_credential_request(&issuer.public_key()).unwrap();
21//!
22//! // Issuer issues credential with attributes
23//! let mut attributes = std::collections::HashMap::new();
24//! attributes.insert("age".to_string(), vec![18]); // Age >= 18
25//! attributes.insert("country".to_string(), vec![1]); // Country code 1
26//!
27//! let credential = issuer.issue_credential(&request, &attributes).unwrap();
28//!
29//! // User creates presentation revealing only age >= 18
30//! let mut revealed = std::collections::HashSet::new();
31//! revealed.insert("age".to_string());
32//!
33//! let presentation = user.create_presentation(&credential, &revealed).unwrap();
34//!
35//! // Verifier checks presentation
36//! assert!(issuer.verify_presentation(&presentation, &revealed).unwrap());
37//! ```
38
39use curve25519_dalek::ristretto::RistrettoPoint;
40use curve25519_dalek::scalar::Scalar;
41use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
42use serde::{Deserialize, Serialize};
43use sha2::{Digest, Sha256};
44use std::collections::{HashMap, HashSet};
45use thiserror::Error;
46
47/// Anonymous credentials error types
48#[derive(Error, Debug)]
49pub enum AnonCredError {
50    #[error("Invalid credential request")]
51    InvalidRequest,
52    #[error("Invalid credential")]
53    InvalidCredential,
54    #[error("Invalid presentation")]
55    InvalidPresentation,
56    #[error("Missing attribute: {0}")]
57    MissingAttribute(String),
58    #[error("Verification failed")]
59    VerificationFailed,
60    #[error("Revoked credential")]
61    RevokedCredential,
62    #[error("Serialization error: {0}")]
63    SerializationError(String),
64}
65
66/// Result type for anonymous credentials operations
67pub type AnonCredResult<T> = Result<T, AnonCredError>;
68
69/// Issuer public key
70#[derive(Clone, Debug, Serialize, Deserialize)]
71pub struct IssuerPublicKey {
72    /// Public verification key
73    #[serde(with = "serde_verifying_key")]
74    verification_key: VerifyingKey,
75    /// Attribute commitment bases (one per attribute)
76    #[serde(with = "serde_point_vec")]
77    attribute_bases: Vec<RistrettoPoint>,
78    /// Attribute names in order
79    attribute_names: Vec<String>,
80}
81
82/// Credential request from user
83#[derive(Clone, Debug, Serialize, Deserialize)]
84pub struct CredentialRequest {
85    /// User's public commitment to their secret
86    #[serde(with = "serde_point")]
87    user_commitment: RistrettoPoint,
88    /// Proof of knowledge of secret (Schnorr-like)
89    #[serde(with = "serde_scalar")]
90    proof_challenge: Scalar,
91    #[serde(with = "serde_scalar")]
92    proof_response: Scalar,
93}
94
95/// Anonymous credential
96#[derive(Clone, Debug, Serialize, Deserialize)]
97pub struct AnonymousCredential {
98    /// Issuer signature on commitment
99    #[serde(with = "serde_signature")]
100    signature: Signature,
101    /// Attributes (encrypted/committed)
102    attributes: HashMap<String, Vec<u8>>,
103    /// Commitment to attributes and secret
104    #[serde(with = "serde_point")]
105    commitment: RistrettoPoint,
106}
107
108/// Credential presentation (proof of possession without revealing credential)
109#[derive(Clone, Debug, Serialize, Deserialize)]
110pub struct CredentialPresentation {
111    /// Credential signature (proves validity)
112    #[serde(with = "serde_signature")]
113    signature: Signature,
114    /// Commitment
115    #[serde(with = "serde_point")]
116    commitment: RistrettoPoint,
117    /// Revealed attributes
118    revealed_attributes: HashMap<String, Vec<u8>>,
119    /// Presentation nonce (prevents replay)
120    nonce: [u8; 32],
121}
122
123/// Issuer of anonymous credentials
124pub struct Issuer {
125    /// Signing key
126    signing_key: SigningKey,
127    /// Public key
128    public_key: IssuerPublicKey,
129    /// Revocation list (credential commitments)
130    revocation_list: HashSet<Vec<u8>>,
131}
132
133impl Issuer {
134    /// Create a new issuer with default attributes
135    pub fn new() -> Self {
136        Self::with_attributes(vec![
137            "age".to_string(),
138            "country".to_string(),
139            "role".to_string(),
140        ])
141    }
142
143    /// Create a new issuer with custom attributes
144    pub fn with_attributes(attribute_names: Vec<String>) -> Self {
145        let mut secret = [0u8; 32];
146        getrandom::fill(&mut secret).expect("Failed to generate random bytes");
147        let signing_key = SigningKey::from_bytes(&secret);
148        let verification_key = signing_key.verifying_key();
149
150        // Generate commitment bases for each attribute
151        let mut attribute_bases = Vec::with_capacity(attribute_names.len());
152        for (i, name) in attribute_names.iter().enumerate() {
153            let mut hasher = Sha256::new();
154            hasher.update(b"anoncred_attribute_base");
155            hasher.update(name.as_bytes());
156            hasher.update(i.to_le_bytes());
157            let hash = hasher.finalize();
158            let scalar = Scalar::from_bytes_mod_order(hash.into());
159            attribute_bases.push(curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT * scalar);
160        }
161
162        let public_key = IssuerPublicKey {
163            verification_key,
164            attribute_bases,
165            attribute_names,
166        };
167
168        Self {
169            signing_key,
170            public_key,
171            revocation_list: HashSet::new(),
172        }
173    }
174
175    /// Get the issuer's public key
176    pub fn public_key(&self) -> &IssuerPublicKey {
177        &self.public_key
178    }
179
180    /// Issue a credential to a user
181    pub fn issue_credential(
182        &self,
183        request: &CredentialRequest,
184        attributes: &HashMap<String, Vec<u8>>,
185    ) -> AnonCredResult<AnonymousCredential> {
186        // Verify the credential request proof
187        if !self.verify_credential_request(request)? {
188            return Err(AnonCredError::InvalidRequest);
189        }
190
191        // Build commitment to attributes
192        let mut commitment = request.user_commitment;
193
194        for (i, attr_name) in self.public_key.attribute_names.iter().enumerate() {
195            if let Some(attr_value) = attributes.get(attr_name) {
196                // Hash attribute value to scalar
197                let mut hasher = Sha256::new();
198                hasher.update(attr_value);
199                let hash = hasher.finalize();
200                let scalar = Scalar::from_bytes_mod_order(hash.into());
201
202                // Add to commitment
203                commitment += self.public_key.attribute_bases[i] * scalar;
204            }
205        }
206
207        // Sign the commitment
208        let commitment_bytes = commitment.compress().to_bytes();
209        let signature = self.signing_key.sign(&commitment_bytes);
210
211        Ok(AnonymousCredential {
212            signature,
213            attributes: attributes.clone(),
214            commitment,
215        })
216    }
217
218    /// Verify a credential request
219    fn verify_credential_request(&self, request: &CredentialRequest) -> AnonCredResult<bool> {
220        // Verify proof of knowledge of secret
221        let generator = curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
222
223        // Recompute challenge
224        let mut hasher = Sha256::new();
225        hasher.update(b"credential_request_challenge");
226        hasher.update(request.user_commitment.compress().as_bytes());
227        let expected_point = (generator * request.proof_response)
228            - (request.user_commitment * request.proof_challenge);
229        hasher.update(expected_point.compress().as_bytes());
230        let hash = hasher.finalize();
231        let expected_challenge = Scalar::from_bytes_mod_order(hash.into());
232
233        Ok(expected_challenge == request.proof_challenge)
234    }
235
236    /// Verify a credential presentation
237    pub fn verify_presentation(
238        &self,
239        presentation: &CredentialPresentation,
240        expected_attributes: &HashSet<String>,
241    ) -> AnonCredResult<bool> {
242        // Check revealed attributes
243        for attr_name in expected_attributes {
244            if !presentation.revealed_attributes.contains_key(attr_name) {
245                return Err(AnonCredError::MissingAttribute(attr_name.clone()));
246            }
247        }
248
249        // Verify signature on commitment
250        let commitment_bytes = presentation.commitment.compress().to_bytes();
251        self.public_key
252            .verification_key
253            .verify_strict(&commitment_bytes, &presentation.signature)
254            .map_err(|_| AnonCredError::VerificationFailed)?;
255
256        // Check not revoked
257        if self.is_revoked(&commitment_bytes) {
258            return Err(AnonCredError::RevokedCredential);
259        }
260
261        Ok(true)
262    }
263
264    /// Revoke a credential
265    pub fn revoke_credential(&mut self, commitment: &[u8]) {
266        self.revocation_list.insert(commitment.to_vec());
267    }
268
269    /// Check if a credential is revoked
270    pub fn is_revoked(&self, commitment: &[u8]) -> bool {
271        self.revocation_list.contains(commitment)
272    }
273}
274
275impl Default for Issuer {
276    fn default() -> Self {
277        Self::new()
278    }
279}
280
281/// User holding anonymous credentials
282pub struct User {
283    /// User's secret
284    secret: Scalar,
285}
286
287impl User {
288    /// Create a new user
289    pub fn new() -> Self {
290        let secret = Scalar::from_bytes_mod_order(rand::random::<[u8; 32]>());
291        Self { secret }
292    }
293
294    /// Create a credential request
295    pub fn create_credential_request(
296        &self,
297        _issuer_pk: &IssuerPublicKey,
298    ) -> AnonCredResult<CredentialRequest> {
299        let generator = curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
300
301        // Commit to secret: C = g^secret
302        let user_commitment = generator * self.secret;
303
304        // Create proof of knowledge of secret (Schnorr protocol)
305        let random_scalar = Scalar::from_bytes_mod_order(rand::random::<[u8; 32]>());
306        let random_commitment = generator * random_scalar;
307
308        // Challenge
309        let mut hasher = Sha256::new();
310        hasher.update(b"credential_request_challenge");
311        hasher.update(user_commitment.compress().as_bytes());
312        hasher.update(random_commitment.compress().as_bytes());
313        let hash = hasher.finalize();
314        let proof_challenge = Scalar::from_bytes_mod_order(hash.into());
315
316        // Response
317        let proof_response = random_scalar + (proof_challenge * self.secret);
318
319        Ok(CredentialRequest {
320            user_commitment,
321            proof_challenge,
322            proof_response,
323        })
324    }
325
326    /// Create a presentation of the credential
327    pub fn create_presentation(
328        &self,
329        credential: &AnonymousCredential,
330        revealed_attributes: &HashSet<String>,
331    ) -> AnonCredResult<CredentialPresentation> {
332        // Extract revealed attributes
333        let mut revealed_attrs = HashMap::new();
334        for attr_name in revealed_attributes {
335            if let Some(attr_value) = credential.attributes.get(attr_name) {
336                revealed_attrs.insert(attr_name.clone(), attr_value.clone());
337            }
338        }
339
340        // Nonce prevents replay attacks
341        let nonce = rand::random::<[u8; 32]>();
342
343        Ok(CredentialPresentation {
344            signature: credential.signature,
345            commitment: credential.commitment,
346            revealed_attributes: revealed_attrs,
347            nonce,
348        })
349    }
350}
351
352impl Default for User {
353    fn default() -> Self {
354        Self::new()
355    }
356}
357
358// Serde helpers
359mod serde_verifying_key {
360    use super::*;
361    use serde::{Deserializer, Serializer};
362
363    pub fn serialize<S>(key: &VerifyingKey, serializer: S) -> Result<S::Ok, S::Error>
364    where
365        S: Serializer,
366    {
367        serializer.serialize_bytes(key.as_bytes())
368    }
369
370    pub fn deserialize<'de, D>(deserializer: D) -> Result<VerifyingKey, D::Error>
371    where
372        D: Deserializer<'de>,
373    {
374        let bytes: Vec<u8> = Deserialize::deserialize(deserializer)?;
375        if bytes.len() != 32 {
376            return Err(serde::de::Error::custom("invalid key length"));
377        }
378        let mut arr = [0u8; 32];
379        arr.copy_from_slice(&bytes);
380        VerifyingKey::from_bytes(&arr).map_err(serde::de::Error::custom)
381    }
382}
383
384mod serde_signature {
385    use super::*;
386    use serde::{Deserializer, Serializer};
387
388    pub fn serialize<S>(sig: &Signature, serializer: S) -> Result<S::Ok, S::Error>
389    where
390        S: Serializer,
391    {
392        serializer.serialize_bytes(&sig.to_bytes())
393    }
394
395    pub fn deserialize<'de, D>(deserializer: D) -> Result<Signature, D::Error>
396    where
397        D: Deserializer<'de>,
398    {
399        let bytes: Vec<u8> = Deserialize::deserialize(deserializer)?;
400        if bytes.len() != 64 {
401            return Err(serde::de::Error::custom("invalid signature length"));
402        }
403        let mut arr = [0u8; 64];
404        arr.copy_from_slice(&bytes);
405        Ok(Signature::from_bytes(&arr))
406    }
407}
408
409mod serde_point {
410    use super::*;
411    use curve25519_dalek::ristretto::CompressedRistretto;
412    use serde::{Deserializer, Serializer};
413
414    pub fn serialize<S>(point: &RistrettoPoint, serializer: S) -> Result<S::Ok, S::Error>
415    where
416        S: Serializer,
417    {
418        serializer.serialize_bytes(&point.compress().to_bytes())
419    }
420
421    pub fn deserialize<'de, D>(deserializer: D) -> Result<RistrettoPoint, D::Error>
422    where
423        D: Deserializer<'de>,
424    {
425        let bytes: Vec<u8> = Deserialize::deserialize(deserializer)?;
426        if bytes.len() != 32 {
427            return Err(serde::de::Error::custom("invalid point length"));
428        }
429        let mut arr = [0u8; 32];
430        arr.copy_from_slice(&bytes);
431        CompressedRistretto(arr)
432            .decompress()
433            .ok_or_else(|| serde::de::Error::custom("invalid point"))
434    }
435}
436
437mod serde_point_vec {
438    use super::*;
439    use curve25519_dalek::ristretto::CompressedRistretto;
440    use serde::{Deserializer, Serializer};
441
442    pub fn serialize<S>(points: &[RistrettoPoint], serializer: S) -> Result<S::Ok, S::Error>
443    where
444        S: Serializer,
445    {
446        let bytes: Vec<Vec<u8>> = points
447            .iter()
448            .map(|p| p.compress().to_bytes().to_vec())
449            .collect();
450        bytes.serialize(serializer)
451    }
452
453    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<RistrettoPoint>, D::Error>
454    where
455        D: Deserializer<'de>,
456    {
457        let bytes_vec: Vec<Vec<u8>> = Deserialize::deserialize(deserializer)?;
458        bytes_vec
459            .into_iter()
460            .map(|bytes| {
461                if bytes.len() != 32 {
462                    return Err(serde::de::Error::custom("invalid point length"));
463                }
464                let mut arr = [0u8; 32];
465                arr.copy_from_slice(&bytes);
466                CompressedRistretto(arr)
467                    .decompress()
468                    .ok_or_else(|| serde::de::Error::custom("invalid point"))
469            })
470            .collect()
471    }
472}
473
474mod serde_scalar {
475    use super::*;
476    use serde::{Deserializer, Serializer};
477
478    pub fn serialize<S>(scalar: &Scalar, serializer: S) -> Result<S::Ok, S::Error>
479    where
480        S: Serializer,
481    {
482        serializer.serialize_bytes(&scalar.to_bytes())
483    }
484
485    pub fn deserialize<'de, D>(deserializer: D) -> Result<Scalar, D::Error>
486    where
487        D: Deserializer<'de>,
488    {
489        let bytes: Vec<u8> = Deserialize::deserialize(deserializer)?;
490        if bytes.len() != 32 {
491            return Err(serde::de::Error::custom("invalid scalar length"));
492        }
493        let mut arr = [0u8; 32];
494        arr.copy_from_slice(&bytes);
495        Ok(Scalar::from_bytes_mod_order(arr))
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn test_basic_credential_flow() {
505        let issuer = Issuer::new();
506        let user = User::new();
507
508        // User requests credential
509        let request = user.create_credential_request(issuer.public_key()).unwrap();
510
511        // Issuer issues credential
512        let mut attributes = HashMap::new();
513        attributes.insert("age".to_string(), vec![18]);
514        attributes.insert("country".to_string(), vec![1]);
515
516        let credential = issuer.issue_credential(&request, &attributes).unwrap();
517
518        // Verify credential has attributes
519        assert!(credential.attributes.contains_key("age"));
520        assert!(credential.attributes.contains_key("country"));
521    }
522
523    #[test]
524    fn test_credential_presentation() {
525        let issuer = Issuer::new();
526        let user = User::new();
527
528        let request = user.create_credential_request(issuer.public_key()).unwrap();
529
530        let mut attributes = HashMap::new();
531        attributes.insert("age".to_string(), vec![25]);
532        attributes.insert("country".to_string(), vec![2]);
533        attributes.insert("role".to_string(), vec![3]);
534
535        let credential = issuer.issue_credential(&request, &attributes).unwrap();
536
537        // User creates presentation revealing only age
538        let mut revealed = HashSet::new();
539        revealed.insert("age".to_string());
540
541        let presentation = user.create_presentation(&credential, &revealed).unwrap();
542
543        // Verify presentation
544        assert!(
545            issuer
546                .verify_presentation(&presentation, &revealed)
547                .unwrap()
548        );
549
550        // Presentation should only contain revealed attribute
551        assert_eq!(presentation.revealed_attributes.len(), 1);
552        assert!(presentation.revealed_attributes.contains_key("age"));
553    }
554
555    #[test]
556    fn test_unlinkable_presentations() {
557        let issuer = Issuer::new();
558        let user = User::new();
559
560        let request = user.create_credential_request(issuer.public_key()).unwrap();
561
562        let mut attributes = HashMap::new();
563        attributes.insert("age".to_string(), vec![30]);
564
565        let credential = issuer.issue_credential(&request, &attributes).unwrap();
566
567        let mut revealed = HashSet::new();
568        revealed.insert("age".to_string());
569
570        // Create two presentations from same credential
571        let presentation1 = user.create_presentation(&credential, &revealed).unwrap();
572        let presentation2 = user.create_presentation(&credential, &revealed).unwrap();
573
574        // Both should verify
575        assert!(
576            issuer
577                .verify_presentation(&presentation1, &revealed)
578                .unwrap()
579        );
580        assert!(
581            issuer
582                .verify_presentation(&presentation2, &revealed)
583                .unwrap()
584        );
585
586        // But nonces should be different (prevents replay)
587        assert_ne!(presentation1.nonce, presentation2.nonce);
588    }
589
590    #[test]
591    fn test_selective_disclosure() {
592        let issuer = Issuer::new();
593        let user = User::new();
594
595        let request = user.create_credential_request(issuer.public_key()).unwrap();
596
597        let mut attributes = HashMap::new();
598        attributes.insert("age".to_string(), vec![21]);
599        attributes.insert("country".to_string(), vec![5]);
600        attributes.insert("role".to_string(), vec![7]);
601
602        let credential = issuer.issue_credential(&request, &attributes).unwrap();
603
604        // Reveal only age and role, not country
605        let mut revealed = HashSet::new();
606        revealed.insert("age".to_string());
607        revealed.insert("role".to_string());
608
609        let presentation = user.create_presentation(&credential, &revealed).unwrap();
610
611        assert!(
612            issuer
613                .verify_presentation(&presentation, &revealed)
614                .unwrap()
615        );
616
617        // Country should not be in presentation
618        assert!(!presentation.revealed_attributes.contains_key("country"));
619        assert_eq!(presentation.revealed_attributes.len(), 2);
620    }
621
622    #[test]
623    fn test_missing_attribute_verification() {
624        let issuer = Issuer::new();
625        let user = User::new();
626
627        let request = user.create_credential_request(issuer.public_key()).unwrap();
628
629        let mut attributes = HashMap::new();
630        attributes.insert("age".to_string(), vec![18]);
631
632        let credential = issuer.issue_credential(&request, &attributes).unwrap();
633
634        let mut revealed = HashSet::new();
635        revealed.insert("age".to_string());
636
637        let presentation = user.create_presentation(&credential, &revealed).unwrap();
638
639        // Try to verify with expected attribute that wasn't revealed
640        let mut expected = HashSet::new();
641        expected.insert("age".to_string());
642        expected.insert("country".to_string()); // Not in presentation
643
644        let result = issuer.verify_presentation(&presentation, &expected);
645        assert!(result.is_err());
646    }
647
648    #[test]
649    fn test_credential_revocation() {
650        let mut issuer = Issuer::new();
651        let user = User::new();
652
653        let request = user.create_credential_request(issuer.public_key()).unwrap();
654
655        let mut attributes = HashMap::new();
656        attributes.insert("age".to_string(), vec![18]);
657
658        let credential = issuer.issue_credential(&request, &attributes).unwrap();
659
660        let commitment_bytes = credential.commitment.compress().to_bytes();
661
662        // Check not revoked initially
663        assert!(!issuer.is_revoked(&commitment_bytes));
664
665        // Revoke the credential
666        issuer.revoke_credential(&commitment_bytes);
667
668        // Check is revoked
669        assert!(issuer.is_revoked(&commitment_bytes));
670    }
671
672    #[test]
673    fn test_custom_attributes() {
674        let custom_attrs = vec!["email_verified".to_string(), "premium_member".to_string()];
675        let issuer = Issuer::with_attributes(custom_attrs);
676
677        let user = User::new();
678        let request = user.create_credential_request(issuer.public_key()).unwrap();
679
680        let mut attributes = HashMap::new();
681        attributes.insert("email_verified".to_string(), vec![1]);
682        attributes.insert("premium_member".to_string(), vec![1]);
683
684        let credential = issuer.issue_credential(&request, &attributes).unwrap();
685
686        let mut revealed = HashSet::new();
687        revealed.insert("premium_member".to_string());
688
689        let presentation = user.create_presentation(&credential, &revealed).unwrap();
690
691        assert!(
692            issuer
693                .verify_presentation(&presentation, &revealed)
694                .unwrap()
695        );
696    }
697
698    #[test]
699    fn test_serialization() {
700        let issuer = Issuer::new();
701        let user = User::new();
702
703        let request = user.create_credential_request(issuer.public_key()).unwrap();
704
705        // Serialize and deserialize request
706        let request_bytes = crate::codec::encode(&request).unwrap();
707        let request_restored: CredentialRequest = crate::codec::decode(&request_bytes).unwrap();
708
709        let mut attributes = HashMap::new();
710        attributes.insert("age".to_string(), vec![22]);
711
712        let credential = issuer
713            .issue_credential(&request_restored, &attributes)
714            .unwrap();
715
716        // Serialize and deserialize credential
717        let cred_bytes = crate::codec::encode(&credential).unwrap();
718        let cred_restored: AnonymousCredential = crate::codec::decode(&cred_bytes).unwrap();
719
720        let mut revealed = HashSet::new();
721        revealed.insert("age".to_string());
722
723        let presentation = user.create_presentation(&cred_restored, &revealed).unwrap();
724
725        // Serialize and deserialize presentation
726        let pres_bytes = crate::codec::encode(&presentation).unwrap();
727        let pres_restored: CredentialPresentation = crate::codec::decode(&pres_bytes).unwrap();
728
729        assert!(
730            issuer
731                .verify_presentation(&pres_restored, &revealed)
732                .unwrap()
733        );
734    }
735
736    #[test]
737    fn test_empty_attributes() {
738        let issuer = Issuer::new();
739        let user = User::new();
740
741        let request = user.create_credential_request(issuer.public_key()).unwrap();
742
743        let attributes = HashMap::new(); // No attributes
744
745        let credential = issuer.issue_credential(&request, &attributes).unwrap();
746
747        let revealed = HashSet::new(); // Reveal nothing
748
749        let presentation = user.create_presentation(&credential, &revealed).unwrap();
750
751        assert!(
752            issuer
753                .verify_presentation(&presentation, &revealed)
754                .unwrap()
755        );
756    }
757
758    #[test]
759    fn test_public_key_serialization() {
760        let issuer = Issuer::new();
761
762        let pk_bytes = crate::codec::encode(&issuer.public_key()).unwrap();
763        let pk_restored: IssuerPublicKey = crate::codec::decode(&pk_bytes).unwrap();
764
765        assert_eq!(
766            pk_restored.attribute_names.len(),
767            issuer.public_key().attribute_names.len()
768        );
769    }
770
771    #[test]
772    fn test_multiple_users_same_issuer() {
773        let issuer = Issuer::new();
774
775        let user1 = User::new();
776        let user2 = User::new();
777
778        let request1 = user1
779            .create_credential_request(issuer.public_key())
780            .unwrap();
781        let request2 = user2
782            .create_credential_request(issuer.public_key())
783            .unwrap();
784
785        let mut attrs1 = HashMap::new();
786        attrs1.insert("age".to_string(), vec![20]);
787
788        let mut attrs2 = HashMap::new();
789        attrs2.insert("age".to_string(), vec![30]);
790
791        let cred1 = issuer.issue_credential(&request1, &attrs1).unwrap();
792        let cred2 = issuer.issue_credential(&request2, &attrs2).unwrap();
793
794        // Credentials should be different
795        assert_ne!(cred1.commitment, cred2.commitment);
796
797        let mut revealed = HashSet::new();
798        revealed.insert("age".to_string());
799
800        let pres1 = user1.create_presentation(&cred1, &revealed).unwrap();
801        let pres2 = user2.create_presentation(&cred2, &revealed).unwrap();
802
803        assert!(issuer.verify_presentation(&pres1, &revealed).unwrap());
804        assert!(issuer.verify_presentation(&pres2, &revealed).unwrap());
805
806        // Revealed attributes should be different
807        assert_ne!(
808            pres1.revealed_attributes.get("age"),
809            pres2.revealed_attributes.get("age")
810        );
811    }
812}