1use std::io::Write;
4
5use quick_xml::Writer;
6use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
7
8use crate::c14n::{C14nAlgorithm, C14nMode};
9
10use super::{
11 DigestAlgorithm, ENVELOPED_SIGNATURE_URI, SignatureAlgorithm, Transform, XPATH_TRANSFORM_URI,
12};
13
14const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
15const EXCLUSIVE_C14N_NS: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
16const XPATH_EXCLUDE_ALL_SIGNATURES: &str = "not(ancestor-or-self::dsig:Signature)";
17
18#[derive(Debug, thiserror::Error)]
20pub enum SignatureBuilderError {
21 #[error("invalid XML namespace prefix: {0}")]
23 InvalidNamespacePrefix(String),
24 #[error("invalid {element} Id: {value}")]
26 InvalidId {
27 element: &'static str,
29 value: String,
31 },
32 #[error("a signature template requires at least one Reference")]
34 MissingReference,
35 #[error("algorithm is not allowed for signing: {0}")]
37 SigningAlgorithmDisabled(&'static str),
38 #[error("XML serialization error: {0}")]
40 Serialization(#[from] std::io::Error),
41 #[error("XML writer emitted invalid UTF-8: {0}")]
43 InvalidUtf8(#[from] std::string::FromUtf8Error),
44}
45
46#[derive(Debug, Clone)]
48pub struct ReferenceBuilder {
49 uri: Option<String>,
50 id: Option<String>,
51 ref_type: Option<String>,
52 transforms: Vec<Transform>,
53 digest_method: DigestAlgorithm,
54}
55
56impl ReferenceBuilder {
57 #[must_use]
59 pub fn new(digest_method: DigestAlgorithm) -> Self {
60 Self {
61 uri: None,
62 id: None,
63 ref_type: None,
64 transforms: Vec::new(),
65 digest_method,
66 }
67 }
68
69 #[must_use]
71 pub fn uri(mut self, uri: impl Into<String>) -> Self {
72 self.uri = Some(uri.into());
73 self
74 }
75
76 #[must_use]
78 pub fn id(mut self, id: impl Into<String>) -> Self {
79 self.id = Some(id.into());
80 self
81 }
82
83 #[must_use]
85 pub fn ref_type(mut self, ref_type: impl Into<String>) -> Self {
86 self.ref_type = Some(ref_type.into());
87 self
88 }
89
90 #[must_use]
92 pub fn transform(mut self, transform: Transform) -> Self {
93 self.transforms.push(transform);
94 self
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct SignatureBuilder {
101 c14n_method: C14nAlgorithm,
102 sign_method: SignatureAlgorithm,
103 ns_prefix: Option<String>,
104 signature_id: Option<String>,
105 references: Vec<ReferenceBuilder>,
106 include_key_info: bool,
107}
108
109impl SignatureBuilder {
110 #[must_use]
112 pub fn new(c14n_method: C14nAlgorithm, sign_method: SignatureAlgorithm) -> Self {
113 Self {
114 c14n_method,
115 sign_method,
116 ns_prefix: None,
117 signature_id: None,
118 references: Vec::new(),
119 include_key_info: false,
120 }
121 }
122
123 #[must_use]
125 pub fn ns_prefix(mut self, prefix: impl Into<String>) -> Self {
126 self.ns_prefix = Some(prefix.into());
127 self
128 }
129
130 #[must_use]
132 pub fn signature_id(mut self, id: impl Into<String>) -> Self {
133 self.signature_id = Some(id.into());
134 self
135 }
136
137 #[must_use]
139 pub fn add_reference(mut self, reference: ReferenceBuilder) -> Self {
140 self.references.push(reference);
141 self
142 }
143
144 #[must_use]
146 pub fn key_info(mut self, include: bool) -> Self {
147 self.include_key_info = include;
148 self
149 }
150
151 pub fn build_template(&self) -> Result<String, SignatureBuilderError> {
153 self.validate()?;
154
155 let prefix = self.ns_prefix.as_deref();
156 let mut writer = Writer::new(Vec::new());
157 let signature_name = qualified_name(prefix, "Signature");
158 let mut signature = BytesStart::new(&signature_name);
159 let namespace_attr = prefix.map_or_else(|| "xmlns".to_owned(), |p| format!("xmlns:{p}"));
160 signature.push_attribute((namespace_attr.as_str(), XMLDSIG_NS));
161 if let Some(id) = &self.signature_id {
162 signature.push_attribute(("Id", id.as_str()));
163 }
164 writer.write_event(Event::Start(signature))?;
165
166 write_start(&mut writer, prefix, "SignedInfo")?;
167 write_algorithm(
168 &mut writer,
169 prefix,
170 "CanonicalizationMethod",
171 self.c14n_method.uri(),
172 )?;
173 write_algorithm(
174 &mut writer,
175 prefix,
176 "SignatureMethod",
177 self.sign_method.uri(),
178 )?;
179 for reference in &self.references {
180 write_reference(&mut writer, prefix, reference)?;
181 }
182 write_end(&mut writer, prefix, "SignedInfo")?;
183 write_empty(&mut writer, prefix, "SignatureValue")?;
184 if self.include_key_info {
185 write_empty(&mut writer, prefix, "KeyInfo")?;
186 }
187 writer.write_event(Event::End(BytesEnd::new(signature_name)))?;
188
189 Ok(String::from_utf8(writer.into_inner())?)
190 }
191
192 fn validate(&self) -> Result<(), SignatureBuilderError> {
193 if let Some(prefix) = &self.ns_prefix
194 && !is_namespace_prefix(prefix)
195 {
196 return Err(SignatureBuilderError::InvalidNamespacePrefix(
197 prefix.clone(),
198 ));
199 }
200 if let Some(id) = &self.signature_id
201 && !is_ncname(id)
202 {
203 return Err(SignatureBuilderError::InvalidId {
204 element: "Signature",
205 value: id.clone(),
206 });
207 }
208 if self.references.is_empty() {
209 return Err(SignatureBuilderError::MissingReference);
210 }
211 if !self.sign_method.signing_allowed() {
212 return Err(SignatureBuilderError::SigningAlgorithmDisabled(
213 self.sign_method.uri(),
214 ));
215 }
216 for reference in &self.references {
217 if let Some(id) = &reference.id
218 && !is_ncname(id)
219 {
220 return Err(SignatureBuilderError::InvalidId {
221 element: "Reference",
222 value: id.clone(),
223 });
224 }
225 if !reference.digest_method.signing_allowed() {
226 return Err(SignatureBuilderError::SigningAlgorithmDisabled(
227 reference.digest_method.uri(),
228 ));
229 }
230 }
231 Ok(())
232 }
233}
234
235fn write_reference<W: Write>(
236 writer: &mut Writer<W>,
237 prefix: Option<&str>,
238 reference: &ReferenceBuilder,
239) -> Result<(), std::io::Error> {
240 let name = qualified_name(prefix, "Reference");
241 let mut element = BytesStart::new(&name);
242 if let Some(id) = &reference.id {
243 element.push_attribute(("Id", id.as_str()));
244 }
245 if let Some(ref_type) = &reference.ref_type {
246 element.push_attribute(("Type", ref_type.as_str()));
247 }
248 if let Some(uri) = &reference.uri {
249 element.push_attribute(("URI", uri.as_str()));
250 }
251 writer.write_event(Event::Start(element))?;
252
253 if !reference.transforms.is_empty() {
254 write_start(writer, prefix, "Transforms")?;
255 for transform in &reference.transforms {
256 write_transform(writer, prefix, transform)?;
257 }
258 write_end(writer, prefix, "Transforms")?;
259 }
260 write_algorithm(
261 writer,
262 prefix,
263 "DigestMethod",
264 reference.digest_method.uri(),
265 )?;
266 write_empty(writer, prefix, "DigestValue")?;
267 writer.write_event(Event::End(BytesEnd::new(name)))?;
268 Ok(())
269}
270
271fn write_transform<W: Write>(
272 writer: &mut Writer<W>,
273 prefix: Option<&str>,
274 transform: &Transform,
275) -> Result<(), std::io::Error> {
276 match transform {
277 Transform::Enveloped => {
278 write_algorithm(writer, prefix, "Transform", ENVELOPED_SIGNATURE_URI)
279 }
280 Transform::XpathExcludeAllSignatures => {
281 let name = qualified_name(prefix, "Transform");
282 let mut element = BytesStart::new(&name);
283 element.push_attribute(("Algorithm", XPATH_TRANSFORM_URI));
284 writer.write_event(Event::Start(element))?;
285 let xpath_name = qualified_name(prefix, "XPath");
286 let mut xpath = BytesStart::new(&xpath_name);
287 xpath.push_attribute(("xmlns:dsig", XMLDSIG_NS));
288 writer.write_event(Event::Start(xpath))?;
289 writer.write_event(Event::Text(BytesText::new(XPATH_EXCLUDE_ALL_SIGNATURES)))?;
290 writer.write_event(Event::End(BytesEnd::new(xpath_name)))?;
291 writer.write_event(Event::End(BytesEnd::new(name)))?;
292 Ok(())
293 }
294 Transform::C14n(algorithm) if algorithm.inclusive_prefixes().is_empty() => {
295 write_algorithm(writer, prefix, "Transform", algorithm.uri())
296 }
297 Transform::C14n(algorithm) => {
298 let name = qualified_name(prefix, "Transform");
299 let mut element = BytesStart::new(&name);
300 element.push_attribute(("Algorithm", algorithm.uri()));
301 writer.write_event(Event::Start(element))?;
302
303 if algorithm.mode() == C14nMode::Exclusive1_0 {
304 let mut prefixes: Vec<&str> = algorithm
305 .inclusive_prefixes()
306 .iter()
307 .map(String::as_str)
308 .collect();
309 prefixes.sort_unstable();
310 let prefix_list = prefixes
311 .into_iter()
312 .map(|p| if p.is_empty() { "#default" } else { p })
313 .collect::<Vec<_>>()
314 .join(" ");
315 let mut inclusive = BytesStart::new("ec:InclusiveNamespaces");
316 inclusive.push_attribute(("xmlns:ec", EXCLUSIVE_C14N_NS));
317 inclusive.push_attribute(("PrefixList", prefix_list.as_str()));
318 writer.write_event(Event::Empty(inclusive))?;
319 }
320 writer.write_event(Event::End(BytesEnd::new(name)))?;
321 Ok(())
322 }
323 }
324}
325
326fn write_algorithm<W: Write>(
327 writer: &mut Writer<W>,
328 prefix: Option<&str>,
329 local_name: &str,
330 algorithm: &str,
331) -> Result<(), std::io::Error> {
332 let name = qualified_name(prefix, local_name);
333 let mut element = BytesStart::new(name);
334 element.push_attribute(("Algorithm", algorithm));
335 writer.write_event(Event::Empty(element))?;
336 Ok(())
337}
338
339fn write_start<W: Write>(
340 writer: &mut Writer<W>,
341 prefix: Option<&str>,
342 local_name: &str,
343) -> Result<(), std::io::Error> {
344 writer.write_event(Event::Start(BytesStart::new(qualified_name(
345 prefix, local_name,
346 ))))?;
347 Ok(())
348}
349
350fn write_end<W: Write>(
351 writer: &mut Writer<W>,
352 prefix: Option<&str>,
353 local_name: &str,
354) -> Result<(), std::io::Error> {
355 writer.write_event(Event::End(BytesEnd::new(qualified_name(
356 prefix, local_name,
357 ))))?;
358 Ok(())
359}
360
361fn write_empty<W: Write>(
362 writer: &mut Writer<W>,
363 prefix: Option<&str>,
364 local_name: &str,
365) -> Result<(), std::io::Error> {
366 writer.write_event(Event::Empty(BytesStart::new(qualified_name(
367 prefix, local_name,
368 ))))?;
369 Ok(())
370}
371
372fn qualified_name(prefix: Option<&str>, local_name: &str) -> String {
373 prefix.map_or_else(
374 || local_name.to_owned(),
375 |prefix| format!("{prefix}:{local_name}"),
376 )
377}
378
379fn is_ncname(value: &str) -> bool {
380 if value.is_empty() || value.contains(':') {
381 return false;
382 }
383
384 roxmltree::Document::parse(&format!("<{value}/>"))
385 .is_ok_and(|document| document.root_element().tag_name().name() == value)
386}
387
388fn is_namespace_prefix(value: &str) -> bool {
389 if !is_ncname(value) {
390 return false;
391 }
392
393 roxmltree::Document::parse(&format!(
396 "<{value}:n xmlns:{value}=\"urn:xml-sec:prefix-validation\"/>"
397 ))
398 .is_ok()
399}