Skip to main content

xml_sec/xmlenc/
agreement.rs

1//! Agreement metadata remains separate from application-owned private keys.
2
3use crate::policy::{KeyAgreementAlgorithm, ResourcePolicy};
4use crate::provider::{KdfContext, KdfParameters, KeyAgreementParameters};
5use crate::xml::dom::Node;
6use crate::xmldsig::parse::{KeyInfo, KeyInfoParsingSession};
7
8use super::parse::{decode_bounded_base64_text, require_element, validate_metadata_len};
9use super::types::{XMLDSIG_NS, XMLENC_NS, XMLENC11_NS};
10use super::{KeyDerivationMethod, XmlEncError};
11
12/// Public agreement descriptor. Key information is advisory; a document never
13/// supplies the application's private handle or chooses its security profile.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct AgreementMethod {
16    /// Exact agreement mechanism, independently permission checked at execution.
17    pub algorithm: KeyAgreementAlgorithm,
18    /// Explicit derivation; omission requires application knowledge except for
19    /// the legacy DH algorithm, whose derivation is specified by its URI.
20    pub method: Option<KeyDerivationMethod>,
21    /// Public agreement nonce, retained in the trusted descriptor comparison.
22    /// Legacy DH incorporates it directly; explicit KDFs use their own context
23    /// fields rather than an undocumented nonce concatenation.
24    pub nonce: Vec<u8>,
25    /// Optional legacy DH digest. No default digest is guessed.
26    pub legacy_digest: Option<String>,
27    /// Originator public key or application lookup hints in the original role.
28    pub originator: Option<KeyInfo>,
29    /// Recipient lookup hints, never permission to substitute a private key.
30    pub recipient: Option<KeyInfo>,
31}
32
33impl AgreementMethod {
34    /// Establish a consuming key with explicitly bound private and peer keys.
35    /// The caller validates party identities before invoking this method.
36    pub fn derive_key(
37        &self,
38        consuming_algorithm: &str,
39        output_len: usize,
40        key: &dyn crate::provider::KeyAgreementKey,
41        peer: &[u8],
42        provider: &dyn crate::provider::CryptoProvider,
43        budget: &mut super::KeyEstablishmentBudget<'_>,
44    ) -> Result<zeroize::Zeroizing<Vec<u8>>, XmlEncError> {
45        let agreement = KeyAgreementParameters {
46            algorithm: self.algorithm.uri(),
47            peer_public_key: peer,
48        };
49        let parameters = self.parameters(consuming_algorithm, output_len)?;
50        budget.agree_and_derive(provider, key, &agreement, &parameters)
51    }
52
53    pub(super) fn parameters<'a>(
54        &'a self,
55        consuming_algorithm: &'a str,
56        output_len: usize,
57    ) -> Result<KdfParameters<'a>, XmlEncError> {
58        if self.algorithm == KeyAgreementAlgorithm::LegacyDh {
59            // XMLEnc 1.1 §5.6.2.2 binds EncryptionMethod's URI, not the
60            // agreement URI, and its nonce/decimal consuming key width.
61            // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-DHKeyAgreementLegacyKDF
62            return Ok(KdfParameters {
63                algorithm: self.algorithm.uri(),
64                digest: self.legacy_digest.as_deref(),
65                salt: &[],
66                info: KdfContext::LegacyDh {
67                    encryption_algorithm: consuming_algorithm,
68                    nonce: &self.nonce,
69                },
70                iterations: 0,
71                output_len,
72            });
73        }
74        // XMLEnc 1.1 sections 5.4.1 and 5.6 define explicit KDF inputs but
75        // do not define an additional KA-Nonce-to-OtherInfo concatenation.
76        // Preserve the nonce in descriptor identity; applications requiring
77        // fresh explicit-KDF output must also vary its declared context/salt.
78        // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-ConcatKDF
79        // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-Alg-KeyAgreement
80        self.method
81            .as_ref()
82            .ok_or(XmlEncError::MissingRequired(
83                "agreement key derivation method",
84            ))?
85            .parameters(output_len)
86    }
87}
88
89pub(super) fn parse(
90    node: Node<'_, '_>,
91    resources: &ResourcePolicy,
92    key_info: &mut KeyInfoParsingSession<'_>,
93    provider: &dyn crate::provider::CryptoProvider,
94) -> Result<AgreementMethod, XmlEncError> {
95    require_element(node, XMLENC_NS, "AgreementMethod")?;
96    let algorithm = node
97        .attribute("Algorithm")
98        .ok_or(XmlEncError::MissingRequired("AgreementMethod Algorithm"))?;
99    validate_metadata_len(algorithm.len(), resources.max_encryption_metadata_bytes)?;
100    let algorithm = KeyAgreementAlgorithm::from_uri(algorithm)
101        .ok_or_else(|| XmlEncError::UnsupportedAlgorithm(algorithm.into()))?;
102    let mut result = AgreementMethod {
103        algorithm,
104        method: None,
105        nonce: Vec::new(),
106        legacy_digest: None,
107        originator: None,
108        recipient: None,
109    };
110    let mut phase = 0;
111    let mut nonce_seen = false;
112    // XMLEnc 1.1 §5.6: AgreementMethodType is mixed=true. Only element
113    // sequence is constrained; non-whitespace text is not a schema error.
114    // Unlike libxmlsec1 KAMRead, the two KeyInfoType roles are optional.
115    // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-Alg-KeyAgreement
116    for child in node.children().filter(Node::is_element) {
117        match (child.tag_name().namespace(), child.tag_name().name()) {
118            (Some(XMLENC_NS), "KA-Nonce") if phase == 0 && !nonce_seen => {
119                result.nonce = decode_bounded_base64_text(
120                    child,
121                    "KA-Nonce",
122                    resources.max_encryption_metadata_bytes,
123                )?;
124                nonce_seen = true;
125            }
126            (Some(XMLENC11_NS), "KeyDerivationMethod") if phase <= 1 && result.method.is_none() => {
127                result.method = Some(super::key_derivation::parse_node(
128                    child,
129                    resources.max_encryption_metadata_bytes,
130                )?);
131                phase = 1;
132            }
133            (Some(XMLDSIG_NS), "DigestMethod") if phase <= 1 && result.legacy_digest.is_none() => {
134                if algorithm != KeyAgreementAlgorithm::LegacyDh
135                    || child.children().any(|node| node.is_element())
136                {
137                    return Err(invalid(
138                        "DigestMethod is only defined for legacy DH agreement",
139                    ));
140                }
141                let digest = child
142                    .attribute("Algorithm")
143                    .ok_or(XmlEncError::MissingRequired("DigestMethod Algorithm"))?;
144                validate_metadata_len(digest.len(), resources.max_encryption_metadata_bytes)?;
145                result.legacy_digest = Some(digest.into());
146                phase = 1;
147            }
148            (Some(XMLENC_NS), "OriginatorKeyInfo") if phase <= 1 => {
149                preflight_role_metadata(child, resources.max_encryption_metadata_bytes)?;
150                result.originator = Some(
151                    key_info
152                        .parse_agreement_role(child, provider)
153                        .map_err(map_key_info_error)?,
154                );
155                phase = 2;
156            }
157            (Some(XMLENC_NS), "RecipientKeyInfo") if phase <= 2 => {
158                preflight_role_metadata(child, resources.max_encryption_metadata_bytes)?;
159                result.recipient = Some(
160                    key_info
161                        .parse_agreement_role(child, provider)
162                        .map_err(map_key_info_error)?,
163                );
164                phase = 3;
165            }
166            (Some(namespace), _) if namespace != XMLENC_NS && phase <= 1 => {
167                // The schema's ##other extension point permits unrelated
168                // metadata. Recognized duplicate parameters must not fall here.
169                if child.has_tag_name((XMLENC11_NS, "KeyDerivationMethod"))
170                    || child.has_tag_name((XMLDSIG_NS, "DigestMethod"))
171                {
172                    return Err(invalid("duplicate agreement parameter"));
173                }
174                phase = 1;
175            }
176            _ => return Err(invalid("unexpected or unordered AgreementMethod child")),
177        }
178    }
179    if algorithm == KeyAgreementAlgorithm::LegacyDh && result.method.is_some() {
180        return Err(invalid("legacy DH URI fixes the derivation algorithm"));
181    }
182    Ok(result)
183}
184
185fn invalid(message: &str) -> XmlEncError {
186    XmlEncError::InvalidStructure(message.into())
187}
188
189fn preflight_role_metadata(node: Node<'_, '_>, maximum: usize) -> Result<(), XmlEncError> {
190    for child in node.descendants().filter(Node::is_element) {
191        for attribute in child.attributes() {
192            validate_metadata_len(attribute.value().len(), maximum)?;
193        }
194        if child.children().any(|node| node.is_element()) {
195            continue;
196        }
197        let binary = matches!(
198            (child.tag_name().namespace(), child.tag_name().name()),
199            (
200                Some(XMLDSIG_NS),
201                "Modulus"
202                    | "Exponent"
203                    | "P"
204                    | "Q"
205                    | "G"
206                    | "Y"
207                    | "J"
208                    | "Seed"
209                    | "PgenCounter"
210                    | "X509Certificate"
211                    | "X509CRL"
212                    | "X509SKI",
213            ) | (
214                Some(XMLENC_NS),
215                "P" | "Q" | "Generator" | "Public" | "seed" | "pgenCounter"
216            ) | (
217                Some(crate::xmldsig::parse::XMLDSIG11_NS),
218                "PublicKey" | "DEREncodedKeyValue" | "X509Digest",
219            )
220        );
221        if binary {
222            let payload = crate::xmldsig::whitespace::XmlBase64Payload::bounded(
223                child,
224                usize::MAX,
225                usize::MAX,
226            )
227            .map_err(|_| invalid("invalid agreement key binary metadata"))?;
228            validate_metadata_len(payload.decoded_len, maximum)?;
229        } else if child.tag_name().namespace() == Some(XMLDSIG_NS)
230            && matches!(
231                child.tag_name().name(),
232                "KeyName" | "X509SubjectName" | "X509IssuerName" | "X509SerialNumber" | "XPath"
233            )
234        {
235            let mut length = 0usize;
236            for text in child
237                .children()
238                .filter(Node::is_text)
239                .filter_map(|node| node.text())
240            {
241                length = length
242                    .checked_add(text.len())
243                    .ok_or_else(|| invalid("agreement metadata length overflow"))?;
244                validate_metadata_len(length, maximum)?;
245            }
246        }
247    }
248    Ok(())
249}
250
251pub(super) fn validate_role_metadata(info: &KeyInfo, maximum: usize) -> Result<(), XmlEncError> {
252    use crate::xmldsig::parse::{KeyInfoSource, KeyValueInfo, RetrievalMethodTransforms};
253    let check = |bytes: &[u8]| validate_metadata_len(bytes.len(), maximum);
254    let check_optional = |bytes: &Option<Vec<u8>>| check(bytes.as_deref().unwrap_or(&[]));
255    for source in &info.sources {
256        match source {
257            KeyInfoSource::KeyName(value) | KeyInfoSource::KeyInfoReference { uri: value } => {
258                check(value.as_bytes())?
259            }
260            KeyInfoSource::DerEncodedKeyValue(value) => check(value)?,
261            KeyInfoSource::KeyValue(value) => match value {
262                KeyValueInfo::Dh {
263                    p,
264                    q,
265                    generator,
266                    public,
267                    seed,
268                    pgen_counter,
269                } => {
270                    for bytes in [p, q, generator, seed, pgen_counter] {
271                        check_optional(bytes)?;
272                    }
273                    check(public)?;
274                }
275                KeyValueInfo::Dsa { p, q, g, y } => {
276                    for bytes in [p, q, g] {
277                        check_optional(bytes)?;
278                    }
279                    check(y)?;
280                }
281                KeyValueInfo::Rsa { modulus, exponent } => {
282                    check(modulus)?;
283                    check(exponent)?;
284                }
285                KeyValueInfo::Ec {
286                    curve_oid,
287                    public_key,
288                } => {
289                    check(curve_oid.as_bytes())?;
290                    check(public_key)?;
291                }
292                KeyValueInfo::Unsupported {
293                    namespace,
294                    local_name,
295                } => {
296                    if let Some(namespace) = namespace {
297                        check(namespace.as_bytes())?;
298                    }
299                    check(local_name.as_bytes())?;
300                }
301                KeyValueInfo::InvalidEcKeyValue => {}
302            },
303            KeyInfoSource::RetrievalMethod {
304                uri,
305                resource_type,
306                transforms,
307            } => {
308                check(uri.as_bytes())?;
309                if let Some(kind) = resource_type {
310                    check(kind.as_bytes())?;
311                }
312                if let RetrievalMethodTransforms::X509DataNodeSetFilter {
313                    expression,
314                    namespaces,
315                } = transforms
316                {
317                    check(expression.as_bytes())?;
318                    for (prefix, uri) in namespaces {
319                        check(prefix.as_bytes())?;
320                        check(uri.as_bytes())?;
321                    }
322                }
323            }
324            KeyInfoSource::X509Data(data) => {
325                for bytes in data.certificates.iter().chain(&data.crls).chain(&data.skis) {
326                    check(bytes)?;
327                }
328                for value in &data.subject_names {
329                    check(value.as_bytes())?;
330                }
331                for (issuer, serial) in &data.issuer_serials {
332                    check(issuer.as_bytes())?;
333                    check(serial.as_bytes())?;
334                }
335                for (algorithm, bytes) in &data.digests {
336                    check(algorithm.as_bytes())?;
337                    check(bytes)?;
338                }
339            }
340        }
341    }
342    Ok(())
343}
344
345pub(super) fn map_key_info_error(error: crate::xmldsig::parse::ParseError) -> XmlEncError {
346    match error {
347        crate::xmldsig::parse::ParseError::Policy(error) => XmlEncError::Policy(error),
348        crate::xmldsig::parse::ParseError::Provider(error) => XmlEncError::Provider(error),
349        error => invalid(&error.to_string()),
350    }
351}
352
353/// Bind an expected agreement descriptor and trusted keys to a content cipher.
354/// Compare XML parameters before provider work; party/KDF substitutions fail.
355pub struct AgreementDecryptor<'a> {
356    expected: &'a AgreementMethod,
357    key: &'a dyn crate::provider::KeyAgreementKey,
358    peer: &'a [u8],
359    purpose: AgreementPurpose,
360}
361
362enum AgreementPurpose {
363    Content(super::DataEncryptionAlgorithm),
364    Wrapping(super::KeyWrapAlgorithm),
365}
366
367impl<'a> AgreementDecryptor<'a> {
368    /// Expectations must come from the application's trusted configuration,
369    /// not be copied from the message as a substitute for party validation.
370    pub fn content(
371        expected: &'a AgreementMethod,
372        key: &'a dyn crate::provider::KeyAgreementKey,
373        peer: &'a [u8],
374        algorithm: super::DataEncryptionAlgorithm,
375    ) -> Self {
376        Self {
377            expected,
378            key,
379            peer,
380            purpose: AgreementPurpose::Content(algorithm),
381        }
382    }
383
384    /// Bind this agreement to a KEK algorithm instead of a content cipher.
385    pub fn wrapping(
386        expected: &'a AgreementMethod,
387        key: &'a dyn crate::provider::KeyAgreementKey,
388        peer: &'a [u8],
389        algorithm: super::KeyWrapAlgorithm,
390    ) -> Self {
391        Self {
392            expected,
393            key,
394            peer,
395            purpose: AgreementPurpose::Wrapping(algorithm),
396        }
397    }
398
399    fn derive(
400        &self,
401        provider: &dyn crate::provider::CryptoProvider,
402        uri: &str,
403        width: usize,
404        policy: &crate::policy::DecryptionPolicy,
405        budget: &mut super::KeyCandidateBudget,
406    ) -> Result<Vec<crate::provider::RecoveredContentKey>, XmlEncError> {
407        let agreement = KeyAgreementParameters {
408            algorithm: self.expected.algorithm.uri(),
409            peer_public_key: self.peer,
410        };
411        let parameters = self.expected.parameters(uri, width)?;
412        let mut secret =
413            budget.agree_and_derive(provider, policy, self.key, &agreement, &parameters)?;
414        Ok(vec![crate::provider::RecoveredContentKey::confirmed(
415            core::mem::take(&mut *secret),
416        )])
417    }
418}
419
420impl super::DecryptionKeyResolver for AgreementDecryptor<'_> {
421    fn resolve_key_encryption_keys_with_policy(
422        &self,
423        provider: &dyn crate::provider::CryptoProvider,
424        algorithm: super::KeyWrapAlgorithm,
425        source: super::KeyEncryptionKeySource<'_>,
426        policy: &crate::policy::DecryptionPolicy,
427        budget: &mut super::KeyCandidateBudget,
428    ) -> Result<Vec<crate::provider::RecoveredContentKey>, XmlEncError> {
429        let super::KeyEncryptionKeySource::Agreement(descriptor) = source else {
430            return Err(XmlEncError::KeyNotFound);
431        };
432        if !matches!(self.purpose, AgreementPurpose::Wrapping(expected) if expected == algorithm)
433            || descriptor != self.expected
434        {
435            return Err(XmlEncError::KeyNotFound);
436        }
437        // Reserve the consuming unwrap too, before either expensive primitive.
438        budget.require_available(3)?;
439        self.derive(
440            provider,
441            algorithm.uri(),
442            algorithm.key_len(),
443            policy,
444            budget,
445        )
446    }
447    fn resolve_agreement_content_keys_with_policy(
448        &self,
449        provider: &dyn crate::provider::CryptoProvider,
450        algorithm: super::DataEncryptionAlgorithm,
451        descriptor: &AgreementMethod,
452        policy: &crate::policy::DecryptionPolicy,
453        budget: &mut super::KeyCandidateBudget,
454    ) -> Result<Vec<crate::provider::RecoveredContentKey>, XmlEncError> {
455        if !matches!(self.purpose, AgreementPurpose::Content(expected) if expected == algorithm)
456            || descriptor != self.expected
457        {
458            return Err(XmlEncError::KeyNotFound);
459        }
460        self.derive(
461            provider,
462            algorithm.uri(),
463            algorithm.key_len(),
464            policy,
465            budget,
466        )
467    }
468
469    fn resolve_key(
470        &self,
471        _provider: &dyn crate::provider::CryptoProvider,
472        _algorithm: super::DataEncryptionAlgorithm,
473        _encrypted_key: Option<&super::EncryptedKey>,
474    ) -> Result<Vec<u8>, XmlEncError> {
475        // Raw key resolution cannot assert that transported agreement metadata
476        // was checked against the application's expected parties and KDF.
477        Err(XmlEncError::KeyNotFound)
478    }
479}