1use std::io::Write;
4
5use quick_xml::Writer;
6use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
7
8use crate::c14n::{C14nAlgorithm, C14nMode};
9use crate::xml::is_xml_1_0_character;
10
11use super::parse::MAX_REFERENCES_PER_SIGNATURE;
12use super::transforms::{
13 MAX_TRANSFORMS_PER_REFERENCE, MAX_XPATH_FILTERS, XPathSignatureParseBudget,
14 validate_xpath_namespace_budget,
15};
16use super::xpath::compile_xpath;
17use super::{
18 BASE64_TRANSFORM_URI, DigestAlgorithm, ENVELOPED_SIGNATURE_URI, SignatureAlgorithm, Transform,
19 XPATH_FILTER2_TRANSFORM_URI, XPATH_TRANSFORM_URI, XPathExpression,
20};
21
22const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
23const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
24const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/";
25const EXCLUSIVE_C14N_NS: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
26const XPATH_EXCLUDE_ALL_SIGNATURES: &str = "not(ancestor-or-self::dsig:Signature)";
27
28#[derive(Debug, thiserror::Error)]
30pub enum SignatureBuilderError {
31 #[error("invalid XML namespace prefix: {0}")]
33 InvalidNamespacePrefix(String),
34 #[error("XML namespace URI contains a character forbidden by XML 1.0: {0:?}")]
36 InvalidNamespaceUri(String),
37 #[error("XPath namespace binding conflicts with XMLDSig prefix: {0}")]
39 NamespacePrefixConflict(String),
40 #[error("invalid {element} Id: {value}")]
42 InvalidId {
43 element: &'static str,
45 value: String,
47 },
48 #[error("a signature template requires at least one Reference")]
50 MissingReference,
51 #[error("signature template contains {count} references; maximum is {max}")]
53 TooManyReferences {
54 count: usize,
56 max: usize,
58 },
59 #[error("transform chain contains {count} transforms; maximum is {max}")]
61 TooManyTransforms {
62 count: usize,
64 max: usize,
66 },
67 #[error("XPath Filter 2.0 requires between 1 and {max} expressions, got {count}")]
69 InvalidXPathFilterCount {
70 count: usize,
72 max: usize,
74 },
75 #[error("invalid XPath expression: {0}")]
77 InvalidXPath(String),
78 #[error("algorithm is not allowed for signing: {0}")]
80 SigningAlgorithmDisabled(&'static str),
81 #[error("XML serialization error: {0}")]
83 Serialization(#[from] std::io::Error),
84 #[error("XML writer emitted invalid UTF-8: {0}")]
86 InvalidUtf8(#[from] std::string::FromUtf8Error),
87}
88
89#[derive(Debug, Clone)]
91pub struct ReferenceBuilder {
92 uri: Option<String>,
93 id: Option<String>,
94 ref_type: Option<String>,
95 transforms: Vec<Transform>,
96 digest_method: DigestAlgorithm,
97}
98
99impl ReferenceBuilder {
100 #[must_use]
102 pub fn new(digest_method: DigestAlgorithm) -> Self {
103 Self {
104 uri: None,
105 id: None,
106 ref_type: None,
107 transforms: Vec::new(),
108 digest_method,
109 }
110 }
111
112 #[must_use]
114 pub fn uri(mut self, uri: impl Into<String>) -> Self {
115 self.uri = Some(uri.into());
116 self
117 }
118
119 #[must_use]
121 pub fn id(mut self, id: impl Into<String>) -> Self {
122 self.id = Some(id.into());
123 self
124 }
125
126 #[must_use]
128 pub fn ref_type(mut self, ref_type: impl Into<String>) -> Self {
129 self.ref_type = Some(ref_type.into());
130 self
131 }
132
133 #[must_use]
135 pub fn transform(mut self, transform: Transform) -> Self {
136 self.transforms.push(transform);
137 self
138 }
139}
140
141#[derive(Debug, Clone)]
143pub struct SignatureBuilder {
144 c14n_method: C14nAlgorithm,
145 sign_method: SignatureAlgorithm,
146 ns_prefix: Option<String>,
147 signature_id: Option<String>,
148 references: Vec<ReferenceBuilder>,
149 include_key_info: bool,
150}
151
152impl SignatureBuilder {
153 #[must_use]
155 pub fn new(c14n_method: C14nAlgorithm, sign_method: SignatureAlgorithm) -> Self {
156 Self {
157 c14n_method,
158 sign_method,
159 ns_prefix: None,
160 signature_id: None,
161 references: Vec::new(),
162 include_key_info: false,
163 }
164 }
165
166 #[must_use]
168 pub fn ns_prefix(mut self, prefix: impl Into<String>) -> Self {
169 self.ns_prefix = Some(prefix.into());
170 self
171 }
172
173 #[must_use]
175 pub fn signature_id(mut self, id: impl Into<String>) -> Self {
176 self.signature_id = Some(id.into());
177 self
178 }
179
180 #[must_use]
182 pub fn add_reference(mut self, reference: ReferenceBuilder) -> Self {
183 self.references.push(reference);
184 self
185 }
186
187 #[must_use]
189 pub fn key_info(mut self, include: bool) -> Self {
190 self.include_key_info = include;
191 self
192 }
193
194 pub fn build_template(&self) -> Result<String, SignatureBuilderError> {
196 self.validate()?;
197
198 let prefix = self.ns_prefix.as_deref();
199 let mut writer = Writer::new(Vec::new());
200 let signature_name = qualified_name(prefix, "Signature");
201 let mut signature = BytesStart::new(&signature_name);
202 let namespace_attr = prefix.map_or_else(|| "xmlns".to_owned(), |p| format!("xmlns:{p}"));
203 signature.push_attribute((namespace_attr.as_str(), XMLDSIG_NS));
204 if let Some(id) = &self.signature_id {
205 signature.push_attribute(("Id", id.as_str()));
206 }
207 writer.write_event(Event::Start(signature))?;
208
209 write_start(&mut writer, prefix, "SignedInfo")?;
210 write_algorithm(
211 &mut writer,
212 prefix,
213 "CanonicalizationMethod",
214 self.c14n_method.uri(),
215 )?;
216 write_algorithm(
217 &mut writer,
218 prefix,
219 "SignatureMethod",
220 self.sign_method.uri(),
221 )?;
222 for reference in &self.references {
223 write_reference(&mut writer, prefix, reference)?;
224 }
225 write_end(&mut writer, prefix, "SignedInfo")?;
226 write_empty(&mut writer, prefix, "SignatureValue")?;
227 if self.include_key_info {
228 write_empty(&mut writer, prefix, "KeyInfo")?;
229 }
230 writer.write_event(Event::End(BytesEnd::new(signature_name)))?;
231
232 Ok(String::from_utf8(writer.into_inner())?)
233 }
234
235 fn validate(&self) -> Result<(), SignatureBuilderError> {
236 if let Some(prefix) = &self.ns_prefix
237 && !is_namespace_prefix(prefix)
238 {
239 return Err(SignatureBuilderError::InvalidNamespacePrefix(
240 prefix.clone(),
241 ));
242 }
243 if let Some(id) = &self.signature_id
244 && !is_ncname(id)
245 {
246 return Err(SignatureBuilderError::InvalidId {
247 element: "Signature",
248 value: id.clone(),
249 });
250 }
251 if self.references.is_empty() {
252 return Err(SignatureBuilderError::MissingReference);
253 }
254 if self.references.len() > MAX_REFERENCES_PER_SIGNATURE {
255 return Err(SignatureBuilderError::TooManyReferences {
256 count: self.references.len(),
257 max: MAX_REFERENCES_PER_SIGNATURE,
258 });
259 }
260 let mut xpath_signature_budget = XPathSignatureParseBudget::default();
261 for reference in &self.references {
262 if reference.transforms.len() > MAX_TRANSFORMS_PER_REFERENCE {
263 return Err(SignatureBuilderError::TooManyTransforms {
264 count: reference.transforms.len(),
265 max: MAX_TRANSFORMS_PER_REFERENCE,
266 });
267 }
268 for transform in &reference.transforms {
269 match transform {
270 Transform::XPath(xpath) => {
271 validate_xpath_source(xpath.expression())?;
272 xpath_signature_budget.charge().map_err(|error| {
273 SignatureBuilderError::InvalidXPath(error.to_string())
274 })?;
275 }
276 Transform::XPathFilter2(filters) => {
277 if filters.is_empty() || filters.len() > MAX_XPATH_FILTERS {
278 return Err(SignatureBuilderError::InvalidXPathFilterCount {
279 count: filters.len(),
280 max: MAX_XPATH_FILTERS,
281 });
282 }
283 for filter in filters {
284 validate_xpath_source(filter.xpath().expression())?;
285 xpath_signature_budget.charge().map_err(|error| {
286 SignatureBuilderError::InvalidXPath(error.to_string())
287 })?;
288 }
289 }
290 _ => {}
291 }
292 }
293 validate_xpath_namespace_budget(
294 &reference.transforms,
295 self.ns_prefix.as_deref().map(|prefix| (prefix, XMLDSIG_NS)),
296 )
297 .map_err(|error| SignatureBuilderError::InvalidXPath(error.to_string()))?;
298 }
299 for (prefix, uri, shares_signature_namespace) in
300 self.references.iter().flat_map(|reference| {
301 reference
302 .transforms
303 .iter()
304 .flat_map(|transform| match transform {
305 Transform::XPath(xpath) => xpath
306 .namespaces()
307 .iter()
308 .map(|(prefix, uri)| (prefix, uri, true))
309 .collect::<Vec<_>>(),
310 Transform::XPathFilter2(filters) => filters
311 .iter()
312 .flat_map(|filter| {
313 filter
314 .xpath()
315 .namespaces()
316 .iter()
317 .map(|(prefix, uri)| (prefix, uri, false))
318 })
319 .collect(),
320 _ => Vec::new(),
321 })
322 })
323 {
324 if uri.is_empty() || uri == XMLNS_NS || !uri.chars().all(is_xml_1_0_character) {
327 return Err(SignatureBuilderError::InvalidNamespaceUri(uri.clone()));
328 }
329 if prefix == "xmlns"
330 || (prefix == "xml") != (uri == XML_NS)
331 || (prefix != "xml" && !is_namespace_prefix(prefix))
332 {
333 return Err(SignatureBuilderError::InvalidNamespacePrefix(
334 prefix.clone(),
335 ));
336 }
337 if shares_signature_namespace
340 && self.ns_prefix.as_ref() == Some(prefix)
341 && uri != XMLDSIG_NS
342 {
343 return Err(SignatureBuilderError::NamespacePrefixConflict(
344 prefix.clone(),
345 ));
346 }
347 }
348 if !self.sign_method.signing_allowed() {
349 return Err(SignatureBuilderError::SigningAlgorithmDisabled(
350 self.sign_method.uri(),
351 ));
352 }
353 for reference in &self.references {
354 if let Some(id) = &reference.id
355 && !is_ncname(id)
356 {
357 return Err(SignatureBuilderError::InvalidId {
358 element: "Reference",
359 value: id.clone(),
360 });
361 }
362 if !reference.digest_method.signing_allowed() {
363 return Err(SignatureBuilderError::SigningAlgorithmDisabled(
364 reference.digest_method.uri(),
365 ));
366 }
367 }
368 Ok(())
369 }
370}
371
372fn validate_xpath_source(source: &str) -> Result<(), SignatureBuilderError> {
373 if let Some(character) = source
374 .chars()
375 .find(|character| !is_xml_1_0_character(*character))
376 {
377 return Err(SignatureBuilderError::InvalidXPath(format!(
378 "XPath expression contains a character forbidden by XML 1.0: {character:?}"
379 )));
380 }
381 compile_xpath(source).map_err(SignatureBuilderError::InvalidXPath)?;
382 Ok(())
383}
384
385fn write_reference<W: Write>(
386 writer: &mut Writer<W>,
387 prefix: Option<&str>,
388 reference: &ReferenceBuilder,
389) -> Result<(), std::io::Error> {
390 let name = qualified_name(prefix, "Reference");
391 let mut element = BytesStart::new(&name);
392 if let Some(id) = &reference.id {
393 element.push_attribute(("Id", id.as_str()));
394 }
395 if let Some(ref_type) = &reference.ref_type {
396 element.push_attribute(("Type", ref_type.as_str()));
397 }
398 if let Some(uri) = &reference.uri {
399 element.push_attribute(("URI", uri.as_str()));
400 }
401 writer.write_event(Event::Start(element))?;
402
403 if !reference.transforms.is_empty() {
404 write_start(writer, prefix, "Transforms")?;
405 for transform in &reference.transforms {
406 write_transform(writer, prefix, transform)?;
407 }
408 write_end(writer, prefix, "Transforms")?;
409 }
410 write_algorithm(
411 writer,
412 prefix,
413 "DigestMethod",
414 reference.digest_method.uri(),
415 )?;
416 write_empty(writer, prefix, "DigestValue")?;
417 writer.write_event(Event::End(BytesEnd::new(name)))?;
418 Ok(())
419}
420
421fn write_transform<W: Write>(
422 writer: &mut Writer<W>,
423 prefix: Option<&str>,
424 transform: &Transform,
425) -> Result<(), std::io::Error> {
426 match transform {
427 Transform::Enveloped => {
428 write_algorithm(writer, prefix, "Transform", ENVELOPED_SIGNATURE_URI)
429 }
430 Transform::XpathExcludeAllSignatures => {
431 let name = qualified_name(prefix, "Transform");
432 let mut element = BytesStart::new(&name);
433 element.push_attribute(("Algorithm", XPATH_TRANSFORM_URI));
434 writer.write_event(Event::Start(element))?;
435 let xpath_name = qualified_name(prefix, "XPath");
436 let mut xpath = BytesStart::new(&xpath_name);
437 xpath.push_attribute(("xmlns:dsig", XMLDSIG_NS));
438 writer.write_event(Event::Start(xpath))?;
439 writer.write_event(Event::Text(BytesText::new(XPATH_EXCLUDE_ALL_SIGNATURES)))?;
440 writer.write_event(Event::End(BytesEnd::new(xpath_name)))?;
441 writer.write_event(Event::End(BytesEnd::new(name)))?;
442 Ok(())
443 }
444 Transform::XPath(xpath) => {
445 let transform_name = qualified_name(prefix, "Transform");
446 let mut transform_element = BytesStart::new(&transform_name);
447 transform_element.push_attribute(("Algorithm", XPATH_TRANSFORM_URI));
448 writer.write_event(Event::Start(transform_element))?;
449 write_xpath_expression(writer, prefix, "XPath", None, xpath)?;
450 writer.write_event(Event::End(BytesEnd::new(transform_name)))?;
451 Ok(())
452 }
453 Transform::XPathFilter2(filters) => {
454 let transform_name = qualified_name(prefix, "Transform");
455 let mut transform_element = BytesStart::new(&transform_name);
456 transform_element.push_attribute(("Algorithm", XPATH_FILTER2_TRANSFORM_URI));
457 writer.write_event(Event::Start(transform_element))?;
458 for filter in filters {
459 write_xpath_expression(
460 writer,
461 None,
462 "XPath",
463 Some(filter.operation().as_str()),
464 filter.xpath(),
465 )?;
466 }
467 writer.write_event(Event::End(BytesEnd::new(transform_name)))?;
468 Ok(())
469 }
470 Transform::Base64Decode => {
471 write_algorithm(writer, prefix, "Transform", BASE64_TRANSFORM_URI)
472 }
473 Transform::C14n(algorithm) if algorithm.inclusive_prefixes().is_empty() => {
474 write_algorithm(writer, prefix, "Transform", algorithm.uri())
475 }
476 Transform::C14n(algorithm) => {
477 let name = qualified_name(prefix, "Transform");
478 let mut element = BytesStart::new(&name);
479 element.push_attribute(("Algorithm", algorithm.uri()));
480 writer.write_event(Event::Start(element))?;
481
482 if algorithm.mode() == C14nMode::Exclusive1_0 {
483 let mut prefixes: Vec<&str> = algorithm
484 .inclusive_prefixes()
485 .iter()
486 .map(String::as_str)
487 .collect();
488 prefixes.sort_unstable();
489 let prefix_list = prefixes
490 .into_iter()
491 .map(|p| if p.is_empty() { "#default" } else { p })
492 .collect::<Vec<_>>()
493 .join(" ");
494 let mut inclusive = BytesStart::new("ec:InclusiveNamespaces");
495 inclusive.push_attribute(("xmlns:ec", EXCLUSIVE_C14N_NS));
496 inclusive.push_attribute(("PrefixList", prefix_list.as_str()));
497 writer.write_event(Event::Empty(inclusive))?;
498 }
499 writer.write_event(Event::End(BytesEnd::new(name)))?;
500 Ok(())
501 }
502 }
503}
504
505fn write_xpath_expression<W: Write>(
506 writer: &mut Writer<W>,
507 prefix: Option<&str>,
508 local_name: &str,
509 filter: Option<&str>,
510 xpath: &XPathExpression,
511) -> Result<(), std::io::Error> {
512 let name = qualified_name(prefix, local_name);
513 let mut element = BytesStart::new(&name);
514 let namespace_attributes = xpath
515 .namespaces()
516 .iter()
517 .filter(|(namespace_prefix, _)| namespace_prefix.as_str() != "xml")
518 .map(|(namespace_prefix, uri)| (format!("xmlns:{namespace_prefix}"), uri))
519 .collect::<Vec<_>>();
520 if prefix.is_none() && filter.is_some() {
521 element.push_attribute(("xmlns", XPATH_FILTER2_TRANSFORM_URI));
522 }
523 if let Some(filter) = filter {
524 element.push_attribute(("Filter", filter));
525 }
526 for (attribute, uri) in &namespace_attributes {
527 element.push_attribute((attribute.as_str(), uri.as_str()));
528 }
529 writer.write_event(Event::Start(element))?;
530 writer.write_event(Event::Text(BytesText::new(xpath.expression())))?;
531 writer.write_event(Event::End(BytesEnd::new(name)))?;
532 Ok(())
533}
534
535fn write_algorithm<W: Write>(
536 writer: &mut Writer<W>,
537 prefix: Option<&str>,
538 local_name: &str,
539 algorithm: &str,
540) -> Result<(), std::io::Error> {
541 let name = qualified_name(prefix, local_name);
542 let mut element = BytesStart::new(name);
543 element.push_attribute(("Algorithm", algorithm));
544 writer.write_event(Event::Empty(element))?;
545 Ok(())
546}
547
548fn write_start<W: Write>(
549 writer: &mut Writer<W>,
550 prefix: Option<&str>,
551 local_name: &str,
552) -> Result<(), std::io::Error> {
553 writer.write_event(Event::Start(BytesStart::new(qualified_name(
554 prefix, local_name,
555 ))))?;
556 Ok(())
557}
558
559fn write_end<W: Write>(
560 writer: &mut Writer<W>,
561 prefix: Option<&str>,
562 local_name: &str,
563) -> Result<(), std::io::Error> {
564 writer.write_event(Event::End(BytesEnd::new(qualified_name(
565 prefix, local_name,
566 ))))?;
567 Ok(())
568}
569
570fn write_empty<W: Write>(
571 writer: &mut Writer<W>,
572 prefix: Option<&str>,
573 local_name: &str,
574) -> Result<(), std::io::Error> {
575 writer.write_event(Event::Empty(BytesStart::new(qualified_name(
576 prefix, local_name,
577 ))))?;
578 Ok(())
579}
580
581fn qualified_name(prefix: Option<&str>, local_name: &str) -> String {
582 prefix.map_or_else(
583 || local_name.to_owned(),
584 |prefix| format!("{prefix}:{local_name}"),
585 )
586}
587
588fn is_ncname(value: &str) -> bool {
589 if value.is_empty() || value.contains(':') {
590 return false;
591 }
592
593 roxmltree::Document::parse(&format!("<{value}/>"))
594 .is_ok_and(|document| document.root_element().tag_name().name() == value)
595}
596
597fn is_namespace_prefix(value: &str) -> bool {
598 if matches!(value, "xml" | "xmlns") || !is_ncname(value) {
602 return false;
603 }
604
605 roxmltree::Document::parse(&format!(
608 "<{value}:n xmlns:{value}=\"urn:xml-sec:prefix-validation\"/>"
609 ))
610 .is_ok()
611}