baseid_anoncreds/
holder.rs

1//! AnonCreds credential holder for storing credentials and responding to proof requests.
2
3use std::collections::BTreeMap;
4
5use crate::credential::AnonCredsCredential;
6use crate::proof::{ProofRequest, ProofResponse, RevealedAttribute};
7
8/// A credential holder that stores AnonCreds credentials and matches them to proof requests.
9#[derive(Debug, Clone, Default)]
10pub struct CredentialHolder {
11    /// Stored credentials keyed by a local identifier.
12    credentials: BTreeMap<String, AnonCredsCredential>,
13    /// Auto-incrementing counter for generating credential IDs.
14    next_id: u64,
15}
16
17impl CredentialHolder {
18    /// Create a new, empty credential holder.
19    pub fn new() -> Self {
20        Self {
21            credentials: BTreeMap::new(),
22            next_id: 0,
23        }
24    }
25
26    /// Store a credential and return its local identifier.
27    pub fn store_credential(&mut self, credential: AnonCredsCredential) -> String {
28        let id = format!("anoncred-{}", self.next_id);
29        self.next_id += 1;
30        self.credentials.insert(id.clone(), credential);
31        id
32    }
33
34    /// Find all stored credentials that satisfy a proof request.
35    ///
36    /// Returns a list of `(credential_id, credential)` pairs.
37    pub fn find_credentials_for_request(
38        &self,
39        request: &ProofRequest,
40    ) -> Vec<(&str, &AnonCredsCredential)> {
41        self.credentials
42            .iter()
43            .filter(|(_, cred)| request.matches_credential(cred))
44            .map(|(id, cred)| (id.as_str(), cred))
45            .collect()
46    }
47
48    /// Build a proof response from a stored credential for a proof request.
49    ///
50    /// Reveals all requested attributes from the given credential. Returns `None`
51    /// if the credential does not match the request or is not found.
52    pub fn create_proof_response(
53        &self,
54        credential_id: &str,
55        request: &ProofRequest,
56    ) -> Option<ProofResponse> {
57        let credential = self.credentials.get(credential_id)?;
58
59        if !request.matches_credential(credential) {
60            return None;
61        }
62
63        let mut revealed_attrs = BTreeMap::new();
64
65        for (referent, attr) in &request.requested_attributes {
66            if let Some(ref name) = attr.name {
67                if let Some(value) = credential.values.get(name) {
68                    revealed_attrs.insert(
69                        referent.clone(),
70                        RevealedAttribute {
71                            sub_proof_index: 0,
72                            raw: value.raw.clone(),
73                            encoded: value.encoded.clone(),
74                        },
75                    );
76                }
77            }
78            if let Some(ref names) = attr.names {
79                // For grouped attributes, reveal each one with the same referent prefix.
80                for (i, name) in names.iter().enumerate() {
81                    if let Some(value) = credential.values.get(name) {
82                        let key = if names.len() == 1 {
83                            referent.clone()
84                        } else {
85                            format!("{referent}_{i}")
86                        };
87                        revealed_attrs.insert(
88                            key,
89                            RevealedAttribute {
90                                sub_proof_index: 0,
91                                raw: value.raw.clone(),
92                                encoded: value.encoded.clone(),
93                            },
94                        );
95                    }
96                }
97            }
98        }
99
100        Some(ProofResponse {
101            revealed_attrs,
102            self_attested_attrs: BTreeMap::new(),
103        })
104    }
105
106    /// Return the number of stored credentials.
107    pub fn len(&self) -> usize {
108        self.credentials.len()
109    }
110
111    /// Check whether the holder has no credentials.
112    pub fn is_empty(&self) -> bool {
113        self.credentials.is_empty()
114    }
115
116    /// Get a credential by its local identifier.
117    pub fn get(&self, id: &str) -> Option<&AnonCredsCredential> {
118        self.credentials.get(id)
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::credential::AttributeValue;
126    use crate::proof::{RequestedAttribute, RequestedPredicate};
127
128    fn sample_credential() -> AnonCredsCredential {
129        let mut values = BTreeMap::new();
130        values.insert(
131            "name".to_string(),
132            AttributeValue {
133                raw: "Alice".to_string(),
134                encoded: "123".to_string(),
135            },
136        );
137        values.insert(
138            "degree".to_string(),
139            AttributeValue {
140                raw: "Computer Science".to_string(),
141                encoded: "456".to_string(),
142            },
143        );
144        values.insert(
145            "university".to_string(),
146            AttributeValue {
147                raw: "University of Toronto".to_string(),
148                encoded: "789".to_string(),
149            },
150        );
151        AnonCredsCredential {
152            schema_id: "did:sov:NcYx:2:degree:1.0".to_string(),
153            cred_def_id: "did:sov:NcYx:3:CL:12:default".to_string(),
154            rev_reg_id: None,
155            issuer_did: "did:sov:NcYx".to_string(),
156            values,
157        }
158    }
159
160    fn sample_proof_request() -> ProofRequest {
161        let mut requested_attributes = BTreeMap::new();
162        requested_attributes.insert(
163            "attr1_referent".to_string(),
164            RequestedAttribute {
165                name: Some("name".to_string()),
166                names: None,
167                restrictions: None,
168            },
169        );
170        requested_attributes.insert(
171            "attr2_referent".to_string(),
172            RequestedAttribute {
173                name: Some("degree".to_string()),
174                names: None,
175                restrictions: None,
176            },
177        );
178
179        ProofRequest {
180            name: "degree-proof".to_string(),
181            version: "1.0".to_string(),
182            nonce: "12345".to_string(),
183            requested_attributes,
184            requested_predicates: BTreeMap::new(),
185        }
186    }
187
188    #[test]
189    fn store_and_retrieve() {
190        let mut holder = CredentialHolder::new();
191        assert!(holder.is_empty());
192
193        let id = holder.store_credential(sample_credential());
194        assert_eq!(holder.len(), 1);
195
196        let cred = holder.get(&id).unwrap();
197        assert_eq!(cred.get_raw_value("name"), Some("Alice"));
198    }
199
200    #[test]
201    fn find_credentials_for_request() {
202        let mut holder = CredentialHolder::new();
203        holder.store_credential(sample_credential());
204
205        let request = sample_proof_request();
206        let matches = holder.find_credentials_for_request(&request);
207        assert_eq!(matches.len(), 1);
208    }
209
210    #[test]
211    fn find_credentials_no_match() {
212        let mut holder = CredentialHolder::new();
213        holder.store_credential(sample_credential());
214
215        let mut requested_attributes = BTreeMap::new();
216        requested_attributes.insert(
217            "attr1_referent".to_string(),
218            RequestedAttribute {
219                name: Some("nonexistent".to_string()),
220                names: None,
221                restrictions: None,
222            },
223        );
224
225        let request = ProofRequest {
226            name: "bad-req".to_string(),
227            version: "1.0".to_string(),
228            nonce: "99999".to_string(),
229            requested_attributes,
230            requested_predicates: BTreeMap::new(),
231        };
232
233        let matches = holder.find_credentials_for_request(&request);
234        assert!(matches.is_empty());
235    }
236
237    #[test]
238    fn create_proof_response() {
239        let mut holder = CredentialHolder::new();
240        let id = holder.store_credential(sample_credential());
241
242        let request = sample_proof_request();
243        let response = holder.create_proof_response(&id, &request).unwrap();
244
245        assert_eq!(response.revealed_attrs.len(), 2);
246        assert_eq!(response.revealed_attrs["attr1_referent"].raw, "Alice");
247        assert_eq!(
248            response.revealed_attrs["attr2_referent"].raw,
249            "Computer Science"
250        );
251    }
252
253    #[test]
254    fn create_proof_response_missing_credential() {
255        let holder = CredentialHolder::new();
256        let request = sample_proof_request();
257        assert!(holder
258            .create_proof_response("nonexistent", &request)
259            .is_none());
260    }
261
262    #[test]
263    fn predicate_matching() {
264        let mut holder = CredentialHolder::new();
265        holder.store_credential(sample_credential());
266
267        let mut requested_predicates = BTreeMap::new();
268        requested_predicates.insert(
269            "pred1_referent".to_string(),
270            RequestedPredicate {
271                name: "name".to_string(),
272                p_type: ">=".to_string(),
273                p_value: 18,
274            },
275        );
276
277        let request = ProofRequest {
278            name: "pred-req".to_string(),
279            version: "1.0".to_string(),
280            nonce: "77777".to_string(),
281            requested_attributes: BTreeMap::new(),
282            requested_predicates,
283        };
284
285        let matches = holder.find_credentials_for_request(&request);
286        // The credential has a "name" attribute, so it matches the predicate field requirement
287        assert_eq!(matches.len(), 1);
288    }
289}