1use baseid_core::error::CryptoError;
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::collections::BTreeMap;
7
8use crate::mdoc::MobileDocument;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct MobileSecurityObject {
13 pub version: String,
15 pub digest_algorithm: String,
17 pub value_digests: BTreeMap<String, Vec<DigestEntry>>,
19 pub doc_type: String,
21 pub validity_info: ValidityInfo,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct DigestEntry {
28 pub digest_id: u64,
29 pub digest: Vec<u8>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ValidityInfo {
35 pub signed: String,
36 pub valid_from: String,
37 pub valid_until: String,
38}
39
40impl MobileSecurityObject {
41 pub fn create(doc: &MobileDocument, validity_info: ValidityInfo) -> baseid_core::Result<Self> {
44 let mut value_digests = BTreeMap::new();
45
46 for (namespace, elements) in &doc.namespaces {
47 let mut entries = Vec::new();
48 for (idx, element) in elements.iter().enumerate() {
49 let cbor = element.to_cbor()?;
50 let hash = Sha256::digest(&cbor);
51 entries.push(DigestEntry {
52 digest_id: idx as u64,
53 digest: hash.to_vec(),
54 });
55 }
56 value_digests.insert(namespace.clone(), entries);
57 }
58
59 Ok(Self {
60 version: "1.0".to_string(),
61 digest_algorithm: "SHA-256".to_string(),
62 value_digests,
63 doc_type: doc.doc_type.clone(),
64 validity_info,
65 })
66 }
67
68 pub fn verify_digests(&self, doc: &MobileDocument) -> baseid_core::Result<bool> {
80 if self.doc_type != doc.doc_type {
81 return Err(CryptoError::VerificationFailed.into());
82 }
83
84 for (namespace, elements) in &doc.namespaces {
85 let mso_entries = self
86 .value_digests
87 .get(namespace)
88 .ok_or(CryptoError::VerificationFailed)?;
89
90 let mso_digests: Vec<&[u8]> = mso_entries.iter().map(|e| e.digest.as_slice()).collect();
92
93 for element in elements {
94 let cbor = element.to_cbor()?;
95 let hash = Sha256::digest(&cbor);
96
97 if !mso_digests.iter().any(|d| *d == &hash[..]) {
98 return Ok(false);
99 }
100 }
101 }
102
103 Ok(true)
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::mdoc::{DataElement, MobileDocument};
111
112 fn sample_doc() -> MobileDocument {
113 let mut namespaces = BTreeMap::new();
114 namespaces.insert(
115 "org.iso.18013.5.1".to_string(),
116 vec![
117 DataElement {
118 identifier: "family_name".to_string(),
119 value: serde_json::json!("Doe"),
120 },
121 DataElement {
122 identifier: "given_name".to_string(),
123 value: serde_json::json!("John"),
124 },
125 ],
126 );
127 MobileDocument {
128 doc_type: "org.iso.18013.5.1.mDL".to_string(),
129 namespaces,
130 }
131 }
132
133 fn sample_validity() -> ValidityInfo {
134 ValidityInfo {
135 signed: "2024-01-01T00:00:00Z".to_string(),
136 valid_from: "2024-01-01T00:00:00Z".to_string(),
137 valid_until: "2025-01-01T00:00:00Z".to_string(),
138 }
139 }
140
141 #[test]
142 fn mso_create_and_verify() {
143 let doc = sample_doc();
144 let mso = MobileSecurityObject::create(&doc, sample_validity()).unwrap();
145
146 assert_eq!(mso.doc_type, doc.doc_type);
147 assert_eq!(mso.digest_algorithm, "SHA-256");
148 assert_eq!(mso.value_digests.len(), 1);
149
150 let entries = &mso.value_digests["org.iso.18013.5.1"];
151 assert_eq!(entries.len(), 2);
152 assert_eq!(entries[0].digest.len(), 32); assert!(mso.verify_digests(&doc).unwrap());
155 }
156
157 #[test]
158 fn tampered_element_detected() {
159 let doc = sample_doc();
160 let mso = MobileSecurityObject::create(&doc, sample_validity()).unwrap();
161
162 let mut tampered = doc.clone();
164 tampered.namespaces.get_mut("org.iso.18013.5.1").unwrap()[0].value =
165 serde_json::json!("Smith");
166
167 assert!(!mso.verify_digests(&tampered).unwrap());
168 }
169
170 #[test]
171 fn unknown_namespace_in_doc_detected() {
172 let doc = sample_doc();
173 let mso = MobileSecurityObject::create(&doc, sample_validity()).unwrap();
174
175 let mut bad_doc = MobileDocument {
177 doc_type: "org.iso.18013.5.1.mDL".to_string(),
178 namespaces: BTreeMap::new(),
179 };
180 bad_doc.namespaces.insert(
181 "org.unknown.namespace".to_string(),
182 vec![DataElement {
183 identifier: "field".to_string(),
184 value: serde_json::json!("value"),
185 }],
186 );
187
188 assert!(mso.verify_digests(&bad_doc).is_err());
189 }
190
191 #[test]
192 fn empty_doc_passes_trivially() {
193 let doc = sample_doc();
194 let mso = MobileSecurityObject::create(&doc, sample_validity()).unwrap();
195
196 let empty_doc = MobileDocument {
198 doc_type: "org.iso.18013.5.1.mDL".to_string(),
199 namespaces: BTreeMap::new(),
200 };
201
202 assert!(mso.verify_digests(&empty_doc).unwrap());
203 }
204
205 #[test]
206 fn selective_disclosure_subset_passes() {
207 let doc = sample_doc(); let mso = MobileSecurityObject::create(&doc, sample_validity()).unwrap();
209
210 let mut subset = BTreeMap::new();
212 subset.insert(
213 "org.iso.18013.5.1".to_string(),
214 vec![DataElement {
215 identifier: "family_name".to_string(),
216 value: serde_json::json!("Doe"),
217 }],
218 );
219 let subset_doc = MobileDocument {
220 doc_type: "org.iso.18013.5.1.mDL".to_string(),
221 namespaces: subset,
222 };
223
224 assert!(mso.verify_digests(&subset_doc).unwrap());
225 }
226
227 #[test]
228 fn selective_disclosure_with_tampered_element_fails() {
229 let doc = sample_doc();
230 let mso = MobileSecurityObject::create(&doc, sample_validity()).unwrap();
231
232 let mut subset = BTreeMap::new();
234 subset.insert(
235 "org.iso.18013.5.1".to_string(),
236 vec![DataElement {
237 identifier: "family_name".to_string(),
238 value: serde_json::json!("TAMPERED"),
239 }],
240 );
241 let subset_doc = MobileDocument {
242 doc_type: "org.iso.18013.5.1.mDL".to_string(),
243 namespaces: subset,
244 };
245
246 assert!(!mso.verify_digests(&subset_doc).unwrap());
247 }
248
249 #[test]
250 fn forged_element_not_in_mso_fails() {
251 let doc = sample_doc();
252 let mso = MobileSecurityObject::create(&doc, sample_validity()).unwrap();
253
254 let mut forged = BTreeMap::new();
256 forged.insert(
257 "org.iso.18013.5.1".to_string(),
258 vec![DataElement {
259 identifier: "social_security_number".to_string(),
260 value: serde_json::json!("123-45-6789"),
261 }],
262 );
263 let forged_doc = MobileDocument {
264 doc_type: "org.iso.18013.5.1.mDL".to_string(),
265 namespaces: forged,
266 };
267
268 assert!(!mso.verify_digests(&forged_doc).unwrap());
269 }
270
271 #[test]
272 fn doc_type_mismatch_detected() {
273 let doc = sample_doc();
274 let mso = MobileSecurityObject::create(&doc, sample_validity()).unwrap();
275
276 let mut wrong_doc = doc.clone();
277 wrong_doc.doc_type = "wrong.type".to_string();
278
279 assert!(mso.verify_digests(&wrong_doc).is_err());
280 }
281
282 #[test]
283 fn multiple_namespaces() {
284 let mut namespaces = BTreeMap::new();
285 namespaces.insert(
286 "org.iso.18013.5.1".to_string(),
287 vec![DataElement {
288 identifier: "family_name".to_string(),
289 value: serde_json::json!("Doe"),
290 }],
291 );
292 namespaces.insert(
293 "org.iso.18013.5.1.aamva".to_string(),
294 vec![DataElement {
295 identifier: "DHS_compliance".to_string(),
296 value: serde_json::json!("F"),
297 }],
298 );
299 let doc = MobileDocument {
300 doc_type: "org.iso.18013.5.1.mDL".to_string(),
301 namespaces,
302 };
303
304 let mso = MobileSecurityObject::create(&doc, sample_validity()).unwrap();
305 assert_eq!(mso.value_digests.len(), 2);
306 assert!(mso.verify_digests(&doc).unwrap());
307 }
308}