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