Skip to main content

xml_sec/xmlenc/
key_derivation.rs

1//! XML KDF syntax normalized into the provider's single parameter contract.
2
3use crate::document::{
4    DocumentParseSettings, XmlParseWorkBudget, parse_borrowed_with_settings_and_budget,
5};
6use crate::provider::{KdfContext, KdfParameters};
7use crate::xml::dom::Node;
8
9use super::parse::{
10    bounded_simple_text_with_limit, decode_bounded_base64_text, require_element as require,
11    validate_metadata_len,
12};
13use super::types::{XMLDSIG_NS, XMLENC11_NS};
14use super::{XmlEncError, map_document_error};
15
16mod serialize;
17
18const MORE_NS: &str = "http://www.w3.org/2021/04/xmldsig-more#";
19const CONCAT: &str = "http://www.w3.org/2009/xmlenc11#ConcatKDF";
20const PBKDF2: &str = "http://www.w3.org/2009/xmlenc11#pbkdf2";
21const HKDF: &str = "http://www.w3.org/2021/04/xmldsig-more#hkdf";
22
23/// Identity/context field in XMLEnc's ConcatKDF OtherInfo.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum ConcatKdfField {
26    /// AlgorithmID, interpreted by the application.
27    AlgorithmId,
28    /// Originator's PartyUInfo.
29    PartyUInfo,
30    /// Recipient's PartyVInfo.
31    PartyVInfo,
32    /// Supplementary public information.
33    SuppPubInfo,
34    /// Supplementary private information.
35    SuppPrivInfo,
36}
37
38/// Parsed XML key-derivation parameters, without private key material.
39///
40/// Parsing does not grant permission to derive a key. The enclosing operation
41/// must validate its compiled policy, reserve work and gate provider execution.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct KeyDerivationMethod {
44    algorithm: String,
45    digest: String,
46    salt: Vec<u8>,
47    context: Vec<u8>,
48    context_bits: Option<usize>,
49    concat_fields: [Option<(usize, usize)>; 5],
50    iterations: u64,
51    key_length: Option<usize>,
52}
53
54impl KeyDerivationMethod {
55    pub(super) fn validate_metadata(&self, maximum: usize) -> Result<(), XmlEncError> {
56        serialize::validate_metadata(self, maximum)
57    }
58    /// Serialize public KDF parameters without secret key material. The exact
59    /// escaped output length is checked before allocating the output buffer.
60    /// Field boundaries and significant ConcatKDF bits survive transport.
61    pub fn to_xml(&self, resources: &crate::policy::ResourcePolicy) -> Result<String, XmlEncError> {
62        serialize::serialize(self, resources)
63    }
64    pub(super) fn hkdf(digest: String, salt: Vec<u8>, context: Vec<u8>, key_length: usize) -> Self {
65        Self {
66            algorithm: HKDF.into(),
67            digest,
68            salt,
69            context,
70            context_bits: None,
71            concat_fields: [None; 5],
72            iterations: 0,
73            key_length: Some(key_length),
74        }
75    }
76    /// Execute parsed parameters under the enclosing operation's shared
77    /// allowance. Explicit XML KeyLength must match the consuming cipher.
78    pub fn derive_key(
79        &self,
80        output_len: usize,
81        provider: &dyn crate::provider::CryptoProvider,
82        secret: &[u8],
83        budget: &mut super::KeyEstablishmentBudget<'_>,
84    ) -> Result<zeroize::Zeroizing<Vec<u8>>, XmlEncError> {
85        budget.derive_key(provider, &self.parameters(output_len)?, secret)
86    }
87    /// Borrow one original ConcatKDF field as significant bits, without copying
88    /// its bytes. None distinguishes omission from an explicitly empty field.
89    ///
90    /// XMLEnc 1.1 section 5.4.1 requires application-specific validation of the
91    /// algorithm and party identities. Combining OtherInfo must not erase the
92    /// boundaries needed for that check.
93    pub fn concat_field_bits(
94        &self,
95        field: ConcatKdfField,
96    ) -> Option<impl Iterator<Item = bool> + '_> {
97        let index = match field {
98            ConcatKdfField::AlgorithmId => 0,
99            ConcatKdfField::PartyUInfo => 1,
100            ConcatKdfField::PartyVInfo => 2,
101            ConcatKdfField::SuppPubInfo => 3,
102            ConcatKdfField::SuppPrivInfo => 4,
103        };
104        let (start, length) = self.concat_fields[index]?;
105        Some((start..start + length).map(|bit| self.context[bit / 8] & (1 << (7 - bit % 8)) != 0))
106    }
107    /// Bind the requested key width supplied by EncryptionMethod. An explicit
108    /// XML KeyLength must agree; it is not an override of the consuming cipher.
109    pub fn parameters(&self, output_len: usize) -> Result<KdfParameters<'_>, XmlEncError> {
110        if output_len == 0 || self.key_length.is_some_and(|length| length != output_len) {
111            return Err(invalid(
112                "KDF KeyLength does not match the consuming algorithm",
113            ));
114        }
115        Ok(KdfParameters {
116            algorithm: &self.algorithm,
117            digest: Some(&self.digest),
118            salt: &self.salt,
119            info: match self.context_bits {
120                Some(bit_len) => KdfContext::Bits {
121                    bytes: &self.context,
122                    bit_len,
123                },
124                None => KdfContext::Octets(&self.context),
125            },
126            iterations: self.iterations,
127            output_len,
128        })
129    }
130}
131
132/// Parse a standalone xenc11:KeyDerivationMethod with the same XML and metadata
133/// bounds as the containing decryption operation. No resource is fetched and
134/// no provider or key resolver is invoked by this function.
135pub fn parse_key_derivation_method(
136    xml: &str,
137    policy: &crate::policy::DecryptionPolicy,
138) -> Result<KeyDerivationMethod, XmlEncError> {
139    parse_key_derivation_method_with_backend(xml, policy, crate::XmlBackend::default())
140}
141
142/// Parse KDF parameters using the operation-selected XML backend.
143pub fn parse_key_derivation_method_with_backend(
144    xml: &str,
145    policy: &crate::policy::DecryptionPolicy,
146    backend: crate::XmlBackend,
147) -> Result<KeyDerivationMethod, XmlEncError> {
148    policy.validate()?;
149    policy.resources.validate_xml_document_len(xml.len())?;
150    let settings =
151        DocumentParseSettings::from_policy(&policy.xml, &policy.resources).with_backend(backend);
152    let budget = XmlParseWorkBudget::from_resources(&policy.resources);
153    let document = parse_borrowed_with_settings_and_budget(xml, settings, Some(&budget))
154        .map_err(|error| map_document_error(error, settings))?;
155    parse_node(
156        document.root_element(),
157        policy.resources.max_encryption_metadata_bytes,
158    )
159}
160
161pub(super) fn parse_node(
162    node: Node<'_, '_>,
163    maximum: usize,
164) -> Result<KeyDerivationMethod, XmlEncError> {
165    require(node, XMLENC11_NS, "KeyDerivationMethod")?;
166    let algorithm = attribute(node, "Algorithm", maximum)?;
167    let parameters = single_child(node)?;
168    element_content(parameters)?;
169    let mut result = KeyDerivationMethod {
170        algorithm: algorithm.to_owned(),
171        digest: String::new(),
172        salt: Vec::new(),
173        context: Vec::new(),
174        context_bits: None,
175        concat_fields: [None; 5],
176        iterations: 0,
177        key_length: None,
178    };
179    match algorithm {
180        CONCAT => {
181            require(parameters, XMLENC11_NS, "ConcatKDFParams")?;
182            result.digest = algorithm_child(
183                single_child(parameters)?,
184                XMLDSIG_NS,
185                "DigestMethod",
186                maximum,
187            )?;
188            // XMLEnc 1.1 section 5.4.1: concatenate the original UNPADDED
189            // bit strings, not hexBinary storage or each attribute's prefix.
190            // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-ConcatKDF
191            let fields = [
192                "AlgorithmID",
193                "PartyUInfo",
194                "PartyVInfo",
195                "SuppPubInfo",
196                "SuppPrivInfo",
197            ]
198            .map(|name| parameters.attribute(name));
199            let (bytes, bits, boundaries) = concat_context(fields, maximum)?;
200            result.context = bytes;
201            result.context_bits = Some(bits);
202            result.concat_fields = boundaries;
203        }
204        PBKDF2 => {
205            require(parameters, XMLENC11_NS, "PBKDF2-params")?;
206            let mut children = parameters.children().filter(Node::is_element);
207            let salt = next(&mut children, XMLENC11_NS, "Salt")?;
208            let specified = single_child(salt)?;
209            if specified.has_tag_name((XMLENC11_NS, "OtherSource")) {
210                // This is an algorithm invocation, not a URI or a salt value.
211                // Do not silently treat unsupported derivation as empty salt.
212                return Err(XmlEncError::UnsupportedAlgorithm(
213                    attribute(specified, "Algorithm", maximum)?.to_owned(),
214                ));
215            }
216            require(specified, XMLENC11_NS, "Specified")?;
217            result.salt = decode_bounded_base64_text(specified, "Specified", maximum)?;
218            result.iterations = positive(
219                next(&mut children, XMLENC11_NS, "IterationCount")?,
220                "IterationCount",
221                maximum,
222            )?;
223            // XML's KeyLength and PRF are REQUIRED, unlike ASN.1 defaults.
224            // XMLEnc 1.1 section 5.4.2:
225            // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-PBKDF2
226            result.key_length = Some(
227                usize::try_from(positive(
228                    next(&mut children, XMLENC11_NS, "KeyLength")?,
229                    "KeyLength",
230                    maximum,
231                )?)
232                .map_err(|_| invalid("KeyLength exceeds platform width"))?,
233            );
234            result.digest = algorithm_child(
235                next(&mut children, XMLENC11_NS, "PRF")?,
236                XMLENC11_NS,
237                "PRF",
238                maximum,
239            )?;
240            if children.next().is_some() {
241                return Err(invalid("unexpected PBKDF2 parameter"));
242            }
243        }
244        HKDF => {
245            // libxmlsec1's HKDFParams profile is distinct from RFC 9231
246            // section 2.8.1's AgreementMethod layout and hex encodings.
247            // https://www.rfc-editor.org/rfc/rfc9231.html#section-2.8.1
248            require(parameters, MORE_NS, "HKDFParams")?;
249            let mut children = parameters.children().filter(Node::is_element).peekable();
250            result.digest = algorithm_child(
251                next(&mut children, MORE_NS, "PRF")?,
252                MORE_NS,
253                "PRF",
254                maximum,
255            )?;
256            if children
257                .peek()
258                .is_some_and(|node| node.has_tag_name((MORE_NS, "Salt")))
259            {
260                result.salt = decode_bounded_base64_text(
261                    next(&mut children, MORE_NS, "Salt")?,
262                    "Salt",
263                    maximum,
264                )?;
265            }
266            if children
267                .peek()
268                .is_some_and(|node| node.has_tag_name((MORE_NS, "Info")))
269            {
270                result.context = decode_bounded_base64_text(
271                    next(&mut children, MORE_NS, "Info")?,
272                    "Info",
273                    maximum,
274                )?;
275            }
276            if children
277                .peek()
278                .is_some_and(|node| node.has_tag_name((MORE_NS, "KeyLength")))
279            {
280                result.key_length = Some(
281                    usize::try_from(positive(
282                        next(&mut children, MORE_NS, "KeyLength")?,
283                        "KeyLength",
284                        maximum,
285                    )?)
286                    .map_err(|_| invalid("KeyLength exceeds platform width"))?,
287                );
288            }
289            if children.next().is_some() {
290                return Err(invalid("unexpected HKDF parameter"));
291            }
292        }
293        _ => return Err(XmlEncError::UnsupportedAlgorithm(algorithm.to_owned())),
294    }
295    Ok(result)
296}
297
298fn invalid(message: &str) -> XmlEncError {
299    XmlEncError::InvalidStructure(message.into())
300}
301
302fn single_child<'a>(node: Node<'a, 'a>) -> Result<Node<'a, 'a>, XmlEncError> {
303    element_content(node)?;
304    let mut children = node.children().filter(Node::is_element);
305    let child = children
306        .next()
307        .ok_or(XmlEncError::MissingRequired("KDF parameters"))?;
308    if children.next().is_some() {
309        return Err(invalid("expected exactly one KDF parameter element"));
310    }
311    Ok(child)
312}
313
314fn element_content(node: Node<'_, '_>) -> Result<(), XmlEncError> {
315    for child in node.children() {
316        if child.is_text()
317            && child.text().is_some_and(|text| {
318                !text
319                    .bytes()
320                    .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
321            })
322        {
323            return Err(invalid("KDF parameter container contains character data"));
324        }
325    }
326    Ok(())
327}
328
329fn next<'a>(
330    children: &mut impl Iterator<Item = Node<'a, 'a>>,
331    namespace: &str,
332    name: &'static str,
333) -> Result<Node<'a, 'a>, XmlEncError> {
334    let node = children.next().ok_or(XmlEncError::MissingRequired(name))?;
335    require(node, namespace, name)?;
336    Ok(node)
337}
338
339fn attribute<'a>(node: Node<'a, 'a>, name: &str, maximum: usize) -> Result<&'a str, XmlEncError> {
340    let value = node
341        .attribute(name)
342        .ok_or_else(|| invalid(&format!("missing {name}")))?;
343    validate_metadata_len(value.len(), maximum)?;
344    Ok(value)
345}
346
347fn algorithm_child(
348    node: Node<'_, '_>,
349    namespace: &str,
350    name: &str,
351    maximum: usize,
352) -> Result<String, XmlEncError> {
353    require(node, namespace, name)?;
354    element_content(node)?;
355    if node.children().any(|child| child.is_element()) {
356        return Err(invalid("unsupported digest/PRF parameters"));
357    }
358    Ok(attribute(node, "Algorithm", maximum)?.to_owned())
359}
360
361fn positive(node: Node<'_, '_>, field: &'static str, maximum: usize) -> Result<u64, XmlEncError> {
362    let text = bounded_simple_text_with_limit(node, field, maximum)?;
363    let value = text.trim_matches([' ', '\t', '\r', '\n']);
364    let value = value.strip_prefix('+').unwrap_or(value);
365    if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
366        return Err(invalid("KDF integer must be positive"));
367    }
368    let number = value
369        .parse::<u64>()
370        .map_err(|_| invalid("KDF integer exceeds u64"))?;
371    if number == 0 {
372        return Err(invalid("KDF integer must be positive"));
373    }
374    Ok(number)
375}
376
377fn nibble(byte: u8) -> Result<u8, XmlEncError> {
378    match byte {
379        b'0'..=b'9' => Ok(byte - b'0'),
380        b'a'..=b'f' => Ok(byte - b'a' + 10),
381        b'A'..=b'F' => Ok(byte - b'A' + 10),
382        _ => Err(invalid("invalid ConcatKDF hexBinary")),
383    }
384}
385
386fn hex_byte(bytes: &[u8]) -> Result<u8, XmlEncError> {
387    Ok(nibble(bytes[0])? * 16 + nibble(bytes[1])?)
388}
389
390fn concat_field(value: &str, maximum: usize) -> Result<(&[u8], usize), XmlEncError> {
391    validate_metadata_len(value.len(), maximum)?;
392    let hex = value.trim_matches([' ', '\t', '\r', '\n']).as_bytes();
393    if hex.is_empty() {
394        return Ok((hex, 0));
395    }
396    if !hex.len().is_multiple_of(2) {
397        return Err(invalid("odd ConcatKDF hexBinary length"));
398    }
399    let padding = hex_byte(&hex[..2])? as usize;
400    let payload = &hex[2..];
401    if padding > 7 || payload.is_empty() && padding != 0 {
402        return Err(invalid("invalid ConcatKDF padding count"));
403    }
404    for pair in payload.as_chunks::<2>().0 {
405        hex_byte(pair)?;
406    }
407    if padding != 0 && hex_byte(&payload[payload.len() - 2..])? & ((1 << padding) - 1) != 0 {
408        return Err(invalid("nonzero ConcatKDF padding bits"));
409    }
410    Ok((payload, payload.len() / 2 * 8 - padding))
411}
412
413type ConcatContext = (Vec<u8>, usize, [Option<(usize, usize)>; 5]);
414
415fn concat_context(fields: [Option<&str>; 5], maximum: usize) -> Result<ConcatContext, XmlEncError> {
416    let mut total = 0usize;
417    let mut boundaries = [None; 5];
418    // Validate every field and calculate capacity before allocating anything.
419    for (index, field) in fields.iter().enumerate() {
420        let Some(value) = field else {
421            continue;
422        };
423        let (_, bits) = concat_field(value, maximum)?;
424        boundaries[index] = Some((total, bits));
425        total = total
426            .checked_add(bits)
427            .ok_or_else(|| invalid("ConcatKDF context overflow"))?;
428    }
429    validate_metadata_len(total.div_ceil(8), maximum)?;
430    let mut output = vec![0; total.div_ceil(8)];
431    let mut offset = 0;
432    for value in fields.into_iter().flatten() {
433        let (hex, bits) = concat_field(value, maximum)?;
434        let mut remaining = bits;
435        for pair in hex.as_chunks::<2>().0 {
436            let byte = hex_byte(pair)?;
437            let shift = offset % 8;
438            let index = offset / 8;
439            output[index] |= byte >> shift;
440            if shift != 0 && index + 1 < output.len() {
441                output[index + 1] |= byte << (8 - shift);
442            }
443            let consumed = remaining.min(8);
444            offset += consumed;
445            remaining -= consumed;
446        }
447    }
448    Ok((output, total, boundaries))
449}