1use std::collections::BTreeMap;
8
9use baseid_core::claims::{ClaimDisclosure, ClaimSet, DisclosureSelection};
10use baseid_core::error::{CredentialError, CryptoError, SerializationError};
11use baseid_core::lifecycle::{
12 CredentialIssuer, CredentialPresenter, CredentialVerifier, IssuanceOptions, IssuedCredential,
13 PresentationOptions, PresentedCredential, RevocationStatus, VerificationOutcome,
14};
15use baseid_core::types::CredentialFormat;
16use baseid_crypto::signer::{Signer, Verifier};
17use serde::{Deserialize, Serialize};
18
19use sha2::{Digest, Sha256};
20
21use crate::mdoc::{DataElement, MobileDocument};
22use crate::mso::{MobileSecurityObject, ValidityInfo};
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
31struct IssuerSigned {
32 #[serde(rename = "docType")]
33 doc_type: String,
34 namespaces: BTreeMap<String, Vec<DataElement>>,
35 #[serde(rename = "issuerAuth")]
36 issuer_auth: Vec<u8>,
37}
38
39pub struct MdocLifecycle<'a> {
44 signer: &'a dyn Signer,
45 verifier: &'a dyn Verifier,
46 doc_type: String,
47}
48
49impl<'a> MdocLifecycle<'a> {
50 pub fn new(signer: &'a dyn Signer, verifier: &'a dyn Verifier, doc_type: &str) -> Self {
57 Self {
58 signer,
59 verifier,
60 doc_type: doc_type.to_string(),
61 }
62 }
63
64 fn default_namespace(&self) -> String {
68 if let Some(pos) = self.doc_type.rfind('.') {
70 self.doc_type[..pos].to_string()
71 } else {
72 self.doc_type.clone()
73 }
74 }
75}
76
77impl CredentialIssuer for MdocLifecycle<'_> {
78 fn format(&self) -> CredentialFormat {
79 CredentialFormat::Mdl
80 }
81
82 fn issue(
83 &self,
84 issuer_did: &str,
85 subject_did: Option<&str>,
86 claims: &ClaimSet,
87 options: &IssuanceOptions,
88 ) -> baseid_core::Result<IssuedCredential> {
89 let default_ns = self.default_namespace();
90 let mut namespaces: BTreeMap<String, Vec<DataElement>> = BTreeMap::new();
91
92 for (ns_key, ns_claims) in claims.namespaces() {
94 let target_ns = if ns_key.is_empty() {
96 default_ns.clone()
97 } else {
98 ns_key.to_string()
99 };
100
101 let elements: Vec<DataElement> = ns_claims
102 .iter()
103 .map(|(name, value)| DataElement {
104 identifier: name.clone(),
105 value: value.clone(),
106 })
107 .collect();
108
109 namespaces.entry(target_ns).or_default().extend(elements);
110 }
111
112 let doc = MobileDocument {
113 doc_type: self.doc_type.clone(),
114 namespaces: namespaces.clone(),
115 };
116
117 let now = options
119 .valid_from
120 .clone()
121 .unwrap_or_else(|| "2024-01-01T00:00:00Z".to_string());
122 let valid_until = options
123 .valid_until
124 .clone()
125 .unwrap_or_else(|| "2099-12-31T23:59:59Z".to_string());
126
127 let validity = ValidityInfo {
128 signed: now.clone(),
129 valid_from: now,
130 valid_until,
131 };
132
133 let mso = MobileSecurityObject::create(&doc, validity)?;
135 let issuer_auth = crate::cose::sign_mso(&mso, self.signer)?;
136
137 let issuer_signed = IssuerSigned {
139 doc_type: self.doc_type.clone(),
140 namespaces,
141 issuer_auth,
142 };
143
144 let mut cbor_bytes = Vec::new();
145 ciborium::into_writer(&issuer_signed, &mut cbor_bytes)
146 .map_err(|_| SerializationError::Cbor)?;
147
148 Ok(IssuedCredential {
149 data: cbor_bytes,
150 format: CredentialFormat::Mdl,
151 id: options.credential_id.clone(),
152 issuer: issuer_did.to_string(),
153 subject: subject_did.map(|s| s.to_string()),
154 })
155 }
156}
157
158impl CredentialVerifier for MdocLifecycle<'_> {
159 fn format(&self) -> CredentialFormat {
160 CredentialFormat::Mdl
161 }
162
163 fn verify(&self, credential_data: &[u8]) -> baseid_core::Result<VerificationOutcome> {
164 let issuer_signed: IssuerSigned =
166 ciborium::from_reader(credential_data).map_err(|_| SerializationError::Cbor)?;
167
168 let mso = crate::cose::verify_signed_mso(&issuer_signed.issuer_auth, self.verifier)?;
170
171 if mso.doc_type != issuer_signed.doc_type {
175 return Err(CryptoError::VerificationFailed.into());
176 }
177
178 for (ns_key, elements) in &issuer_signed.namespaces {
179 let mso_entries = mso
180 .value_digests
181 .get(ns_key)
182 .ok_or(CryptoError::VerificationFailed)?;
183
184 let mso_digests: Vec<&[u8]> = mso_entries.iter().map(|e| e.digest.as_slice()).collect();
186
187 for element in elements {
188 let cbor = element.to_cbor()?;
189 let hash = Sha256::digest(&cbor);
190 if !mso_digests.iter().any(|d| *d == &hash[..]) {
191 return Err(CryptoError::VerificationFailed.into());
192 }
193 }
194 }
195
196 let default_ns = self.default_namespace();
198 let mut claim_set = ClaimSet::new();
199
200 for (ns_key, elements) in &issuer_signed.namespaces {
201 let claim_ns = if *ns_key == default_ns {
203 ""
204 } else {
205 ns_key.as_str()
206 };
207
208 for element in elements {
209 claim_set.insert(claim_ns, &element.identifier, element.value.clone());
210 }
211 }
212
213 let valid_until = Some(mso.validity_info.valid_until.clone());
215
216 Ok(VerificationOutcome {
217 valid: true,
218 format: CredentialFormat::Mdl,
219 issuer: String::new(), subject: None,
221 claims: claim_set,
222 unlinkable: false,
223 predicates_verified: vec![],
224 valid_until,
225 revocation_status: RevocationStatus::NotChecked,
226 })
227 }
228}
229
230impl CredentialPresenter for MdocLifecycle<'_> {
231 fn format(&self) -> CredentialFormat {
232 CredentialFormat::Mdl
233 }
234
235 fn present(
236 &self,
237 credential_data: &[u8],
238 disclosure: &DisclosureSelection,
239 _options: &PresentationOptions,
240 ) -> baseid_core::Result<PresentedCredential> {
241 if disclosure.has_predicates() {
243 return Err(CredentialError::UnsupportedPredicate.into());
244 }
245
246 let issuer_signed: IssuerSigned =
248 ciborium::from_reader(credential_data).map_err(|_| SerializationError::Cbor)?;
249
250 let default_ns = self.default_namespace();
251
252 let mut filtered_namespaces: BTreeMap<String, Vec<DataElement>> = BTreeMap::new();
254
255 for (ns_key, elements) in &issuer_signed.namespaces {
256 let mut kept = Vec::new();
257
258 for element in elements {
259 let namespaced_disc = disclosure.get(ns_key, &element.identifier);
261 let default_disc = if *ns_key == default_ns {
262 disclosure.get("", &element.identifier)
263 } else {
264 None
265 };
266
267 let disc = namespaced_disc.or(default_disc);
268
269 match disc {
270 Some(ClaimDisclosure::Hide) => {
271 }
273 Some(ClaimDisclosure::Reveal) | None => {
274 kept.push(element.clone());
276 }
277 Some(ClaimDisclosure::Predicate(_)) => {
278 return Err(CredentialError::UnsupportedPredicate.into());
280 }
281 }
282 }
283
284 if !kept.is_empty() {
285 filtered_namespaces.insert(ns_key.clone(), kept);
286 }
287 }
288
289 let presented = IssuerSigned {
291 doc_type: issuer_signed.doc_type,
292 namespaces: filtered_namespaces,
293 issuer_auth: issuer_signed.issuer_auth,
294 };
295
296 let mut cbor_bytes = Vec::new();
297 ciborium::into_writer(&presented, &mut cbor_bytes).map_err(|_| SerializationError::Cbor)?;
298
299 Ok(PresentedCredential {
300 data: cbor_bytes,
301 format: CredentialFormat::Mdl,
302 unlinkable: false,
303 })
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use baseid_core::claims::PredicateType;
311 use baseid_core::types::KeyType;
312 use baseid_crypto::KeyPair;
313 use serde_json::json;
314
315 fn setup() -> (KeyPair, ClaimSet) {
316 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
317 let mut claims = ClaimSet::new();
318 claims.insert("", "family_name", json!("Doe"));
319 claims.insert("", "given_name", json!("John"));
320 claims.insert("", "document_number", json!("DL-123456"));
321 claims.insert("", "birth_date", json!("1990-01-15"));
322 (kp, claims)
323 }
324
325 fn make_lifecycle(kp: &KeyPair) -> MdocLifecycle<'_> {
326 MdocLifecycle::new(kp, &kp.public, "org.iso.18013.5.1.mDL")
327 }
328
329 #[test]
330 fn issue_and_verify() {
331 let (kp, claims) = setup();
332 let lifecycle = make_lifecycle(&kp);
333
334 let issued = lifecycle
335 .issue(
336 "did:key:issuer",
337 Some("did:key:holder"),
338 &claims,
339 &IssuanceOptions::default(),
340 )
341 .unwrap();
342
343 assert_eq!(issued.format, CredentialFormat::Mdl);
344 assert_eq!(issued.issuer, "did:key:issuer");
345 assert_eq!(issued.subject, Some("did:key:holder".to_string()));
346
347 let outcome = lifecycle.verify(&issued.data).unwrap();
348 assert!(outcome.valid);
349 assert_eq!(outcome.format, CredentialFormat::Mdl);
350 assert_eq!(outcome.claims.get("", "family_name"), Some(&json!("Doe")));
351 assert_eq!(outcome.claims.get("", "given_name"), Some(&json!("John")));
352 assert_eq!(
353 outcome.claims.get("", "document_number"),
354 Some(&json!("DL-123456"))
355 );
356 assert!(!outcome.unlinkable);
357 }
358
359 #[test]
360 fn issue_with_namespaced_claims() {
361 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
362 let lifecycle = make_lifecycle(&kp);
363
364 let mut claims = ClaimSet::new();
365 claims.insert("org.iso.18013.5.1", "family_name", json!("Smith"));
366 claims.insert("org.iso.18013.5.1", "given_name", json!("Alice"));
367 claims.insert("org.iso.18013.5.1.aamva", "DHS_compliance", json!("F"));
368
369 let issued = lifecycle
370 .issue("did:key:issuer", None, &claims, &IssuanceOptions::default())
371 .unwrap();
372
373 let outcome = lifecycle.verify(&issued.data).unwrap();
374 assert!(outcome.valid);
375 assert_eq!(outcome.claims.get("", "family_name"), Some(&json!("Smith")));
377 assert_eq!(outcome.claims.get("", "given_name"), Some(&json!("Alice")));
378 assert_eq!(
380 outcome
381 .claims
382 .get("org.iso.18013.5.1.aamva", "DHS_compliance"),
383 Some(&json!("F"))
384 );
385 }
386
387 #[test]
388 fn present_selective_disclosure() {
389 let (kp, claims) = setup();
390 let lifecycle = make_lifecycle(&kp);
391
392 let issued = lifecycle
393 .issue(
394 "did:key:issuer",
395 Some("did:key:holder"),
396 &claims,
397 &IssuanceOptions::default(),
398 )
399 .unwrap();
400
401 let disclosure = DisclosureSelection::new()
403 .reveal("family_name")
404 .reveal("given_name")
405 .hide("document_number")
406 .hide("birth_date");
407
408 let presented = lifecycle
409 .present(&issued.data, &disclosure, &PresentationOptions::default())
410 .unwrap();
411
412 assert_eq!(presented.format, CredentialFormat::Mdl);
413 assert!(!presented.unlinkable);
414
415 let outcome = lifecycle.verify(&presented.data).unwrap();
417 assert!(outcome.valid);
418 assert_eq!(outcome.claims.get("", "family_name"), Some(&json!("Doe")));
419 assert_eq!(outcome.claims.get("", "given_name"), Some(&json!("John")));
420 assert_eq!(outcome.claims.get("", "document_number"), None);
421 assert_eq!(outcome.claims.get("", "birth_date"), None);
422 }
423
424 #[test]
425 fn predicate_returns_unsupported() {
426 let (kp, claims) = setup();
427 let lifecycle = make_lifecycle(&kp);
428
429 let issued = lifecycle
430 .issue("did:key:issuer", None, &claims, &IssuanceOptions::default())
431 .unwrap();
432
433 let disclosure = DisclosureSelection::new()
434 .reveal("family_name")
435 .predicate("birth_date", PredicateType::LessThan(json!("2008-03-01")));
436
437 let result = lifecycle.present(&issued.data, &disclosure, &PresentationOptions::default());
438 assert!(result.is_err());
439
440 let err = result.unwrap_err();
441 assert!(
442 format!("{err}").contains("Predicate"),
443 "Error should mention predicate: {err}"
444 );
445 }
446
447 #[test]
448 fn issuance_with_options() {
449 let (kp, claims) = setup();
450 let lifecycle = make_lifecycle(&kp);
451
452 let opts = IssuanceOptions {
453 credential_id: Some("urn:uuid:mdl-1234".to_string()),
454 types: vec!["mDL".to_string()],
455 valid_from: Some("2024-01-01T00:00:00Z".to_string()),
456 valid_until: Some("2025-01-01T00:00:00Z".to_string()),
457 status: None,
458 };
459
460 let issued = lifecycle
461 .issue("did:key:issuer", Some("did:key:holder"), &claims, &opts)
462 .unwrap();
463
464 assert_eq!(issued.id, Some("urn:uuid:mdl-1234".to_string()));
465
466 let outcome = lifecycle.verify(&issued.data).unwrap();
467 assert_eq!(
468 outcome.valid_until,
469 Some("2025-01-01T00:00:00Z".to_string())
470 );
471 }
472
473 #[test]
474 fn format_method() {
475 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
476 let lifecycle = make_lifecycle(&kp);
477
478 assert_eq!(CredentialIssuer::format(&lifecycle), CredentialFormat::Mdl);
479 assert_eq!(
480 CredentialVerifier::format(&lifecycle),
481 CredentialFormat::Mdl
482 );
483 assert_eq!(
484 CredentialPresenter::format(&lifecycle),
485 CredentialFormat::Mdl
486 );
487 }
488}