1use roxmltree::Node;
13
14use super::digest::{DigestAlgorithm, compute_digest, constant_time_eq};
15use super::parse::Reference;
16use super::transforms::execute_transforms;
17use super::uri::UriReferenceResolver;
18
19#[derive(Debug)]
21pub struct ReferenceResult {
22 pub uri: Option<String>,
24 pub digest_algorithm: DigestAlgorithm,
26 pub valid: bool,
28 pub pre_digest_data: Option<Vec<u8>>,
30}
31
32#[derive(Debug)]
34pub struct ReferencesResult {
35 pub results: Vec<ReferenceResult>,
38 pub first_failure: Option<usize>,
40}
41
42impl ReferencesResult {
43 #[must_use]
45 pub fn all_valid(&self) -> bool {
46 self.first_failure.is_none()
47 }
48}
49
50pub fn process_reference(
65 reference: &Reference,
66 resolver: &UriReferenceResolver<'_>,
67 signature_node: Node<'_, '_>,
68 store_pre_digest: bool,
69) -> Result<ReferenceResult, ReferenceProcessingError> {
70 let uri = reference
73 .uri
74 .as_deref()
75 .ok_or(ReferenceProcessingError::MissingUri)?;
76 let initial_data = resolver
77 .dereference(uri)
78 .map_err(ReferenceProcessingError::UriDereference)?;
79
80 let pre_digest_bytes = execute_transforms(signature_node, initial_data, &reference.transforms)
82 .map_err(ReferenceProcessingError::Transform)?;
83
84 let computed_digest = compute_digest(reference.digest_method, &pre_digest_bytes);
86
87 let valid = constant_time_eq(&computed_digest, &reference.digest_value);
89
90 Ok(ReferenceResult {
91 uri: reference.uri.clone(),
92 digest_algorithm: reference.digest_method,
93 valid,
94 pre_digest_data: if store_pre_digest {
95 Some(pre_digest_bytes)
96 } else {
97 None
98 },
99 })
100}
101
102pub fn process_all_references(
114 references: &[Reference],
115 resolver: &UriReferenceResolver<'_>,
116 signature_node: Node<'_, '_>,
117 store_pre_digest: bool,
118) -> Result<ReferencesResult, ReferenceProcessingError> {
119 let mut results = Vec::with_capacity(references.len());
120
121 for (i, reference) in references.iter().enumerate() {
122 let result = process_reference(reference, resolver, signature_node, store_pre_digest)?;
123 let failed = !result.valid;
124 results.push(result);
125
126 if failed {
127 return Ok(ReferencesResult {
128 results,
129 first_failure: Some(i),
130 });
131 }
132 }
133
134 Ok(ReferencesResult {
135 results,
136 first_failure: None,
137 })
138}
139
140#[derive(Debug, thiserror::Error)]
144#[non_exhaustive]
145pub enum ReferenceProcessingError {
146 #[error("reference URI is required; omitted URI references are not supported")]
148 MissingUri,
149
150 #[error("URI dereference failed: {0}")]
152 UriDereference(super::types::TransformError),
153
154 #[error("transform failed: {0}")]
156 Transform(super::types::TransformError),
157}
158
159#[cfg(test)]
160#[expect(clippy::unwrap_used, reason = "tests use trusted XML fixtures")]
161mod tests {
162 use super::*;
163 use crate::xmldsig::digest::DigestAlgorithm;
164 use crate::xmldsig::parse::{Reference, parse_signed_info};
165 use crate::xmldsig::transforms::Transform;
166 use crate::xmldsig::uri::UriReferenceResolver;
167 use roxmltree::Document;
168
169 fn make_reference(
173 uri: &str,
174 transforms: Vec<Transform>,
175 digest_method: DigestAlgorithm,
176 digest_value: Vec<u8>,
177 ) -> Reference {
178 Reference {
179 uri: Some(uri.to_string()),
180 id: None,
181 ref_type: None,
182 transforms,
183 digest_method,
184 digest_value,
185 }
186 }
187
188 #[test]
191 fn reference_with_correct_digest_passes() {
192 let xml = r##"<root>
195 <data>hello world</data>
196 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="sig1">
197 <ds:SignedInfo/>
198 </ds:Signature>
199 </root>"##;
200 let doc = Document::parse(xml).unwrap();
201 let resolver = UriReferenceResolver::new(&doc);
202 let sig_node = doc
203 .descendants()
204 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
205 .unwrap();
206
207 let initial_data = resolver.dereference("").unwrap();
209 let transforms = vec![
210 Transform::Enveloped,
211 Transform::C14n(
212 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
213 .unwrap(),
214 ),
215 ];
216 let pre_digest_bytes =
217 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
218 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest_bytes);
219
220 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, expected_digest);
222
223 let result = process_reference(&reference, &resolver, sig_node, false).unwrap();
224 assert!(result.valid, "digest should match");
225 assert!(result.pre_digest_data.is_none());
226 }
227
228 #[test]
229 fn reference_with_wrong_digest_fails() {
230 let xml = r##"<root>
231 <data>hello</data>
232 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
233 <ds:SignedInfo/>
234 </ds:Signature>
235 </root>"##;
236 let doc = Document::parse(xml).unwrap();
237 let resolver = UriReferenceResolver::new(&doc);
238 let sig_node = doc
239 .descendants()
240 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
241 .unwrap();
242
243 let transforms = vec![Transform::Enveloped];
244 let wrong_digest = vec![0u8; 32];
246 let reference = make_reference("", transforms, DigestAlgorithm::Sha256, wrong_digest);
247
248 let result = process_reference(&reference, &resolver, sig_node, false).unwrap();
249 assert!(!result.valid, "wrong digest should fail");
250 }
251
252 #[test]
253 fn reference_stores_pre_digest_data() {
254 let xml = "<root><child>text</child></root>";
255 let doc = Document::parse(xml).unwrap();
256 let resolver = UriReferenceResolver::new(&doc);
257
258 let initial_data = resolver.dereference("").unwrap();
260 let pre_digest =
261 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
262 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
263
264 let reference = make_reference("", vec![], DigestAlgorithm::Sha256, digest);
265 let result = process_reference(&reference, &resolver, doc.root_element(), true).unwrap();
266
267 assert!(result.valid);
268 assert!(result.pre_digest_data.is_some());
269 assert_eq!(result.pre_digest_data.unwrap(), pre_digest);
270 }
271
272 #[test]
275 fn reference_with_id_uri() {
276 let xml = r##"<root>
277 <item ID="target">specific content</item>
278 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
279 <ds:SignedInfo/>
280 </ds:Signature>
281 </root>"##;
282 let doc = Document::parse(xml).unwrap();
283 let resolver = UriReferenceResolver::new(&doc);
284 let sig_node = doc
285 .descendants()
286 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
287 .unwrap();
288
289 let initial_data = resolver.dereference("#target").unwrap();
291 let transforms = vec![Transform::C14n(
292 crate::c14n::C14nAlgorithm::from_uri("http://www.w3.org/2001/10/xml-exc-c14n#")
293 .unwrap(),
294 )];
295 let pre_digest =
296 crate::xmldsig::execute_transforms(sig_node, initial_data, &transforms).unwrap();
297 let expected_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
298
299 let reference = make_reference(
300 "#target",
301 transforms,
302 DigestAlgorithm::Sha256,
303 expected_digest,
304 );
305 let result = process_reference(&reference, &resolver, sig_node, false).unwrap();
306 assert!(result.valid);
307 }
308
309 #[test]
310 fn reference_with_nonexistent_id_fails() {
311 let xml = "<root><child/></root>";
312 let doc = Document::parse(xml).unwrap();
313 let resolver = UriReferenceResolver::new(&doc);
314
315 let reference =
316 make_reference("#nonexistent", vec![], DigestAlgorithm::Sha256, vec![0; 32]);
317 let result = process_reference(&reference, &resolver, doc.root_element(), false);
318 assert!(result.is_err());
319 }
320
321 #[test]
322 fn reference_with_absent_uri_fails_closed() {
323 let xml = "<root><child>text</child></root>";
324 let doc = Document::parse(xml).unwrap();
325 let resolver = UriReferenceResolver::new(&doc);
326
327 let reference = Reference {
328 uri: None, id: None,
330 ref_type: None,
331 transforms: vec![],
332 digest_method: DigestAlgorithm::Sha256,
333 digest_value: vec![0; 32],
334 };
335
336 let result = process_reference(&reference, &resolver, doc.root_element(), false);
337 assert!(matches!(result, Err(ReferenceProcessingError::MissingUri)));
338 }
339
340 #[test]
343 fn all_references_pass() {
344 let xml = "<root><child>text</child></root>";
345 let doc = Document::parse(xml).unwrap();
346 let resolver = UriReferenceResolver::new(&doc);
347
348 let initial_data = resolver.dereference("").unwrap();
350 let pre_digest =
351 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
352 let digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
353
354 let refs = vec![
355 make_reference("", vec![], DigestAlgorithm::Sha256, digest.clone()),
356 make_reference("", vec![], DigestAlgorithm::Sha256, digest),
357 ];
358
359 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
360 assert!(result.all_valid());
361 assert_eq!(result.results.len(), 2);
362 assert!(result.first_failure.is_none());
363 }
364
365 #[test]
366 fn fail_fast_on_first_mismatch() {
367 let xml = "<root><child>text</child></root>";
368 let doc = Document::parse(xml).unwrap();
369 let resolver = UriReferenceResolver::new(&doc);
370
371 let wrong_digest = vec![0u8; 32];
372 let refs = vec![
373 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest.clone()),
374 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
376 ];
377
378 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
379 assert!(!result.all_valid());
380 assert_eq!(result.first_failure, Some(0));
381 assert_eq!(result.results.len(), 1);
383 assert!(!result.results[0].valid);
384 }
385
386 #[test]
387 fn fail_fast_second_reference() {
388 let xml = "<root><child>text</child></root>";
389 let doc = Document::parse(xml).unwrap();
390 let resolver = UriReferenceResolver::new(&doc);
391
392 let initial_data = resolver.dereference("").unwrap();
394 let pre_digest =
395 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
396 let correct_digest = compute_digest(DigestAlgorithm::Sha256, &pre_digest);
397 let wrong_digest = vec![0u8; 32];
398
399 let refs = vec![
400 make_reference("", vec![], DigestAlgorithm::Sha256, correct_digest),
401 make_reference("", vec![], DigestAlgorithm::Sha256, wrong_digest),
402 ];
403
404 let result = process_all_references(&refs, &resolver, doc.root_element(), false).unwrap();
405 assert!(!result.all_valid());
406 assert_eq!(result.first_failure, Some(1));
407 assert_eq!(result.results.len(), 2);
409 assert!(result.results[0].valid);
410 assert!(!result.results[1].valid);
411 }
412
413 #[test]
414 fn empty_references_list() {
415 let xml = "<root/>";
416 let doc = Document::parse(xml).unwrap();
417 let resolver = UriReferenceResolver::new(&doc);
418
419 let result = process_all_references(&[], &resolver, doc.root_element(), false).unwrap();
420 assert!(result.all_valid());
421 assert!(result.results.is_empty());
422 }
423
424 #[test]
427 fn reference_sha1_digest() {
428 let xml = "<root>content</root>";
429 let doc = Document::parse(xml).unwrap();
430 let resolver = UriReferenceResolver::new(&doc);
431
432 let initial_data = resolver.dereference("").unwrap();
433 let pre_digest =
434 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
435 let digest = compute_digest(DigestAlgorithm::Sha1, &pre_digest);
436
437 let reference = make_reference("", vec![], DigestAlgorithm::Sha1, digest);
438 let result = process_reference(&reference, &resolver, doc.root_element(), false).unwrap();
439 assert!(result.valid);
440 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha1);
441 }
442
443 #[test]
444 fn reference_sha512_digest() {
445 let xml = "<root>content</root>";
446 let doc = Document::parse(xml).unwrap();
447 let resolver = UriReferenceResolver::new(&doc);
448
449 let initial_data = resolver.dereference("").unwrap();
450 let pre_digest =
451 crate::xmldsig::execute_transforms(doc.root_element(), initial_data, &[]).unwrap();
452 let digest = compute_digest(DigestAlgorithm::Sha512, &pre_digest);
453
454 let reference = make_reference("", vec![], DigestAlgorithm::Sha512, digest);
455 let result = process_reference(&reference, &resolver, doc.root_element(), false).unwrap();
456 assert!(result.valid);
457 assert_eq!(result.digest_algorithm, DigestAlgorithm::Sha512);
458 }
459
460 #[test]
463 fn saml_enveloped_reference_processing() {
464 let xml = r##"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
466 xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
467 ID="_resp1">
468 <saml:Assertion ID="_assert1">
469 <saml:Subject>user@example.com</saml:Subject>
470 </saml:Assertion>
471 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
472 <ds:SignedInfo>
473 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
474 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
475 <ds:Reference URI="">
476 <ds:Transforms>
477 <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
478 <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
479 </ds:Transforms>
480 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
481 <ds:DigestValue>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</ds:DigestValue>
482 </ds:Reference>
483 </ds:SignedInfo>
484 <ds:SignatureValue>fakesig==</ds:SignatureValue>
485 </ds:Signature>
486 </samlp:Response>"##;
487 let doc = Document::parse(xml).unwrap();
488 let resolver = UriReferenceResolver::new(&doc);
489 let sig_node = doc
490 .descendants()
491 .find(|n| n.is_element() && n.tag_name().name() == "Signature")
492 .unwrap();
493
494 let signed_info_node = sig_node
496 .children()
497 .find(|n| n.is_element() && n.tag_name().name() == "SignedInfo")
498 .unwrap();
499 let signed_info = parse_signed_info(signed_info_node).unwrap();
500 let reference = &signed_info.references[0];
501
502 let initial_data = resolver.dereference("").unwrap();
504 let pre_digest =
505 crate::xmldsig::execute_transforms(sig_node, initial_data, &reference.transforms)
506 .unwrap();
507 let correct_digest = compute_digest(reference.digest_method, &pre_digest);
508
509 let corrected_ref = make_reference(
511 "",
512 reference.transforms.clone(),
513 reference.digest_method,
514 correct_digest,
515 );
516
517 let result = process_reference(&corrected_ref, &resolver, sig_node, true).unwrap();
519 assert!(result.valid, "SAML reference should verify");
520 assert!(result.pre_digest_data.is_some());
521
522 let pre_digest_str = String::from_utf8(result.pre_digest_data.unwrap()).unwrap();
524 assert!(
525 pre_digest_str.contains("samlp:Response"),
526 "pre-digest should contain Response"
527 );
528 assert!(
529 !pre_digest_str.contains("SignatureValue"),
530 "pre-digest should NOT contain Signature"
531 );
532 }
533}