baseid_anoncreds/
credential.rs1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use baseid_vc::credential::Issuer;
8use baseid_vc::VerifiableCredential;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct AnonCredsCredential {
13 pub schema_id: String,
15 pub cred_def_id: String,
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub rev_reg_id: Option<String>,
20 pub issuer_did: String,
22 pub values: BTreeMap<String, AttributeValue>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct AttributeValue {
29 pub raw: String,
31 pub encoded: String,
33}
34
35impl AnonCredsCredential {
36 pub fn from_aca_py_json(json: &str) -> Result<Self, serde_json::Error> {
50 #[derive(Deserialize)]
51 struct AcaPyCredential {
52 schema_id: String,
53 cred_def_id: String,
54 rev_reg_id: Option<String>,
55 values: BTreeMap<String, AttributeValue>,
56 }
57
58 let parsed: AcaPyCredential = serde_json::from_str(json)?;
59
60 let issuer_did = extract_issuer_did(&parsed.cred_def_id);
64
65 Ok(Self {
66 schema_id: parsed.schema_id,
67 cred_def_id: parsed.cred_def_id,
68 rev_reg_id: parsed.rev_reg_id,
69 issuer_did,
70 values: parsed.values,
71 })
72 }
73
74 pub fn to_w3c_vc(&self) -> VerifiableCredential {
79 let mut subject = serde_json::Map::new();
80 for (name, value) in &self.values {
81 subject.insert(name.clone(), serde_json::Value::String(value.raw.clone()));
82 }
83
84 let schema_name = extract_schema_name(&self.schema_id);
87 let credential_type = schema_name
88 .map(|n| format!("AnonCreds:{n}"))
89 .unwrap_or_else(|| "AnonCredsCredential".to_string());
90
91 VerifiableCredential {
92 context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
93 id: Some(self.cred_def_id.clone()),
94 r#type: vec!["VerifiableCredential".to_string(), credential_type],
95 issuer: Issuer::Uri(self.issuer_did.clone()),
96 valid_from: None,
97 valid_until: None,
98 credential_subject: serde_json::Value::Object(subject),
99 credential_status: None,
100 proof: None,
101 }
102 }
103
104 pub fn attribute_names(&self) -> Vec<&str> {
106 self.values.keys().map(|k| k.as_str()).collect()
107 }
108
109 pub fn get_raw_value(&self, name: &str) -> Option<&str> {
111 self.values.get(name).map(|v| v.raw.as_str())
112 }
113}
114
115fn extract_issuer_did(cred_def_id: &str) -> String {
120 if cred_def_id.starts_with("did:") {
121 let parts: Vec<&str> = cred_def_id.splitn(4, ':').collect();
123 if parts.len() >= 3 {
124 return format!("{}:{}:{}", parts[0], parts[1], parts[2]);
125 }
126 }
127 cred_def_id
129 .split(':')
130 .next()
131 .unwrap_or(cred_def_id)
132 .to_string()
133}
134
135fn extract_schema_name(schema_id: &str) -> Option<String> {
140 let parts: Vec<&str> = schema_id.split(':').collect();
141 if parts.len() >= 4 {
144 return Some(parts[parts.len() - 2].to_string());
146 }
147 None
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 const ACA_PY_JSON: &str = r#"{
155 "schema_id": "did:sov:NcYxiDXkpYi6ov5FcYDi1e:2:degree:1.0",
156 "cred_def_id": "did:sov:NcYxiDXkpYi6ov5FcYDi1e:3:CL:12:default",
157 "rev_reg_id": null,
158 "values": {
159 "name": {"raw": "Alice", "encoded": "27034640024117331033063128044004318218486816931520886405535659934417438781507"},
160 "degree": {"raw": "Computer Science", "encoded": "123456"},
161 "university": {"raw": "University of Toronto", "encoded": "789012"}
162 }
163 }"#;
164
165 #[test]
166 fn parse_aca_py_json() {
167 let cred = AnonCredsCredential::from_aca_py_json(ACA_PY_JSON).unwrap();
168 assert_eq!(
169 cred.schema_id,
170 "did:sov:NcYxiDXkpYi6ov5FcYDi1e:2:degree:1.0"
171 );
172 assert_eq!(
173 cred.cred_def_id,
174 "did:sov:NcYxiDXkpYi6ov5FcYDi1e:3:CL:12:default"
175 );
176 assert!(cred.rev_reg_id.is_none());
177 assert_eq!(cred.issuer_did, "did:sov:NcYxiDXkpYi6ov5FcYDi1e");
178 assert_eq!(cred.values.len(), 3);
179 }
180
181 #[test]
182 fn attribute_access() {
183 let cred = AnonCredsCredential::from_aca_py_json(ACA_PY_JSON).unwrap();
184 assert_eq!(cred.get_raw_value("name"), Some("Alice"));
185 assert_eq!(cred.get_raw_value("degree"), Some("Computer Science"));
186 assert_eq!(cred.get_raw_value("missing"), None);
187
188 let names = cred.attribute_names();
189 assert!(names.contains(&"name"));
190 assert!(names.contains(&"degree"));
191 assert!(names.contains(&"university"));
192 }
193
194 #[test]
195 fn to_w3c_vc_mapping() {
196 let cred = AnonCredsCredential::from_aca_py_json(ACA_PY_JSON).unwrap();
197 let vc = cred.to_w3c_vc();
198
199 assert_eq!(
200 vc.context,
201 vec!["https://www.w3.org/ns/credentials/v2".to_string()]
202 );
203 assert!(vc.r#type.contains(&"VerifiableCredential".to_string()));
204 assert!(vc.r#type.contains(&"AnonCreds:degree".to_string()));
205 assert_eq!(
206 vc.issuer,
207 Issuer::Uri("did:sov:NcYxiDXkpYi6ov5FcYDi1e".to_string())
208 );
209 assert_eq!(vc.credential_subject["name"], "Alice");
210 assert_eq!(vc.credential_subject["degree"], "Computer Science");
211 assert_eq!(vc.credential_subject["university"], "University of Toronto");
212 }
213
214 #[test]
215 fn serde_roundtrip() {
216 let cred = AnonCredsCredential::from_aca_py_json(ACA_PY_JSON).unwrap();
217 let json = serde_json::to_string(&cred).unwrap();
218 let deserialized: AnonCredsCredential = serde_json::from_str(&json).unwrap();
219 assert_eq!(deserialized.schema_id, cred.schema_id);
220 assert_eq!(deserialized.issuer_did, cred.issuer_did);
221 assert_eq!(deserialized.values.len(), cred.values.len());
222 }
223
224 #[test]
225 fn extract_issuer_did_bare_sovrin() {
226 let issuer = super::extract_issuer_did("NcYxiDXkpYi6ov5FcYDi1e:3:CL:12:default");
227 assert_eq!(issuer, "NcYxiDXkpYi6ov5FcYDi1e");
228 }
229}