1#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
8use std::collections::HashSet;
9#[cfg(feature = "xmldsig")]
10use std::time::SystemTime;
11
12#[cfg(feature = "xmldsig")]
13use crate::xmldsig::{DigestAlgorithm, SignatureAlgorithm, UriTypeSet, XPathHereSemantics};
14#[cfg(feature = "xmlenc")]
15use crate::xmlenc::{
16 DataEncryptionAlgorithm, KeyTransportAlgorithm, KeyWrapAlgorithm, OaepDigestAlgorithm,
17};
18
19pub(crate) mod resource_name {
24 pub const XML_NODES: &str = "XML nodes";
25 pub const SIGNATURE_REFERENCES: &str = "signature references";
26 pub const REFERENCE_TRANSFORMS: &str = "reference transforms";
27 pub const XML_BASE_COMPONENTS: &str = "XML Base components";
28 pub const XML_BASE_RESOLUTION_BYTES: &str = "XML Base resolution bytes";
29 pub const CANONICALIZED_BYTES: &str = "canonicalized bytes";
30 pub const EXTERNAL_RESOURCE_BYTES: &str = "external resource bytes";
31 pub const AGGREGATE_EXTERNAL_RESOURCE_BYTES: &str = "aggregate external resource bytes";
32 pub const ENCRYPTION_PLAINTEXT_BYTES: &str = "encryption plaintext bytes";
33 #[cfg(feature = "xmlenc")]
34 pub const AGGREGATE_ENCRYPTION_CIPHER_VALUE_BYTES: &str =
35 "aggregate encryption CipherValue bytes";
36 pub const XML_DOCUMENT: &str = "XML document";
37 pub const ENCRYPTION_RECIPIENTS: &str = "encryption recipients";
38 pub const ENCRYPTION_METADATA_BYTES: &str = "encryption metadata bytes";
39 pub const KEY_CANDIDATES: &str = "key candidates";
40 pub const BASE64_TRANSFORM_INPUT_BYTES: &str = "Base64 transform input bytes";
41 pub const BASE64_TRANSFORM_OUTPUT_BYTES: &str = "Base64 transform output bytes";
42 pub const XPATH_EXPRESSIONS: &str = "XPath expressions";
43 pub const XPATH_EXPRESSION_BYTES: &str = "XPath expression bytes";
44 pub const XPATH_EXPRESSION_COMPLEXITY: &str = "XPath expression complexity";
45 pub const XPATH_CONTEXT_EVALUATIONS: &str = "XPath context evaluations";
46 pub const XPATH_EVALUATION_WORK: &str = "XPath evaluation work";
47 pub const XPATH_MIRROR_STRING_BYTES: &str = "XPath mirror string bytes";
48 pub const XPATH_STRING_WORK_BYTES: &str = "XPath string-processing work bytes";
49 pub const XPATH_NAMESPACE_BINDINGS: &str = "XPath namespace bindings";
50 pub const XPATH_NAMESPACE_BYTES: &str = "XPath namespace bytes";
51 pub const XPATH_FILTERS: &str = "XPath filters";
52 pub const NODE_SET_FILTER_WORK: &str = "node-set filter work";
53 pub const NODE_SET_ENTRIES: &str = "node-set entries";
54 pub const NODE_SET_OWNED_STRING_BYTES: &str = "node-set owned string bytes";
55 pub const NODE_SET_CUMULATIVE_OWNED_STRING_BYTES: &str =
56 "cumulative node-set owned string bytes";
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
61#[non_exhaustive]
62pub enum PolicyViolation {
63 #[error("{operation} policy rejects algorithm {algorithm}")]
65 Algorithm {
66 operation: &'static str,
68 algorithm: String,
70 },
71 #[error("{resource} exceeds policy maximum {maximum}: got {actual}")]
73 ResourceLimit {
74 resource: &'static str,
76 maximum: usize,
78 actual: usize,
80 },
81 #[error("{resource} has invalid policy limit {actual}: {requirement}")]
83 InvalidResourceLimit {
84 resource: &'static str,
86 requirement: &'static str,
88 actual: usize,
90 },
91 #[error("key/trust policy rejected the operation: {reason}")]
93 KeyTrust {
94 reason: &'static str,
96 },
97 #[error("XML input policy rejected the operation: {reason}")]
99 XmlInput {
100 reason: &'static str,
102 },
103 #[error("{operation} URI policy rejected the operation: {reason}")]
105 Uri {
106 operation: &'static str,
108 reason: &'static str,
110 },
111 #[error(
113 "{operation} policy requires {key_type} keys between {minimum_bits} and {maximum_bits} bits: got {actual_bits}"
114 )]
115 KeySize {
116 operation: &'static str,
118 key_type: &'static str,
120 minimum_bits: usize,
122 maximum_bits: usize,
124 actual_bits: usize,
126 },
127 #[error("{operation} policy rejects invalid {key_type} key material: {reason}")]
129 InvalidKeyMaterial {
130 operation: &'static str,
132 key_type: &'static str,
134 reason: &'static str,
136 },
137}
138
139#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub struct RsaKeyPolicy {
143 pub minimum_modulus_bits: usize,
145}
146
147#[cfg(feature = "xmldsig")]
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub struct DsaKeyPolicy {
151 pub minimum_modulus_bits: usize,
153}
154
155#[cfg(feature = "xmldsig")]
156impl Default for DsaKeyPolicy {
157 fn default() -> Self {
158 Self {
159 minimum_modulus_bits: 2048,
160 }
161 }
162}
163
164#[cfg(feature = "xmldsig")]
165impl DsaKeyPolicy {
166 pub fn validate(&self) -> Result<(), PolicyViolation> {
168 if self.minimum_modulus_bits == 0 || !self.minimum_modulus_bits.is_multiple_of(64) {
169 return Err(PolicyViolation::InvalidResourceLimit {
170 resource: "minimum DSA modulus bits",
171 requirement: "minimum must be a nonzero multiple of 64 bits",
172 actual: self.minimum_modulus_bits,
173 });
174 }
175 ResourcePolicy::within(
176 "minimum DSA modulus bits",
177 self.minimum_modulus_bits,
178 crate::hard_limits::DSA_MODULUS_BIT_CEILING,
179 )
180 }
181
182 pub(crate) fn validate_modulus_bits(&self, actual_bits: usize) -> Result<(), PolicyViolation> {
183 self.validate()?;
184 if !(self.minimum_modulus_bits..=crate::hard_limits::DSA_MODULUS_BIT_CEILING)
185 .contains(&actual_bits)
186 {
187 return Err(PolicyViolation::KeySize {
188 operation: "verification",
189 key_type: "DSA",
190 minimum_bits: self.minimum_modulus_bits,
191 maximum_bits: crate::hard_limits::DSA_MODULUS_BIT_CEILING,
192 actual_bits,
193 });
194 }
195 Ok(())
196 }
197}
198
199#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
200impl Default for RsaKeyPolicy {
201 fn default() -> Self {
202 Self {
203 minimum_modulus_bits: 2048,
204 }
205 }
206}
207
208#[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
209impl RsaKeyPolicy {
210 pub fn validate(&self) -> Result<(), PolicyViolation> {
212 if self.minimum_modulus_bits == 0 || !self.minimum_modulus_bits.is_multiple_of(8) {
213 return Err(PolicyViolation::InvalidResourceLimit {
214 resource: "minimum RSA modulus bits",
215 requirement: "minimum must be a nonzero whole-byte width",
216 actual: self.minimum_modulus_bits,
217 });
218 }
219 ResourcePolicy::within(
220 "minimum RSA modulus bits",
221 self.minimum_modulus_bits,
222 crate::hard_limits::RSA_MODULUS_BIT_CEILING,
223 )
224 }
225
226 pub(crate) fn validate_components(
227 &self,
228 operation: &'static str,
229 modulus: &[u8],
230 exponent: &[u8],
231 ) -> Result<usize, PolicyViolation> {
232 self.validate()?;
233 let modulus = modulus
234 .iter()
235 .position(|byte| *byte != 0)
236 .map(|start| &modulus[start..])
237 .ok_or(PolicyViolation::InvalidKeyMaterial {
238 operation,
239 key_type: "RSA",
240 reason: "modulus is zero",
241 })?;
242 let modulus_bits = modulus
243 .len()
244 .checked_mul(8)
245 .and_then(|width| width.checked_sub(modulus[0].leading_zeros() as usize))
246 .ok_or(PolicyViolation::InvalidKeyMaterial {
247 operation,
248 key_type: "RSA",
249 reason: "modulus width overflows",
250 })?;
251 if !(self.minimum_modulus_bits..=crate::hard_limits::RSA_MODULUS_BIT_CEILING)
252 .contains(&modulus_bits)
253 {
254 return Err(PolicyViolation::KeySize {
255 operation,
256 key_type: "RSA",
257 minimum_bits: self.minimum_modulus_bits,
258 maximum_bits: crate::hard_limits::RSA_MODULUS_BIT_CEILING,
259 actual_bits: modulus_bits,
260 });
261 }
262 if exponent.is_empty() || exponent.len() > 8 {
263 return Err(PolicyViolation::InvalidKeyMaterial {
264 operation,
265 key_type: "RSA",
266 reason: "public exponent has invalid encoding",
267 });
268 }
269 let mut exponent_bytes = [0_u8; 8];
270 exponent_bytes[8 - exponent.len()..].copy_from_slice(exponent);
271 let exponent = u64::from_be_bytes(exponent_bytes);
272 if !(3..=((1_u64 << 33) - 1)).contains(&exponent) || exponent % 2 == 0 {
273 return Err(PolicyViolation::InvalidKeyMaterial {
274 operation,
275 key_type: "RSA",
276 reason: "public exponent is outside the supported odd range",
277 });
278 }
279 Ok(modulus.len())
280 }
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct ResourcePolicy {
286 pub max_xml_nodes: usize,
288 pub max_references: usize,
290 pub max_transforms_per_reference: usize,
292 pub max_xml_base_components: usize,
294 pub max_xml_base_resolution_bytes: usize,
296 pub max_canonicalized_bytes: usize,
298 pub max_external_resource_bytes: usize,
300 pub max_external_resource_total_bytes: usize,
302 pub max_encryption_plaintext_bytes: usize,
304 pub max_xml_document_bytes: usize,
306 pub max_encryption_recipients: usize,
308 pub max_encryption_metadata_bytes: usize,
310 pub max_key_candidates: usize,
312 pub max_base64_transform_input_bytes: usize,
314 pub max_base64_transform_output_bytes: usize,
316 pub max_xpath_expressions: usize,
318 pub max_xpath_expression_bytes: usize,
320 pub max_xpath_expression_complexity: usize,
322 pub max_xpath_context_evaluations: usize,
324 pub max_xpath_evaluation_work: usize,
326 pub max_xpath_mirror_string_bytes: usize,
328 pub max_xpath_string_work_bytes: usize,
330 pub max_xpath_namespace_bindings: usize,
332 pub max_xpath_namespace_bytes: usize,
334 pub max_xpath_filters: usize,
336 pub max_node_set_filter_work: usize,
338 pub max_node_set_entries: usize,
340 pub max_node_set_owned_string_bytes: usize,
342 pub max_node_set_cumulative_owned_string_bytes: usize,
344}
345
346impl Default for ResourcePolicy {
347 fn default() -> Self {
348 Self {
349 max_xml_nodes: crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize,
350 max_references: crate::hard_limits::SIGNATURE_REFERENCE_CEILING,
351 max_transforms_per_reference: crate::hard_limits::REFERENCE_TRANSFORM_CEILING,
352 max_xml_base_components: crate::hard_limits::XML_BASE_COMPONENT_CEILING,
353 max_xml_base_resolution_bytes: crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING,
354 max_canonicalized_bytes: crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
355 max_external_resource_bytes: crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING,
356 max_external_resource_total_bytes:
357 crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING,
358 max_encryption_plaintext_bytes: crate::hard_limits::ENCRYPTION_PLAINTEXT_BYTE_CEILING,
359 max_xml_document_bytes: crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
360 max_encryption_recipients: crate::hard_limits::ENCRYPTION_RECIPIENT_CEILING,
361 max_encryption_metadata_bytes: crate::hard_limits::ENCRYPTION_METADATA_BYTE_CEILING,
362 max_key_candidates: crate::hard_limits::KEY_CANDIDATE_CEILING,
363 max_base64_transform_input_bytes:
364 crate::hard_limits::BASE64_TRANSFORM_INPUT_BYTE_CEILING,
365 max_base64_transform_output_bytes:
366 crate::hard_limits::BASE64_TRANSFORM_OUTPUT_BYTE_CEILING,
367 max_xpath_expressions: crate::hard_limits::XPATH_EXPRESSION_COUNT_CEILING,
368 max_xpath_expression_bytes: crate::hard_limits::XPATH_EXPRESSION_BYTE_CEILING,
369 max_xpath_expression_complexity:
370 crate::hard_limits::XPATH_EXPRESSION_COMPLEXITY_CEILING,
371 max_xpath_context_evaluations: crate::hard_limits::XPATH_CONTEXT_EVALUATION_CEILING,
372 max_xpath_evaluation_work: crate::hard_limits::XPATH_EVALUATION_WORK_CEILING,
373 max_xpath_mirror_string_bytes: crate::hard_limits::XPATH_MIRROR_STRING_BYTE_CEILING,
374 max_xpath_string_work_bytes: crate::hard_limits::XPATH_STRING_WORK_BYTE_CEILING,
375 max_xpath_namespace_bindings: crate::hard_limits::XPATH_NAMESPACE_BINDING_CEILING,
376 max_xpath_namespace_bytes: crate::hard_limits::XPATH_NAMESPACE_BYTE_CEILING,
377 max_xpath_filters: crate::hard_limits::XPATH_FILTER_COUNT_CEILING,
378 max_node_set_filter_work: crate::hard_limits::NODE_SET_FILTER_WORK_CEILING,
379 max_node_set_entries: crate::hard_limits::NODE_SET_ENTRY_CEILING,
380 max_node_set_owned_string_bytes: crate::hard_limits::NODE_SET_OWNED_STRING_BYTE_CEILING,
381 max_node_set_cumulative_owned_string_bytes:
382 crate::hard_limits::NODE_SET_CUMULATIVE_OWNED_STRING_BYTE_CEILING,
383 }
384 }
385}
386
387impl ResourcePolicy {
388 pub fn validate(&self) -> Result<(), PolicyViolation> {
390 Self::within(
391 resource_name::XML_NODES,
392 self.max_xml_nodes,
393 crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize,
394 )?;
395 Self::within(
396 resource_name::CANONICALIZED_BYTES,
397 self.max_canonicalized_bytes,
398 crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
399 )?;
400 Self::within(
401 resource_name::SIGNATURE_REFERENCES,
402 self.max_references,
403 crate::hard_limits::SIGNATURE_REFERENCE_CEILING,
404 )?;
405 Self::within(
406 resource_name::REFERENCE_TRANSFORMS,
407 self.max_transforms_per_reference,
408 crate::hard_limits::REFERENCE_TRANSFORM_CEILING,
409 )?;
410 Self::within(
411 resource_name::XML_BASE_COMPONENTS,
412 self.max_xml_base_components,
413 crate::hard_limits::XML_BASE_COMPONENT_CEILING,
414 )?;
415 Self::within(
416 resource_name::XML_BASE_RESOLUTION_BYTES,
417 self.max_xml_base_resolution_bytes,
418 crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING,
419 )?;
420 Self::within(
421 resource_name::XML_DOCUMENT,
422 self.max_xml_document_bytes,
423 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
424 )?;
425 Self::within(
426 resource_name::EXTERNAL_RESOURCE_BYTES,
427 self.max_external_resource_bytes,
428 crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING,
429 )?;
430 Self::within(
431 resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
432 self.max_external_resource_total_bytes,
433 crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING,
434 )?;
435 Self::within(
436 resource_name::ENCRYPTION_PLAINTEXT_BYTES,
437 self.max_encryption_plaintext_bytes,
438 crate::hard_limits::ENCRYPTION_PLAINTEXT_BYTE_CEILING,
439 )?;
440 Self::within(
441 resource_name::ENCRYPTION_RECIPIENTS,
442 self.max_encryption_recipients,
443 crate::hard_limits::ENCRYPTION_RECIPIENT_CEILING,
444 )?;
445 Self::within(
446 resource_name::ENCRYPTION_METADATA_BYTES,
447 self.max_encryption_metadata_bytes,
448 crate::hard_limits::ENCRYPTION_METADATA_BYTE_CEILING,
449 )?;
450 for (resource, selected, ceiling) in [
451 (
452 resource_name::KEY_CANDIDATES,
453 self.max_key_candidates,
454 crate::hard_limits::KEY_CANDIDATE_CEILING,
455 ),
456 (
457 resource_name::BASE64_TRANSFORM_INPUT_BYTES,
458 self.max_base64_transform_input_bytes,
459 crate::hard_limits::BASE64_TRANSFORM_INPUT_BYTE_CEILING,
460 ),
461 (
462 resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
463 self.max_base64_transform_output_bytes,
464 crate::hard_limits::BASE64_TRANSFORM_OUTPUT_BYTE_CEILING,
465 ),
466 (
467 resource_name::XPATH_EXPRESSIONS,
468 self.max_xpath_expressions,
469 crate::hard_limits::XPATH_EXPRESSION_COUNT_CEILING,
470 ),
471 (
472 resource_name::XPATH_EXPRESSION_BYTES,
473 self.max_xpath_expression_bytes,
474 crate::hard_limits::XPATH_EXPRESSION_BYTE_CEILING,
475 ),
476 (
477 resource_name::XPATH_EXPRESSION_COMPLEXITY,
478 self.max_xpath_expression_complexity,
479 crate::hard_limits::XPATH_EXPRESSION_COMPLEXITY_CEILING,
480 ),
481 (
482 resource_name::XPATH_CONTEXT_EVALUATIONS,
483 self.max_xpath_context_evaluations,
484 crate::hard_limits::XPATH_CONTEXT_EVALUATION_CEILING,
485 ),
486 (
487 resource_name::XPATH_EVALUATION_WORK,
488 self.max_xpath_evaluation_work,
489 crate::hard_limits::XPATH_EVALUATION_WORK_CEILING,
490 ),
491 (
492 resource_name::XPATH_MIRROR_STRING_BYTES,
493 self.max_xpath_mirror_string_bytes,
494 crate::hard_limits::XPATH_MIRROR_STRING_BYTE_CEILING,
495 ),
496 (
497 resource_name::XPATH_STRING_WORK_BYTES,
498 self.max_xpath_string_work_bytes,
499 crate::hard_limits::XPATH_STRING_WORK_BYTE_CEILING,
500 ),
501 (
502 resource_name::XPATH_NAMESPACE_BINDINGS,
503 self.max_xpath_namespace_bindings,
504 crate::hard_limits::XPATH_NAMESPACE_BINDING_CEILING,
505 ),
506 (
507 resource_name::XPATH_NAMESPACE_BYTES,
508 self.max_xpath_namespace_bytes,
509 crate::hard_limits::XPATH_NAMESPACE_BYTE_CEILING,
510 ),
511 (
512 resource_name::XPATH_FILTERS,
513 self.max_xpath_filters,
514 crate::hard_limits::XPATH_FILTER_COUNT_CEILING,
515 ),
516 (
517 resource_name::NODE_SET_FILTER_WORK,
518 self.max_node_set_filter_work,
519 crate::hard_limits::NODE_SET_FILTER_WORK_CEILING,
520 ),
521 (
522 resource_name::NODE_SET_ENTRIES,
523 self.max_node_set_entries,
524 crate::hard_limits::NODE_SET_ENTRY_CEILING,
525 ),
526 (
527 resource_name::NODE_SET_OWNED_STRING_BYTES,
528 self.max_node_set_owned_string_bytes,
529 crate::hard_limits::NODE_SET_OWNED_STRING_BYTE_CEILING,
530 ),
531 (
532 resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
533 self.max_node_set_cumulative_owned_string_bytes,
534 crate::hard_limits::NODE_SET_CUMULATIVE_OWNED_STRING_BYTE_CEILING,
535 ),
536 ] {
537 Self::within(resource, selected, ceiling)?;
538 }
539 Ok(())
540 }
541
542 pub(crate) fn validate_xml_document_len(&self, actual: usize) -> Result<(), PolicyViolation> {
543 if actual > self.max_xml_document_bytes {
544 return Err(PolicyViolation::ResourceLimit {
545 resource: resource_name::XML_DOCUMENT,
546 maximum: self.max_xml_document_bytes,
547 actual,
548 });
549 }
550 Ok(())
551 }
552
553 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
554 pub(crate) fn validate_key_candidates(&self, actual: usize) -> Result<(), PolicyViolation> {
555 if actual > self.max_key_candidates {
556 return Err(PolicyViolation::ResourceLimit {
557 resource: resource_name::KEY_CANDIDATES,
558 maximum: self.max_key_candidates,
559 actual,
560 });
561 }
562 Ok(())
563 }
564
565 pub(crate) fn effective_xml_nodes(&self) -> u32 {
566 u32::try_from(self.max_xml_nodes)
567 .unwrap_or(crate::hard_limits::XML_DOCUMENT_NODE_CEILING)
568 .min(crate::hard_limits::XML_DOCUMENT_NODE_CEILING)
569 }
570
571 #[cfg(feature = "xmldsig")]
572 pub(crate) fn effective_canonicalized_bytes(&self) -> usize {
573 self.max_canonicalized_bytes
574 .min(crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING)
575 }
576
577 #[cfg(feature = "xmldsig")]
578 pub(crate) fn effective_xml_base_components(&self) -> usize {
579 self.max_xml_base_components
580 .min(crate::hard_limits::XML_BASE_COMPONENT_CEILING)
581 }
582
583 #[cfg(feature = "xmldsig")]
584 pub(crate) fn effective_xml_base_resolution_bytes(&self) -> usize {
585 self.max_xml_base_resolution_bytes
586 .min(crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING)
587 }
588
589 fn within(
590 resource: &'static str,
591 selected: usize,
592 ceiling: usize,
593 ) -> Result<(), PolicyViolation> {
594 if selected > ceiling {
595 return Err(PolicyViolation::ResourceLimit {
596 resource,
597 maximum: ceiling,
598 actual: selected,
599 });
600 }
601 Ok(())
602 }
603
604 #[cfg(feature = "xmldsig")]
605 fn nonzero_within(
606 resource: &'static str,
607 selected: usize,
608 ceiling: usize,
609 ) -> Result<(), PolicyViolation> {
610 if selected == 0 {
611 return Err(PolicyViolation::InvalidResourceLimit {
612 resource,
613 requirement: "limit must be nonzero",
614 actual: selected,
615 });
616 }
617 Self::within(resource, selected, ceiling)
618 }
619}
620
621#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
623pub struct XmlInputPolicy {
624 pub allow_internal_dtd: bool,
626}
627
628#[cfg(feature = "xmldsig")]
630#[derive(Debug, Clone, Default, PartialEq, Eq)]
631pub struct TransformPolicy {
632 pub allowed_algorithms: Option<HashSet<String>>,
634 pub xpath_here_semantics: XPathHereSemantics,
636}
637
638#[cfg(feature = "xmldsig")]
640#[derive(Debug, Clone, Copy, PartialEq, Eq)]
641pub struct UriPolicy {
642 pub references: UriTypeSet,
644 pub retrieval_methods: UriTypeSet,
646}
647
648#[cfg(feature = "xmldsig")]
649impl Default for UriPolicy {
650 fn default() -> Self {
651 Self {
652 references: UriTypeSet::SAME_DOCUMENT,
653 retrieval_methods: UriTypeSet::SAME_DOCUMENT,
654 }
655 }
656}
657
658#[cfg(feature = "xmldsig")]
660#[derive(Debug, Clone, Copy, PartialEq, Eq)]
661pub struct KeySourcePolicy {
662 pub preset_key: bool,
664 pub key_name: bool,
666 pub key_value: bool,
668 pub der_encoded_key_value: bool,
670 pub x509_data: bool,
672}
673
674#[cfg(feature = "xmldsig")]
675impl Default for KeySourcePolicy {
676 fn default() -> Self {
677 Self {
678 preset_key: true,
679 key_name: true,
680 key_value: true,
681 der_encoded_key_value: true,
682 x509_data: true,
683 }
684 }
685}
686
687#[cfg(feature = "xmldsig")]
689#[derive(Debug, Clone, PartialEq, Eq, Hash)]
690#[non_exhaustive]
691pub enum ExtendedKeyPurpose {
692 ServerAuth,
694 ClientAuth,
696 CodeSigning,
698 EmailProtection,
700 TimeStamping,
702 OcspSigning,
704 Other(Vec<u64>),
706}
707
708#[cfg(feature = "xmldsig")]
710#[derive(Debug, Clone, PartialEq, Eq)]
711pub struct KeyTrustPolicy {
712 pub verify_x509_chains: bool,
714 pub max_x509_chain_depth: usize,
716 pub max_x509_candidate_paths: usize,
718 pub allowed_legacy_signature_algorithms: HashSet<SignatureAlgorithm>,
720 pub rsa_keys: RsaKeyPolicy,
722 pub dsa_keys: DsaKeyPolicy,
724 pub allowed_extended_key_usages: HashSet<ExtendedKeyPurpose>,
730 pub check_crls: bool,
733 pub verification_time: Option<SystemTime>,
735}
736
737#[cfg(feature = "xmldsig")]
738impl Default for KeyTrustPolicy {
739 fn default() -> Self {
740 Self {
741 verify_x509_chains: false,
742 max_x509_chain_depth: crate::hard_limits::X509_CHAIN_DEPTH_CEILING,
743 max_x509_candidate_paths: crate::hard_limits::X509_CANDIDATE_PATH_CEILING,
744 allowed_legacy_signature_algorithms: HashSet::new(),
745 rsa_keys: RsaKeyPolicy::default(),
746 dsa_keys: DsaKeyPolicy::default(),
747 allowed_extended_key_usages: HashSet::new(),
748 check_crls: false,
749 verification_time: None,
750 }
751 }
752}
753
754#[cfg(feature = "xmldsig")]
755impl KeyTrustPolicy {
756 pub(crate) fn validate(&self) -> Result<(), PolicyViolation> {
757 if self.check_crls && !self.verify_x509_chains {
758 return Err(PolicyViolation::KeyTrust {
759 reason: "CRL checking requires X.509 chain validation",
760 });
761 }
762 if self
763 .allowed_extended_key_usages
764 .iter()
765 .any(|purpose| match purpose {
766 ExtendedKeyPurpose::Other(arcs) => {
767 arcs.len() < 2 || arcs[0] > 2 || (arcs[0] < 2 && arcs[1] > 39)
768 }
769 _ => false,
770 })
771 {
772 return Err(PolicyViolation::KeyTrust {
773 reason: "custom extended key purposes must contain valid OID arcs",
774 });
775 }
776 self.rsa_keys.validate()?;
777 self.dsa_keys.validate()?;
778 ResourcePolicy::nonzero_within(
779 "X.509 chain depth",
780 self.max_x509_chain_depth,
781 crate::hard_limits::X509_CHAIN_DEPTH_CEILING,
782 )?;
783 ResourcePolicy::nonzero_within(
784 "X.509 candidate paths",
785 self.max_x509_candidate_paths,
786 crate::hard_limits::X509_CANDIDATE_PATH_CEILING,
787 )
788 }
789}
790
791#[cfg(feature = "xmldsig")]
793#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
794pub enum ManifestProcessing {
795 #[default]
797 Ignore,
798 Process,
800}
801
802#[cfg(feature = "xmldsig")]
804#[derive(Debug, Clone, Default)]
805pub struct VerificationPolicy {
806 pub signature_algorithms: Option<HashSet<SignatureAlgorithm>>,
809 pub digest_algorithms: Option<HashSet<DigestAlgorithm>>,
811 pub key_trust: KeyTrustPolicy,
813 pub key_sources: KeySourcePolicy,
815 pub uris: UriPolicy,
817 pub transforms: TransformPolicy,
819 pub manifest_processing: ManifestProcessing,
821 pub xml: XmlInputPolicy,
823 pub resources: ResourcePolicy,
825}
826
827#[cfg(feature = "xmldsig")]
828impl VerificationPolicy {
829 pub fn validate(&self) -> Result<(), PolicyViolation> {
831 self.resources.validate()?;
832 self.key_trust.validate()
833 }
834
835 pub fn check_signature_algorithm(
837 &self,
838 algorithm: SignatureAlgorithm,
839 ) -> Result<(), PolicyViolation> {
840 if matches!(
841 algorithm,
842 SignatureAlgorithm::RsaSha1
843 | SignatureAlgorithm::DsaSha1
844 | SignatureAlgorithm::HmacSha1
845 ) && !self
846 .key_trust
847 .allowed_legacy_signature_algorithms
848 .contains(&algorithm)
849 {
850 return Err(PolicyViolation::Algorithm {
851 operation: "verification",
852 algorithm: algorithm.uri().to_string(),
853 });
854 }
855 if self
856 .signature_algorithms
857 .as_ref()
858 .is_some_and(|allowed| !allowed.contains(&algorithm))
859 {
860 return Err(PolicyViolation::Algorithm {
861 operation: "verification",
862 algorithm: algorithm.uri().to_string(),
863 });
864 }
865 Ok(())
866 }
867}
868
869#[cfg(feature = "xmldsig")]
871#[derive(Debug, Clone, Default)]
872pub struct SigningPolicy {
873 pub signature_algorithms: Option<HashSet<SignatureAlgorithm>>,
875 pub digest_algorithms: Option<HashSet<DigestAlgorithm>>,
877 pub rsa_keys: RsaKeyPolicy,
879 pub uris: UriPolicy,
882 pub transforms: TransformPolicy,
884 pub manifest_processing: ManifestProcessing,
886 pub xml: XmlInputPolicy,
888 pub resources: ResourcePolicy,
890}
891
892#[cfg(feature = "xmldsig")]
893impl SigningPolicy {
894 pub fn validate(&self) -> Result<(), PolicyViolation> {
896 self.resources.validate()?;
897 self.rsa_keys.validate()
898 }
899}
900
901#[cfg(feature = "xmlenc")]
903#[derive(Debug, Clone, Default)]
904pub struct EncryptionPolicy {
905 pub data_algorithms: Option<HashSet<DataEncryptionAlgorithm>>,
907 pub key_transport_algorithms: Option<HashSet<KeyTransportAlgorithm>>,
909 pub key_wrap_algorithms: Option<HashSet<KeyWrapAlgorithm>>,
911 pub oaep_digests: Option<HashSet<OaepDigestAlgorithm>>,
913 pub rsa_keys: RsaKeyPolicy,
915 pub xml: XmlInputPolicy,
917 pub resources: ResourcePolicy,
919}
920
921#[cfg(feature = "xmlenc")]
922impl EncryptionPolicy {
923 pub fn validate(&self) -> Result<(), PolicyViolation> {
925 self.resources.validate()?;
926 self.rsa_keys.validate()
927 }
928}
929
930#[cfg(feature = "xmlenc")]
932#[derive(Debug, Clone, Default)]
933pub struct DecryptionPolicy {
934 pub data_algorithms: Option<HashSet<DataEncryptionAlgorithm>>,
936 pub key_transport_algorithms: Option<HashSet<KeyTransportAlgorithm>>,
938 pub key_wrap_algorithms: Option<HashSet<KeyWrapAlgorithm>>,
940 pub oaep_digests: Option<HashSet<OaepDigestAlgorithm>>,
942 pub xml: XmlInputPolicy,
944 pub resources: ResourcePolicy,
946}
947
948#[cfg(feature = "xmlenc")]
949impl DecryptionPolicy {
950 pub fn validate(&self) -> Result<(), PolicyViolation> {
952 self.resources.validate()
953 }
954}
955
956#[cfg(test)]
957mod tests {
958 use super::*;
959
960 #[test]
961 fn resource_policy_cannot_exceed_implementation_ceiling() {
962 let policy = ResourcePolicy {
963 max_xml_nodes: 100_001,
964 ..ResourcePolicy::default()
965 };
966 assert!(matches!(
967 policy.validate(),
968 Err(PolicyViolation::ResourceLimit {
969 resource: resource_name::XML_NODES,
970 maximum: 100_000,
971 actual: 100_001,
972 })
973 ));
974 }
975
976 #[test]
977 fn every_resource_policy_field_obeys_its_hard_ceiling() {
978 type Case = (&'static str, usize, fn(&mut ResourcePolicy) -> &mut usize);
982 let cases: &[Case] = &[
983 (
984 resource_name::XML_NODES,
985 crate::hard_limits::XML_DOCUMENT_NODE_CEILING as usize,
986 |p| &mut p.max_xml_nodes,
987 ),
988 (
989 resource_name::SIGNATURE_REFERENCES,
990 crate::hard_limits::SIGNATURE_REFERENCE_CEILING,
991 |p| &mut p.max_references,
992 ),
993 (
994 resource_name::REFERENCE_TRANSFORMS,
995 crate::hard_limits::REFERENCE_TRANSFORM_CEILING,
996 |p| &mut p.max_transforms_per_reference,
997 ),
998 (
999 resource_name::XML_BASE_COMPONENTS,
1000 crate::hard_limits::XML_BASE_COMPONENT_CEILING,
1001 |p| &mut p.max_xml_base_components,
1002 ),
1003 (
1004 resource_name::XML_BASE_RESOLUTION_BYTES,
1005 crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING,
1006 |p| &mut p.max_xml_base_resolution_bytes,
1007 ),
1008 (
1009 resource_name::CANONICALIZED_BYTES,
1010 crate::hard_limits::CANONICALIZED_SIGNATURE_DATA_BYTE_CEILING,
1011 |p| &mut p.max_canonicalized_bytes,
1012 ),
1013 (
1014 resource_name::EXTERNAL_RESOURCE_BYTES,
1015 crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING,
1016 |p| &mut p.max_external_resource_bytes,
1017 ),
1018 (
1019 resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
1020 crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING,
1021 |p| &mut p.max_external_resource_total_bytes,
1022 ),
1023 (
1024 resource_name::ENCRYPTION_PLAINTEXT_BYTES,
1025 crate::hard_limits::ENCRYPTION_PLAINTEXT_BYTE_CEILING,
1026 |p| &mut p.max_encryption_plaintext_bytes,
1027 ),
1028 (
1029 resource_name::XML_DOCUMENT,
1030 crate::hard_limits::XML_DOCUMENT_BYTE_CEILING,
1031 |p| &mut p.max_xml_document_bytes,
1032 ),
1033 (
1034 resource_name::ENCRYPTION_RECIPIENTS,
1035 crate::hard_limits::ENCRYPTION_RECIPIENT_CEILING,
1036 |p| &mut p.max_encryption_recipients,
1037 ),
1038 (
1039 resource_name::ENCRYPTION_METADATA_BYTES,
1040 crate::hard_limits::ENCRYPTION_METADATA_BYTE_CEILING,
1041 |p| &mut p.max_encryption_metadata_bytes,
1042 ),
1043 (
1044 resource_name::KEY_CANDIDATES,
1045 crate::hard_limits::KEY_CANDIDATE_CEILING,
1046 |p| &mut p.max_key_candidates,
1047 ),
1048 (
1049 resource_name::BASE64_TRANSFORM_INPUT_BYTES,
1050 crate::hard_limits::BASE64_TRANSFORM_INPUT_BYTE_CEILING,
1051 |p| &mut p.max_base64_transform_input_bytes,
1052 ),
1053 (
1054 resource_name::BASE64_TRANSFORM_OUTPUT_BYTES,
1055 crate::hard_limits::BASE64_TRANSFORM_OUTPUT_BYTE_CEILING,
1056 |p| &mut p.max_base64_transform_output_bytes,
1057 ),
1058 (
1059 resource_name::XPATH_EXPRESSIONS,
1060 crate::hard_limits::XPATH_EXPRESSION_COUNT_CEILING,
1061 |p| &mut p.max_xpath_expressions,
1062 ),
1063 (
1064 resource_name::XPATH_EXPRESSION_BYTES,
1065 crate::hard_limits::XPATH_EXPRESSION_BYTE_CEILING,
1066 |p| &mut p.max_xpath_expression_bytes,
1067 ),
1068 (
1069 resource_name::XPATH_EXPRESSION_COMPLEXITY,
1070 crate::hard_limits::XPATH_EXPRESSION_COMPLEXITY_CEILING,
1071 |p| &mut p.max_xpath_expression_complexity,
1072 ),
1073 (
1074 resource_name::XPATH_CONTEXT_EVALUATIONS,
1075 crate::hard_limits::XPATH_CONTEXT_EVALUATION_CEILING,
1076 |p| &mut p.max_xpath_context_evaluations,
1077 ),
1078 (
1079 resource_name::XPATH_EVALUATION_WORK,
1080 crate::hard_limits::XPATH_EVALUATION_WORK_CEILING,
1081 |p| &mut p.max_xpath_evaluation_work,
1082 ),
1083 (
1084 resource_name::XPATH_MIRROR_STRING_BYTES,
1085 crate::hard_limits::XPATH_MIRROR_STRING_BYTE_CEILING,
1086 |p| &mut p.max_xpath_mirror_string_bytes,
1087 ),
1088 (
1089 resource_name::XPATH_STRING_WORK_BYTES,
1090 crate::hard_limits::XPATH_STRING_WORK_BYTE_CEILING,
1091 |p| &mut p.max_xpath_string_work_bytes,
1092 ),
1093 (
1094 resource_name::XPATH_NAMESPACE_BINDINGS,
1095 crate::hard_limits::XPATH_NAMESPACE_BINDING_CEILING,
1096 |p| &mut p.max_xpath_namespace_bindings,
1097 ),
1098 (
1099 resource_name::XPATH_NAMESPACE_BYTES,
1100 crate::hard_limits::XPATH_NAMESPACE_BYTE_CEILING,
1101 |p| &mut p.max_xpath_namespace_bytes,
1102 ),
1103 (
1104 resource_name::XPATH_FILTERS,
1105 crate::hard_limits::XPATH_FILTER_COUNT_CEILING,
1106 |p| &mut p.max_xpath_filters,
1107 ),
1108 (
1109 resource_name::NODE_SET_FILTER_WORK,
1110 crate::hard_limits::NODE_SET_FILTER_WORK_CEILING,
1111 |p| &mut p.max_node_set_filter_work,
1112 ),
1113 (
1114 resource_name::NODE_SET_ENTRIES,
1115 crate::hard_limits::NODE_SET_ENTRY_CEILING,
1116 |p| &mut p.max_node_set_entries,
1117 ),
1118 (
1119 resource_name::NODE_SET_OWNED_STRING_BYTES,
1120 crate::hard_limits::NODE_SET_OWNED_STRING_BYTE_CEILING,
1121 |p| &mut p.max_node_set_owned_string_bytes,
1122 ),
1123 (
1124 resource_name::NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
1125 crate::hard_limits::NODE_SET_CUMULATIVE_OWNED_STRING_BYTE_CEILING,
1126 |p| &mut p.max_node_set_cumulative_owned_string_bytes,
1127 ),
1128 ];
1129
1130 for &(resource, ceiling, field) in cases {
1131 let mut policy = ResourcePolicy::default();
1132 let actual = ceiling.saturating_add(1);
1133 *field(&mut policy) = actual;
1134 assert_eq!(
1135 policy.validate(),
1136 Err(PolicyViolation::ResourceLimit {
1137 resource,
1138 maximum: ceiling,
1139 actual,
1140 }),
1141 "wrong hard-ceiling validation for {resource}",
1142 );
1143 }
1144 }
1145
1146 #[test]
1147 fn resource_policy_accepts_zero_as_a_deny_all_ceiling() {
1148 let policy = ResourcePolicy {
1151 max_xml_nodes: 0,
1152 max_references: 0,
1153 max_transforms_per_reference: 0,
1154 max_xml_base_components: 0,
1155 max_xml_base_resolution_bytes: 0,
1156 max_canonicalized_bytes: 0,
1157 max_external_resource_bytes: 0,
1158 max_external_resource_total_bytes: 0,
1159 max_encryption_plaintext_bytes: 0,
1160 max_xml_document_bytes: 0,
1161 max_encryption_recipients: 0,
1162 max_encryption_metadata_bytes: 0,
1163 max_key_candidates: 0,
1164 max_base64_transform_input_bytes: 0,
1165 max_base64_transform_output_bytes: 0,
1166 max_xpath_expressions: 0,
1167 max_xpath_expression_bytes: 0,
1168 max_xpath_expression_complexity: 0,
1169 max_xpath_context_evaluations: 0,
1170 max_xpath_evaluation_work: 0,
1171 max_xpath_mirror_string_bytes: 0,
1172 max_xpath_string_work_bytes: 0,
1173 max_xpath_namespace_bindings: 0,
1174 max_xpath_namespace_bytes: 0,
1175 max_xpath_filters: 0,
1176 max_node_set_filter_work: 0,
1177 max_node_set_entries: 0,
1178 max_node_set_owned_string_bytes: 0,
1179 max_node_set_cumulative_owned_string_bytes: 0,
1180 };
1181
1182 assert_eq!(policy.validate(), Ok(()));
1183 }
1184
1185 #[cfg(any(feature = "xmldsig", feature = "xmlenc"))]
1186 #[test]
1187 fn rsa_key_policy_enforces_structure_range_and_explicit_relaxation() {
1188 let secure = RsaKeyPolicy::default();
1189 assert!(matches!(
1190 secure.validate_components("test", &[0x80; 128], &[1, 0, 1]),
1191 Err(PolicyViolation::KeySize {
1192 minimum_bits: 2048,
1193 maximum_bits: 8192,
1194 actual_bits: 1024,
1195 ..
1196 })
1197 ));
1198 let mut short_2048_width = [0_u8; 256];
1199 short_2048_width[0] = 1;
1200 assert!(matches!(
1201 secure.validate_components("test", &short_2048_width, &[1, 0, 1]),
1202 Err(PolicyViolation::KeySize {
1203 minimum_bits: 2048,
1204 actual_bits: 2041,
1205 ..
1206 })
1207 ));
1208 assert!(matches!(
1209 secure.validate_components("test", &[1; 1025], &[1, 0, 1]),
1210 Err(PolicyViolation::KeySize {
1211 actual_bits: 8193,
1212 ..
1213 })
1214 ));
1215 assert!(matches!(
1216 secure.validate_components("test", &[0x80; 256], &[2]),
1217 Err(PolicyViolation::InvalidKeyMaterial { .. })
1218 ));
1219 assert_eq!(
1220 secure.validate_components("test", &[0x80; 256], &[0x80, 0, 0, 1]),
1221 Ok(256),
1222 "normalized RSA components encode the exponent as unsigned bytes"
1223 );
1224
1225 let compatibility = RsaKeyPolicy {
1226 minimum_modulus_bits: 1024,
1227 };
1228 assert_eq!(
1229 compatibility.validate_components("test", &[0x80; 128], &[1, 0, 1]),
1230 Ok(128)
1231 );
1232 assert!(
1233 RsaKeyPolicy {
1234 minimum_modulus_bits: 2047,
1235 }
1236 .validate()
1237 .is_err()
1238 );
1239 }
1240
1241 #[cfg(feature = "xmldsig")]
1242 #[test]
1243 fn mandatory_x509_limits_report_the_nonzero_requirement() {
1244 let policy = KeyTrustPolicy {
1247 max_x509_chain_depth: 0,
1248 ..KeyTrustPolicy::default()
1249 };
1250
1251 let error = policy.validate().expect_err("zero depth must be rejected");
1252 assert!(
1253 error.to_string().contains("must be nonzero"),
1254 "unexpected lower-bound diagnostic: {error}"
1255 );
1256 }
1257
1258 #[cfg(feature = "xmldsig")]
1259 #[test]
1260 fn custom_extended_key_purposes_require_valid_oid_arcs() {
1261 let mut policy = KeyTrustPolicy::default();
1264 policy
1265 .allowed_extended_key_usages
1266 .insert(ExtendedKeyPurpose::Other(vec![1, 40, 7]));
1267
1268 assert!(matches!(
1269 policy.validate(),
1270 Err(PolicyViolation::KeyTrust {
1271 reason: "custom extended key purposes must contain valid OID arcs",
1272 })
1273 ));
1274 }
1275
1276 #[cfg(feature = "xmldsig")]
1277 #[test]
1278 fn crl_checking_requires_x509_chain_validation() {
1279 let policy = KeyTrustPolicy {
1282 check_crls: true,
1283 ..KeyTrustPolicy::default()
1284 };
1285
1286 assert!(matches!(
1287 policy.validate(),
1288 Err(PolicyViolation::KeyTrust {
1289 reason: "CRL checking requires X.509 chain validation"
1290 })
1291 ));
1292 }
1293
1294 #[cfg(feature = "xmldsig")]
1295 #[test]
1296 fn legacy_signature_algorithms_require_independent_policy_opt_ins() {
1297 let legacy = [
1298 SignatureAlgorithm::RsaSha1,
1299 SignatureAlgorithm::DsaSha1,
1300 SignatureAlgorithm::HmacSha1,
1301 ];
1302 let mut policy = VerificationPolicy::default();
1303
1304 for algorithm in legacy {
1305 assert!(matches!(
1306 policy.check_signature_algorithm(algorithm),
1307 Err(PolicyViolation::Algorithm { .. })
1308 ));
1309 policy
1310 .key_trust
1311 .allowed_legacy_signature_algorithms
1312 .insert(algorithm);
1313 assert_eq!(policy.check_signature_algorithm(algorithm), Ok(()));
1314 policy
1315 .key_trust
1316 .allowed_legacy_signature_algorithms
1317 .remove(&algorithm);
1318 }
1319 }
1320
1321 #[cfg(feature = "xmldsig")]
1322 #[test]
1323 fn dsa_key_policy_enforces_configured_minimum_and_hard_ceiling() {
1324 let policy = DsaKeyPolicy::default();
1325
1326 assert!(matches!(
1327 policy.validate_modulus_bits(1024),
1328 Err(PolicyViolation::KeySize {
1329 key_type: "DSA",
1330 minimum_bits: 2048,
1331 maximum_bits: 3072,
1332 actual_bits: 1024,
1333 ..
1334 })
1335 ));
1336 assert_eq!(policy.validate_modulus_bits(2048), Ok(()));
1337 assert!(matches!(
1338 policy.validate_modulus_bits(4096),
1339 Err(PolicyViolation::KeySize {
1340 key_type: "DSA",
1341 minimum_bits: 2048,
1342 maximum_bits: 3072,
1343 actual_bits: 4096,
1344 ..
1345 })
1346 ));
1347 }
1348
1349 #[cfg(feature = "xmldsig")]
1350 #[test]
1351 fn documented_xmldsig_limits_match_hard_limits() {
1352 let docs = include_str!("../docs/xmldsig.md");
1355 let mib = 1024 * 1024;
1356 assert!(docs.contains(&format!(
1357 "Individual resources are limited to {} MiB",
1358 crate::hard_limits::EXTERNAL_RESOURCE_BYTE_CEILING / mib
1359 )));
1360 assert!(docs.contains(&format!(
1361 "complete map to {} MiB",
1362 crate::hard_limits::EXTERNAL_RESOURCE_TOTAL_BYTE_CEILING / mib
1363 )));
1364 assert!(docs.contains(&format!(
1365 "ceilings are {} components and {} MiB per operation",
1366 crate::hard_limits::XML_BASE_COMPONENT_CEILING,
1367 crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING / mib
1368 )));
1369 }
1370}