Skip to main content

xml_sec/policy/
key_establishment.rs

1//! Shared key-establishment domain: capability is not permission.
2
3use super::PolicyViolation;
4use crate::xmldsig::DigestAlgorithm;
5use std::collections::HashSet;
6
7/// Agreement mechanisms identified by their XML algorithm URI.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum KeyAgreementAlgorithm {
10    /// Named-curve ECDH-ES.
11    EcdhEs,
12    /// RFC 7748 X25519.
13    X25519,
14    /// RFC 7748 X448; requires explicit agreement permission.
15    X448,
16    /// Finite-field DH-ES with a separate KDF.
17    DhEs,
18    /// Legacy finite-field DH and its SHA-based derivation.
19    LegacyDh,
20}
21
22impl KeyAgreementAlgorithm {
23    /// Stable wire identifier; this does not grant algorithm permission.
24    pub const fn uri(self) -> &'static str {
25        match self {
26            Self::EcdhEs => "http://www.w3.org/2009/xmlenc11#ECDH-ES",
27            Self::X25519 => "http://www.w3.org/2021/04/xmldsig-more#x25519",
28            Self::X448 => "http://www.w3.org/2021/04/xmldsig-more#x448",
29            Self::DhEs => "http://www.w3.org/2009/xmlenc11#dh-es",
30            Self::LegacyDh => "http://www.w3.org/2001/04/xmlenc#dh",
31        }
32    }
33
34    /// Recognize the exact URI, without normalizing or aliasing algorithms.
35    pub fn from_uri(uri: &str) -> Option<Self> {
36        [
37            Self::EcdhEs,
38            Self::X25519,
39            Self::X448,
40            Self::DhEs,
41            Self::LegacyDh,
42        ]
43        .into_iter()
44        .find(|algorithm| algorithm.uri() == uri)
45    }
46}
47
48/// KDF mechanisms accepted by the shared policy domain.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub enum KeyDerivationAlgorithm {
51    /// XMLEnc 1.1 ConcatKDF with bit-string context.
52    ConcatKdf,
53    /// RFC 5869 HKDF.
54    Hkdf,
55    /// RFC 8018 PBKDF2.
56    Pbkdf2,
57    /// XMLEnc's legacy DH derivation.
58    LegacyDh,
59}
60
61impl KeyDerivationAlgorithm {
62    /// Stable wire identifier.
63    pub const fn uri(self) -> &'static str {
64        match self {
65            Self::ConcatKdf => "http://www.w3.org/2009/xmlenc11#ConcatKDF",
66            Self::Hkdf => "http://www.w3.org/2021/04/xmldsig-more#hkdf",
67            Self::Pbkdf2 => "http://www.w3.org/2009/xmlenc11#pbkdf2",
68            Self::LegacyDh => "http://www.w3.org/2001/04/xmlenc#dh",
69        }
70    }
71
72    /// Recognize the exact URI.
73    pub fn from_uri(uri: &str) -> Option<Self> {
74        [Self::ConcatKdf, Self::Hkdf, Self::Pbkdf2, Self::LegacyDh]
75            .into_iter()
76            .find(|algorithm| algorithm.uri() == uri)
77    }
78}
79
80/// Authentication boundary for content encrypted directly with a KEM secret.
81#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
82pub enum KemContentAuthentication {
83    /// Require an authenticated content cipher, such as AES-GCM.
84    #[default]
85    RequireAuthenticatedCipher,
86    /// Compatibility permission: the caller authenticates the complete encrypted
87    /// input before decryption and binds external integrity protection of output
88    /// to its encryption key. CBC padding never proves KEM ciphertext validity
89    /// or sender identity.
90    ExternalAuthenticated,
91}
92
93/// Immutable permission and limits shared by encryption and decryption.
94///
95/// These are deployment decisions, not specification requirements. Requests
96/// supply private keys and expected party identities separately; XML input must
97/// never select this policy or reset its operation-wide budget.
98#[derive(Debug, Clone)]
99pub struct KeyEstablishmentPolicy {
100    /// Exact experimental KEM permissions. Empty denies all parameter sets.
101    pub encapsulation_algorithms: HashSet<crate::provider::KeyEncapsulationAlgorithm>,
102    /// Separate permission for unauthenticated direct KEM content composition.
103    pub kem_content_authentication: KemContentAuthentication,
104    /// Cumulative KEM attempts; failed provider calls consume allowance too.
105    pub max_encapsulation_operations: usize,
106    /// Exact agreement permissions. None permits only ECDH-ES and X25519.
107    pub agreement_algorithms: Option<HashSet<KeyAgreementAlgorithm>>,
108    /// Exact KDF permissions. None permits ConcatKDF/HKDF/PBKDF2, not legacy DH.
109    pub derivation_algorithms: Option<HashSet<KeyDerivationAlgorithm>>,
110    /// Underlying digest permission, including HMAC PRFs. None permits SHA-2.
111    /// SHA-1 requires an explicit grant even when required by a legacy profile.
112    pub digest_algorithms: Option<HashSet<DigestAlgorithm>>,
113    /// Cumulative conservative hash compression-block allowance per operation.
114    pub max_hash_blocks: usize,
115    /// Cumulative shared-secret, derived-key and finite-field validation
116    /// allocations reserved before provider work, including boxed workspace.
117    /// Failed attempts consume the allowance too; no retry may reset it.
118    pub max_owned_bytes: usize,
119    /// Maximum finite-field modulus width; capability alone grants no permission.
120    pub max_dh_modulus_bits: usize,
121    /// Product strength baseline, not XMLEnc's 512-bit wire-format minimum.
122    /// Legacy interoperability requires an explicit caller-selected lower value.
123    pub minimum_dh_modulus_bits: usize,
124    /// Minimum prime-order subgroup width, independent of the modulus width.
125    pub minimum_dh_subgroup_bits: usize,
126    /// Cumulative modular work, in batches of up to 64 limb products. Charged
127    /// conservatively for validation and exponentiation before their execution.
128    pub max_modular_work: usize,
129}
130
131impl Default for KeyEstablishmentPolicy {
132    fn default() -> Self {
133        Self {
134            encapsulation_algorithms: HashSet::new(),
135            kem_content_authentication: KemContentAuthentication::default(),
136            max_encapsulation_operations: 64,
137            agreement_algorithms: None,
138            derivation_algorithms: None,
139            digest_algorithms: None,
140            max_hash_blocks: 1_000_000,
141            max_owned_bytes: 8 * 1024 * 1024,
142            max_dh_modulus_bits: 8_192,
143            minimum_dh_modulus_bits: 2_048,
144            minimum_dh_subgroup_bits: 224,
145            max_modular_work: 64_000_000,
146        }
147    }
148}
149
150impl KeyEstablishmentPolicy {
151    /// Enforce the direct-content boundary independently of KEM capability.
152    #[cfg(feature = "xmlenc")]
153    pub(crate) fn check_kem_content(
154        &self,
155        algorithm: crate::xmlenc::DataEncryptionAlgorithm,
156    ) -> Result<(), PolicyViolation> {
157        // FIPS 203 §6.3 forbids exporting the implicit-rejection flag; CBC
158        // padding therefore cannot confirm a decapsulated key. Requiring an
159        // authenticated cipher by default is product policy, not a FIPS ban
160        // on CBC. The explicit exception requires caller-owned authentication.
161        // https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.pdf
162        // XMLEnc 1.1 §6.1.1: https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-edata-attacks
163        permission(
164            algorithm.is_authenticated()
165                || self.kem_content_authentication
166                    == KemContentAuthentication::ExternalAuthenticated,
167            "direct KEM content without external authentication",
168            algorithm.uri(),
169        )
170    }
171
172    /// Reject configuration exceeding implementation ceilings before any work.
173    pub fn validate(&self) -> Result<(), PolicyViolation> {
174        super::ResourcePolicy::within(
175            "key encapsulation operations",
176            self.max_encapsulation_operations,
177            crate::hard_limits::KEY_ENCAPSULATION_OPERATION_CEILING,
178        )?;
179        for (value, minimum, resource) in [
180            (
181                self.minimum_dh_modulus_bits,
182                512,
183                super::resource_name::DH_MODULUS_BITS,
184            ),
185            (
186                self.minimum_dh_subgroup_bits,
187                160,
188                super::resource_name::DH_SUBGROUP_BITS,
189            ),
190        ] {
191            if value < minimum || value > self.max_dh_modulus_bits {
192                return Err(PolicyViolation::InvalidResourceLimit {
193                    resource,
194                    actual: value,
195                    requirement: "minimum must respect the wire floor and modulus maximum",
196                });
197            }
198        }
199        // XMLEnc 1.1 §5.6.1: p = j*q + 1, j >= 2, hence q needs fewer
200        // bits than p. Equality is valid for the modulus minimum, not q's.
201        // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-DHKeyValue
202        if self.minimum_dh_subgroup_bits >= self.max_dh_modulus_bits {
203            return Err(PolicyViolation::InvalidResourceLimit {
204                resource: super::resource_name::DH_SUBGROUP_BITS,
205                actual: self.minimum_dh_subgroup_bits,
206                requirement: "subgroup minimum must be strictly below modulus maximum",
207            });
208        }
209        super::ResourcePolicy::within(
210            super::resource_name::DH_MODULUS_BITS,
211            self.max_dh_modulus_bits,
212            crate::hard_limits::DH_MODULUS_BIT_CEILING,
213        )?;
214        super::ResourcePolicy::within(
215            super::resource_name::KEY_ESTABLISHMENT_MODULAR_WORK,
216            self.max_modular_work,
217            crate::hard_limits::KEY_ESTABLISHMENT_MODULAR_WORK_CEILING,
218        )?;
219        super::ResourcePolicy::within(
220            super::resource_name::KEY_ESTABLISHMENT_HASH_BLOCKS,
221            self.max_hash_blocks,
222            crate::hard_limits::KEY_ESTABLISHMENT_HASH_BLOCK_CEILING,
223        )?;
224        super::ResourcePolicy::within(
225            super::resource_name::KEY_ESTABLISHMENT_OWNED_BYTES,
226            self.max_owned_bytes,
227            crate::hard_limits::KEY_ESTABLISHMENT_BYTE_CEILING,
228        )
229    }
230
231    /// Enforce domain strength at import and again at operation dispatch.
232    #[cfg(feature = "xmlenc")]
233    pub(crate) fn check_dh_domain(
234        &self,
235        p_bits: usize,
236        q_bits: usize,
237    ) -> Result<(), PolicyViolation> {
238        for (actual_bits, minimum_bits, key_type) in [
239            (p_bits, self.minimum_dh_modulus_bits, "DH modulus"),
240            (q_bits, self.minimum_dh_subgroup_bits, "DH subgroup"),
241        ] {
242            if actual_bits < minimum_bits || actual_bits > self.max_dh_modulus_bits {
243                return Err(PolicyViolation::KeySize {
244                    operation: "key agreement",
245                    key_type,
246                    minimum_bits,
247                    maximum_bits: self.max_dh_modulus_bits,
248                    actual_bits,
249                });
250            }
251        }
252        Ok(())
253    }
254
255    /// Check permission independently of the selected provider's capability.
256    pub fn check_agreement(&self, algorithm: KeyAgreementAlgorithm) -> Result<(), PolicyViolation> {
257        let allowed = match &self.agreement_algorithms {
258            Some(allowed) => allowed.contains(&algorithm),
259            None => matches!(
260                algorithm,
261                KeyAgreementAlgorithm::EcdhEs | KeyAgreementAlgorithm::X25519
262            ),
263        };
264        permission(allowed, "key agreement", algorithm.uri())
265    }
266
267    /// Capability does not grant experimental key-establishment permission.
268    pub fn check_encapsulation(
269        &self,
270        algorithm: crate::provider::KeyEncapsulationAlgorithm,
271    ) -> Result<(), PolicyViolation> {
272        permission(
273            self.encapsulation_algorithms.contains(&algorithm),
274            "key encapsulation",
275            algorithm.uri(),
276        )
277    }
278
279    /// Check KDF and its underlying digest independently. An HMAC URI does not
280    /// make a digest permission apply to signatures or certificate validation.
281    pub fn check_derivation(
282        &self,
283        algorithm: KeyDerivationAlgorithm,
284        digest: DigestAlgorithm,
285    ) -> Result<(), PolicyViolation> {
286        let allowed = match &self.derivation_algorithms {
287            Some(allowed) => allowed.contains(&algorithm),
288            None => algorithm != KeyDerivationAlgorithm::LegacyDh,
289        };
290        permission(allowed, "key derivation", algorithm.uri())?;
291        self.check_derivation_digest(digest)
292    }
293
294    /// Digest permission is independent of the selected derivation mechanism.
295    pub fn check_derivation_digest(&self, digest: DigestAlgorithm) -> Result<(), PolicyViolation> {
296        let allowed = match &self.digest_algorithms {
297            Some(allowed) => allowed.contains(&digest),
298            None => matches!(
299                digest,
300                DigestAlgorithm::Sha224
301                    | DigestAlgorithm::Sha256
302                    | DigestAlgorithm::Sha384
303                    | DigestAlgorithm::Sha512
304            ),
305        };
306        permission(allowed, "key derivation digest", digest.uri())
307    }
308}
309
310fn permission(allowed: bool, operation: &'static str, uri: &str) -> Result<(), PolicyViolation> {
311    if allowed {
312        Ok(())
313    } else {
314        Err(PolicyViolation::Algorithm {
315            operation,
316            algorithm: uri.to_owned(),
317        })
318    }
319}