1use crate::policy::{KeyEstablishmentPolicy, PolicyViolation};
7use crate::provider::{
8 CryptoProvider, EncapsulatedKey, KeyDecapsulationKey, KeyEncapsulationAlgorithm,
9 KeyEncapsulationKey, ProviderCapability, ProviderError, ProviderInputError,
10};
11use zeroize::Zeroizing;
12
13#[derive(Debug, thiserror::Error)]
15pub enum KeyEstablishmentError {
16 #[error("invalid EncapsulationMechanism: {0}")]
18 Structure(&'static str),
19 #[error(transparent)]
21 Policy(#[from] PolicyViolation),
22 #[error(transparent)]
24 Provider(#[from] ProviderError),
25}
26
27pub const ENCAPSULATION_NS: &str = "http://www.aleksey.com/xmlsec/2025/12/xmldsig-more#";
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct EncapsulationMechanism {
33 pub algorithm: KeyEncapsulationAlgorithm,
35 pub key_info: crate::xmldsig::KeyInfo,
37 pub ciphertext: Vec<u8>,
39}
40
41pub struct ParsedEncapsulation<'a, 'input> {
44 pub algorithm: KeyEncapsulationAlgorithm,
46 pub key_info: crate::Node<'a, 'input>,
48 pub cipher_value: crate::Node<'a, 'input>,
50 ciphertext: [u8; 1568],
51 ciphertext_len: usize,
52}
53
54impl ParsedEncapsulation<'_, '_> {
55 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
66pub 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 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 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
145pub 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#[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 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 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 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 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 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 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 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 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}