Skip to main content

xml_sec/
key_establishment.rs

1//! Shared operation-wide key establishment accounting and KEM dispatch.
2//!
3//! Requests supply keys; the immutable policy supplies permission. Keep one
4//! usage value across every key source and retry, including failed attempts.
5
6use crate::policy::{KeyEstablishmentPolicy, PolicyViolation};
7use crate::provider::{
8    CryptoProvider, EncapsulatedKey, KeyDecapsulationKey, KeyEncapsulationAlgorithm,
9    KeyEncapsulationKey, ProviderCapability, ProviderError, ProviderInputError,
10};
11use zeroize::Zeroizing;
12
13/// Failures at the shared policy/provider boundary.
14#[derive(Debug, thiserror::Error)]
15pub enum KeyEstablishmentError {
16    /// Experimental XML grammar or consuming key width is invalid.
17    #[error("invalid EncapsulationMechanism: {0}")]
18    Structure(&'static str),
19    /// Deployment permission or operation allowance was denied.
20    #[error(transparent)]
21    Policy(#[from] PolicyViolation),
22    /// Selected engine rejected the operation. No other engine is tried.
23    #[error(transparent)]
24    Provider(#[from] ProviderError),
25}
26
27/// libxmlsec1's experimental namespace, distinct from the W3C namespaces.
28pub const ENCAPSULATION_NS: &str = "http://www.aleksey.com/xmlsec/2025/12/xmldsig-more#";
29
30/// Owned experimental mechanism retained by parsed XML Encryption input.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct EncapsulationMechanism {
33    /// Exact parameter set, not deployment permission.
34    pub algorithm: KeyEncapsulationAlgorithm,
35    /// Recipient hints; trusted private keys remain request-owned.
36    pub key_info: crate::xmldsig::KeyInfo,
37    /// Public KEM ciphertext with the parameter set's exact fixed width.
38    pub ciphertext: Vec<u8>,
39}
40
41/// Borrowed, fully framed mechanism. Public ciphertext is decoded into fixed
42/// storage, so parsing cannot allocate an attacker-selected temporary buffer.
43pub struct ParsedEncapsulation<'a, 'input> {
44    /// Exact parameter set decoded from the mechanism.
45    pub algorithm: KeyEncapsulationAlgorithm,
46    /// Borrowed recipient hints, never a trust grant.
47    pub key_info: crate::Node<'a, 'input>,
48    /// Original ciphertext element for controlled template mutation.
49    pub cipher_value: crate::Node<'a, 'input>,
50    ciphertext: [u8; 1568],
51    ciphertext_len: usize,
52}
53
54impl ParsedEncapsulation<'_, '_> {
55    /// Fixed-width public ciphertext; empty only for a validated template.
56    pub fn ciphertext(&self) -> &[u8] {
57        &self.ciphertext[..self.ciphertext_len]
58    }
59
60    #[cfg(feature = "xmldsig")]
61    pub(crate) fn into_ciphertext(self) -> [u8; 1568] {
62        self.ciphertext
63    }
64}
65
66/// Validate the experimental ordered grammar and decode into fixed storage.
67pub fn parse_encapsulation<'a, 'input>(
68    node: crate::Node<'a, 'input>,
69    template: bool,
70) -> Result<ParsedEncapsulation<'a, 'input>, KeyEstablishmentError> {
71    use base64::Engine as _;
72    let invalid = KeyEstablishmentError::Structure;
73    if !node.has_tag_name((ENCAPSULATION_NS, "EncapsulationMechanism")) {
74        return Err(invalid("wrong element name or namespace"));
75    }
76    let algorithm = node
77        .attribute("Algorithm")
78        .and_then(KeyEncapsulationAlgorithm::from_uri)
79        .ok_or(invalid("unsupported or missing Algorithm"))?;
80    // This is the experimental donor contract, not a W3C schema requirement:
81    // xmlsec 1.3.13 src/transform_helpers.c, xmlSecTransformKEMRead.
82    // https://github.com/lsh123/xmlsec/blob/xmlsec-1_3_13/src/transform_helpers.c
83    let mut children = node.children().filter(crate::Node::is_element);
84    let key_info = children
85        .next()
86        .filter(|n| n.has_tag_name((crate::xmldsig::parse::XMLDSIG_NS, "KeyInfo")))
87        .ok_or(invalid("expected KeyInfo first"))?;
88    let data = children
89        .next()
90        .filter(|n| n.has_tag_name(("http://www.w3.org/2001/04/xmlenc#", "CipherData")))
91        .ok_or(invalid("expected CipherData after KeyInfo"))?;
92    if children.next().is_some() {
93        return Err(invalid("unexpected mechanism child"));
94    }
95    let mut children = data.children().filter(crate::Node::is_element);
96    let cipher_value = children
97        .next()
98        .filter(|n| n.has_tag_name(("http://www.w3.org/2001/04/xmlenc#", "CipherValue")))
99        .ok_or(invalid("expected CipherValue"))?;
100    if children.next().is_some() || cipher_value.children().any(|n| n.is_element()) {
101        return Err(invalid("unexpected CipherData or CipherValue child"));
102    }
103    for parent in [node, data] {
104        if parent.children().filter(crate::Node::is_text).any(|n| {
105            n.text()
106                .is_some_and(|s| !s.bytes().all(|b| matches!(b, b' ' | b'\t' | b'\r' | b'\n')))
107        }) {
108            return Err(invalid("non-whitespace mixed content"));
109        }
110    }
111    let mut encoded = [0u8; 2092];
112    let mut len = 0;
113    for text in cipher_value.children().filter(crate::Node::is_text) {
114        for byte in text.text().unwrap_or_default().bytes() {
115            if matches!(byte, b' ' | b'\t' | b'\r' | b'\n') {
116                continue;
117            }
118            if len == encoded.len() {
119                return Err(invalid("oversized ciphertext"));
120            }
121            encoded[len] = byte;
122            len += 1;
123        }
124    }
125    let mut ciphertext = [0u8; 1568];
126    // GeneralPurpose checks actual output bytes, including terminal padding,
127    // not decoded_len_estimate (1569 for a padded 1568-byte ciphertext).
128    // Decode directly into retained storage; no staging copy is necessary.
129    // https://docs.rs/base64/0.23.1/src/base64/engine/general_purpose/decode_suffix.rs.html
130    let ciphertext_len = base64::engine::general_purpose::STANDARD
131        .decode_slice(&encoded[..len], &mut ciphertext)
132        .map_err(|_| invalid("invalid ciphertext base64"))?;
133    if ciphertext_len != algorithm.ciphertext_len() && !(template && ciphertext_len == 0) {
134        return Err(invalid("ciphertext size does not match Algorithm"));
135    }
136    Ok(ParsedEncapsulation {
137        algorithm,
138        key_info,
139        cipher_value,
140        ciphertext,
141        ciphertext_len,
142    })
143}
144
145/// Find a single direct mechanism, rejecting ambiguity rather than choosing one.
146pub fn direct_encapsulation<'a, 'input>(
147    key_info: crate::Node<'a, 'input>,
148    template: bool,
149) -> Result<Option<ParsedEncapsulation<'a, 'input>>, KeyEstablishmentError> {
150    let mut mechanisms = key_info
151        .children()
152        .filter(|n| n.has_tag_name((ENCAPSULATION_NS, "EncapsulationMechanism")));
153    let Some(node) = mechanisms.next() else {
154        return Ok(None);
155    };
156    if mechanisms.next().is_some() {
157        return Err(KeyEstablishmentError::Structure(
158            "multiple mechanisms in one KeyInfo",
159        ));
160    }
161    parse_encapsulation(node, template).map(Some)
162}
163
164/// Monotonic reservations shared by KDF, agreement, and encapsulation.
165#[derive(Debug, Default)]
166pub struct KeyEstablishmentUsage {
167    pub(crate) hash_blocks: usize,
168    pub(crate) owned_bytes: usize,
169    pub(crate) modular_work: usize,
170    encapsulation_operations: usize,
171}
172
173impl KeyEstablishmentUsage {
174    /// Generate a fresh secret after permission, capability and resource gates.
175    pub fn encapsulate(
176        &mut self,
177        policy: &KeyEstablishmentPolicy,
178        provider: &dyn CryptoProvider,
179        key: &dyn KeyEncapsulationKey,
180    ) -> Result<EncapsulatedKey, KeyEstablishmentError> {
181        let algorithm = key.algorithm();
182        policy.validate()?;
183        policy.check_encapsulation(algorithm)?;
184        provider.require_capability(ProviderCapability::Encapsulate(algorithm))?;
185        provider.require_capability(ProviderCapability::Random)?;
186        self.reserve_encapsulation(policy, algorithm.ciphertext_len())?;
187        let result = provider.encapsulate_key(key)?;
188        if result.ciphertext.len() != algorithm.ciphertext_len() {
189            return Err(ProviderError::InvalidInput(ProviderInputError::MlKemCiphertext).into());
190        }
191        Ok(result)
192    }
193
194    /// Recover the real-or-rejection secret; never expose ciphertext validity.
195    pub fn decapsulate(
196        &mut self,
197        policy: &KeyEstablishmentPolicy,
198        provider: &dyn CryptoProvider,
199        key: &dyn KeyDecapsulationKey,
200        algorithm: KeyEncapsulationAlgorithm,
201        ciphertext: &[u8],
202    ) -> Result<Zeroizing<[u8; 32]>, KeyEstablishmentError> {
203        policy.validate()?;
204        policy.check_encapsulation(algorithm)?;
205        if key.algorithm() != algorithm || ciphertext.len() != algorithm.ciphertext_len() {
206            return Err(ProviderError::InvalidInput(ProviderInputError::MlKemCiphertext).into());
207        }
208        provider.require_capability(ProviderCapability::Decapsulate(algorithm))?;
209        self.reserve_encapsulation(policy, 0)?;
210        Ok(provider.decapsulate_key(key, ciphertext)?)
211    }
212
213    fn reserve_encapsulation(
214        &mut self,
215        policy: &KeyEstablishmentPolicy,
216        output_bytes: usize,
217    ) -> Result<(), PolicyViolation> {
218        let operations = reserve(
219            "key encapsulation operations",
220            self.encapsulation_operations,
221            1,
222            policy.max_encapsulation_operations,
223        )?;
224        // RustCrypto ml-kem 0.3.2 operates on fixed arrays when using already
225        // imported handles: pke.rs encrypt/decrypt and encapsulation_key.rs
226        // encapsulate_deterministic allocate no heap workspace. The adapter's
227        // ciphertext Vec is the only primitive-owned allocation; consumers
228        // reserve their secret copies separately. Do not invent a workspace
229        // multiplier from encoded key size.
230        self.commit_all(policy, 0, 0, output_bytes as u128)?;
231        self.encapsulation_operations = operations;
232        Ok(())
233    }
234
235    pub(crate) fn commit_all(
236        &mut self,
237        policy: &KeyEstablishmentPolicy,
238        blocks: u128,
239        modular: u128,
240        allocations: u128,
241    ) -> Result<(), PolicyViolation> {
242        let work = reserve(
243            crate::policy::resource_name::KEY_ESTABLISHMENT_HASH_BLOCKS,
244            self.hash_blocks,
245            blocks,
246            policy.max_hash_blocks,
247        )?;
248        let bytes = reserve(
249            crate::policy::resource_name::KEY_ESTABLISHMENT_OWNED_BYTES,
250            self.owned_bytes,
251            allocations,
252            policy.max_owned_bytes,
253        )?;
254        let modular = reserve(
255            crate::policy::resource_name::KEY_ESTABLISHMENT_MODULAR_WORK,
256            self.modular_work,
257            modular,
258            policy.max_modular_work,
259        )?;
260        self.hash_blocks = work;
261        self.owned_bytes = bytes;
262        self.modular_work = modular;
263        Ok(())
264    }
265}
266
267pub(crate) fn reserve(
268    resource: &'static str,
269    used: usize,
270    count: u128,
271    maximum: usize,
272) -> Result<usize, PolicyViolation> {
273    if used > maximum || count > (maximum - used) as u128 {
274        return Err(PolicyViolation::ResourceLimit {
275            resource,
276            maximum,
277            actual: usize::try_from(used as u128 + count).unwrap_or(usize::MAX),
278        });
279    }
280    Ok(used + count as usize)
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use crate::provider::RustCryptoProvider;
287
288    #[cfg(feature = "experimental-pq")]
289    struct FailedRandom;
290
291    #[cfg(feature = "experimental-pq")]
292    impl CryptoProvider for FailedRandom {
293        fn name(&self) -> &'static str {
294            "failed-random"
295        }
296        fn supports(&self, capability: ProviderCapability<'_>) -> bool {
297            RustCryptoProvider.supports(capability)
298        }
299        fn fill_random(&self, _: &mut [u8]) -> Result<(), ProviderError> {
300            Err(ProviderError::Random("test entropy failure".into()))
301        }
302        fn encapsulate_key(
303            &self,
304            key: &dyn KeyEncapsulationKey,
305        ) -> Result<EncapsulatedKey, ProviderError> {
306            key.encapsulate_with_provider(self)
307        }
308        fn digest(
309            &self,
310            _: crate::xmldsig::DigestAlgorithm,
311            _: &[u8],
312        ) -> Result<Vec<u8>, ProviderError> {
313            unreachable!("no digest after failed entropy")
314        }
315        fn sign(
316            &self,
317            _: &dyn crate::xmldsig::SigningKey,
318            _: crate::xmldsig::SignatureAlgorithm,
319            _: &[u8],
320        ) -> Result<Vec<u8>, crate::xmldsig::SigningKeyError> {
321            unreachable!("no signing after failed entropy")
322        }
323        fn verify(
324            &self,
325            _: &dyn crate::xmldsig::VerifyingKey,
326            _: crate::xmldsig::SignatureAlgorithm,
327            _: &[u8],
328            _: &[u8],
329        ) -> Result<bool, crate::xmldsig::DsigError> {
330            unreachable!("no verification in encapsulation")
331        }
332        fn derive_key(
333            &self,
334            _: &crate::provider::KdfParameters<'_>,
335            _: &[u8],
336        ) -> Result<Vec<u8>, ProviderError> {
337            unreachable!("KEM does not use a KDF")
338        }
339        #[cfg(feature = "xmlenc")]
340        fn encrypt_data(
341            &self,
342            _: crate::xmlenc::DataEncryptionAlgorithm,
343            _: &[u8],
344            _: &[u8],
345        ) -> Result<Vec<u8>, ProviderError> {
346            unreachable!("no encryption after failed entropy")
347        }
348        #[cfg(feature = "xmlenc")]
349        fn decrypt_data(
350            &self,
351            _: crate::xmlenc::DataEncryptionAlgorithm,
352            _: &[u8],
353            _: &[u8],
354        ) -> Result<Vec<u8>, ProviderError> {
355            unreachable!("no decryption in encapsulation")
356        }
357        #[cfg(feature = "xmlenc")]
358        fn wrap_key(
359            &self,
360            _: crate::xmlenc::KeyWrapAlgorithm,
361            _: &[u8],
362            _: &[u8],
363        ) -> Result<Vec<u8>, ProviderError> {
364            unreachable!("no wrapping after failed entropy")
365        }
366        #[cfg(feature = "xmlenc")]
367        fn unwrap_key(
368            &self,
369            _: crate::xmlenc::KeyWrapAlgorithm,
370            _: &[u8],
371            _: &[u8],
372        ) -> Result<Vec<u8>, ProviderError> {
373            unreachable!("no unwrapping in encapsulation")
374        }
375        #[cfg(feature = "xmlenc")]
376        fn transport_key(
377            &self,
378            _: &dyn crate::provider::KeyTransportKey,
379            _: &crate::xmlenc::RsaOaepParameters,
380            _: &[u8],
381        ) -> Result<Vec<u8>, ProviderError> {
382            unreachable!("no RSA in encapsulation")
383        }
384        #[cfg(feature = "xmlenc")]
385        fn recover_key(
386            &self,
387            _: &dyn crate::provider::KeyRecoveryKey,
388            _: &crate::xmlenc::RsaOaepParameters,
389            _: &[u8],
390        ) -> Result<Vec<u8>, ProviderError> {
391            unreachable!("no RSA in encapsulation")
392        }
393    }
394
395    #[cfg(feature = "experimental-pq")]
396    #[test]
397    fn entropy_failure_is_terminal_and_charged() {
398        // Actual RustCrypto key execution must use this provider's RNG, and
399        // failure must not release the reservation or fall back to OS entropy.
400        let algorithm = KeyEncapsulationAlgorithm::MlKem512;
401        let key = crate::provider::RustCryptoMlKemPrivateKey::from_seed(algorithm, &[1; 64])
402            .expect("deterministic test recipient")
403            .public_key();
404        let policy = KeyEstablishmentPolicy {
405            encapsulation_algorithms: [algorithm].into(),
406            max_encapsulation_operations: 1,
407            ..KeyEstablishmentPolicy::default()
408        };
409        let mut usage = KeyEstablishmentUsage::default();
410        assert!(matches!(
411            usage.encapsulate(&policy, &FailedRandom, &key),
412            Err(KeyEstablishmentError::Provider(ProviderError::Random(_)))
413        ));
414        assert_eq!(usage.encapsulation_operations, 1);
415        assert!(matches!(
416            usage.encapsulate(&policy, &RustCryptoProvider, &key),
417            Err(KeyEstablishmentError::Policy(
418                PolicyViolation::ResourceLimit { .. }
419            ))
420        ));
421        assert!(
422            crate::provider::RustCryptoMlKemPrivateKey::generate(&FailedRandom, algorithm).is_err()
423        );
424    }
425
426    #[test]
427    fn maximum_ciphertext_decodes_without_estimate_workspace() {
428        use base64::Engine as _;
429        // The final padded quad needs two bytes, not the three in the
430        // conservative length estimate. Exact-capacity decoding must remain
431        // valid; a 1569-byte ciphertext with the same encoded length must fail.
432        let mechanism = |bytes: &[u8]| {
433            format!(
434                "<e:EncapsulationMechanism xmlns:e=\"{ENCAPSULATION_NS}\" xmlns:d=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:x=\"http://www.w3.org/2001/04/xmlenc#\" Algorithm=\"{}\"><d:KeyInfo/><x:CipherData><x:CipherValue>{}</x:CipherValue></x:CipherData></e:EncapsulationMechanism>",
435                KeyEncapsulationAlgorithm::MlKem1024.uri(),
436                base64::engine::general_purpose::STANDARD.encode(bytes)
437            )
438        };
439        let bytes = [7; 1568];
440        let xml = mechanism(&bytes);
441        let document = crate::Document::parse(&xml).expect("maximum ciphertext XML");
442        let parsed = parse_encapsulation(document.root_element(), false).expect("exact capacity");
443        assert_eq!(parsed.ciphertext(), bytes);
444        let spaced = xml.replace("BwcH", "Bw\n cH");
445        let document = crate::Document::parse(&spaced).expect("whitespace ciphertext XML");
446        assert_eq!(
447            parse_encapsulation(document.root_element(), false)
448                .expect("whitespace does not consume ciphertext capacity")
449                .ciphertext(),
450            bytes
451        );
452        let oversized = mechanism(&[7; 1569]);
453        let document = crate::Document::parse(&oversized).expect("oversized ciphertext XML");
454        assert!(parse_encapsulation(document.root_element(), false).is_err());
455    }
456
457    #[test]
458    fn mechanism_grammar_rejects_ambiguous_and_malformed_input() {
459        // The extension requires exactly ordered KeyInfo/CipherData; a
460        // template may omit ciphertext, but a decryption input may not.
461        let valid = format!(
462            "<e:EncapsulationMechanism xmlns:e=\"{ENCAPSULATION_NS}\" xmlns:d=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:x=\"http://www.w3.org/2001/04/xmlenc#\" Algorithm=\"{}\"><d:KeyInfo/><x:CipherData><x:CipherValue/></x:CipherData></e:EncapsulationMechanism>",
463            KeyEncapsulationAlgorithm::MlKem512.uri()
464        );
465        let document = crate::Document::parse(&valid).expect("template XML");
466        assert!(parse_encapsulation(document.root_element(), true).is_ok());
467        assert!(parse_encapsulation(document.root_element(), false).is_err());
468        for invalid in [
469            valid.replace("<d:KeyInfo/>", "<d:KeyInfo/><d:KeyInfo/>"),
470            valid.replace("<d:KeyInfo/>", "unexpected<d:KeyInfo/>"),
471            valid.replace(
472                "<x:CipherValue/>",
473                "<x:CipherValue><d:KeyInfo/></x:CipherValue>",
474            ),
475            valid.replace("<x:CipherValue/>", "<x:CipherValue>AA==</x:CipherValue>"),
476            valid.replace("<x:CipherValue/>", "<x:CipherValue>!</x:CipherValue>"),
477            valid.replace("#ml-kem-512", "#ml-kem-512 "),
478        ] {
479            let document = crate::Document::parse(&invalid).expect("well-formed invalid mechanism");
480            assert!(parse_encapsulation(document.root_element(), true).is_err());
481        }
482    }
483
484    struct NeverCalled;
485    impl KeyDecapsulationKey for NeverCalled {
486        fn algorithm(&self) -> KeyEncapsulationAlgorithm {
487            KeyEncapsulationAlgorithm::MlKem768
488        }
489        fn decapsulate(&self, _: &[u8]) -> Result<Zeroizing<[u8; 32]>, ProviderError> {
490            panic!("preflight must reject before private key work")
491        }
492    }
493
494    #[test]
495    fn permission_and_framing_precede_private_key_work() {
496        // Compiled capability cannot enable an experimental algorithm by itself.
497        let algorithm = KeyEncapsulationAlgorithm::MlKem768;
498        let mut usage = KeyEstablishmentUsage::default();
499        let mut policy = KeyEstablishmentPolicy::default();
500        assert!(matches!(
501            usage.decapsulate(&policy, &RustCryptoProvider, &NeverCalled, algorithm, &[]),
502            Err(KeyEstablishmentError::Policy(
503                PolicyViolation::Algorithm { .. }
504            ))
505        ));
506        policy.encapsulation_algorithms.insert(algorithm);
507        assert!(matches!(
508            usage.decapsulate(&policy, &RustCryptoProvider, &NeverCalled, algorithm, &[]),
509            Err(KeyEstablishmentError::Provider(
510                ProviderError::InvalidInput(_)
511            ))
512        ));
513    }
514
515    #[test]
516    fn failed_attempts_cannot_reset_shared_allowance() {
517        // KEM and KDF reservations consume the same bytes; failed reservation
518        // leaves all counters unchanged, while completed reservations persist.
519        let mut usage = KeyEstablishmentUsage::default();
520        let mut policy = KeyEstablishmentPolicy {
521            max_encapsulation_operations: 1,
522            ..KeyEstablishmentPolicy::default()
523        };
524        usage
525            .reserve_encapsulation(&policy, 768)
526            .expect("first attempt fits");
527        let bytes = usage.owned_bytes;
528        assert!(usage.reserve_encapsulation(&policy, 768).is_err());
529        assert_eq!(usage.owned_bytes, bytes);
530        policy.max_owned_bytes = bytes;
531        assert!(usage.commit_all(&policy, 0, 0, 1).is_err());
532        assert_eq!(usage.owned_bytes, bytes);
533    }
534}