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/// Immutable permission and limits shared by encryption and decryption.
72///
73/// These are deployment decisions, not specification requirements. Requests
74/// supply private keys and expected party identities separately; XML input must
75/// never select this policy or reset its operation-wide budget.
76#[derive(Debug, Clone)]
77pub struct KeyEstablishmentPolicy {
78    /// Exact agreement permissions. None permits ECDH-ES/X25519, not DH.
79    pub agreement_algorithms: Option<HashSet<KeyAgreementAlgorithm>>,
80    /// Exact KDF permissions. None permits ConcatKDF/HKDF/PBKDF2, not legacy DH.
81    pub derivation_algorithms: Option<HashSet<KeyDerivationAlgorithm>>,
82    /// Underlying digest permission, including HMAC PRFs. None permits SHA-2.
83    /// SHA-1 requires an explicit grant even when required by a legacy profile.
84    pub digest_algorithms: Option<HashSet<DigestAlgorithm>>,
85    /// Cumulative conservative hash compression-block allowance per operation.
86    pub max_hash_blocks: usize,
87    /// Cumulative shared-secret, derived-key and finite-field validation
88    /// allocations reserved before provider work, including boxed workspace.
89    /// Failed attempts consume the allowance too; no retry may reset it.
90    pub max_owned_bytes: usize,
91    /// Maximum finite-field modulus width; capability alone grants no permission.
92    pub max_dh_modulus_bits: usize,
93    /// Product strength baseline, not XMLEnc's 512-bit wire-format minimum.
94    /// Legacy interoperability requires an explicit caller-selected lower value.
95    pub minimum_dh_modulus_bits: usize,
96    /// Minimum prime-order subgroup width, independent of the modulus width.
97    pub minimum_dh_subgroup_bits: usize,
98    /// Cumulative modular work, in batches of up to 64 limb products. Charged
99    /// conservatively for validation and exponentiation before their execution.
100    pub max_modular_work: usize,
101}
102
103impl Default for KeyEstablishmentPolicy {
104    fn default() -> Self {
105        Self {
106            agreement_algorithms: None,
107            derivation_algorithms: None,
108            digest_algorithms: None,
109            max_hash_blocks: 1_000_000,
110            max_owned_bytes: 8 * 1024 * 1024,
111            max_dh_modulus_bits: 8_192,
112            minimum_dh_modulus_bits: 2_048,
113            minimum_dh_subgroup_bits: 224,
114            max_modular_work: 64_000_000,
115        }
116    }
117}
118
119impl KeyEstablishmentPolicy {
120    /// Reject configuration exceeding implementation ceilings before any work.
121    pub fn validate(&self) -> Result<(), PolicyViolation> {
122        for (value, minimum, resource) in [
123            (
124                self.minimum_dh_modulus_bits,
125                512,
126                super::resource_name::DH_MODULUS_BITS,
127            ),
128            (
129                self.minimum_dh_subgroup_bits,
130                160,
131                super::resource_name::DH_SUBGROUP_BITS,
132            ),
133        ] {
134            if value < minimum || value > self.max_dh_modulus_bits {
135                return Err(PolicyViolation::InvalidResourceLimit {
136                    resource,
137                    actual: value,
138                    requirement: "minimum must respect the wire floor and modulus maximum",
139                });
140            }
141        }
142        // XMLEnc 1.1 ยง5.6.1: p = j*q + 1, j >= 2, hence q needs fewer
143        // bits than p. Equality is valid for the modulus minimum, not q's.
144        // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-DHKeyValue
145        if self.minimum_dh_subgroup_bits >= self.max_dh_modulus_bits {
146            return Err(PolicyViolation::InvalidResourceLimit {
147                resource: super::resource_name::DH_SUBGROUP_BITS,
148                actual: self.minimum_dh_subgroup_bits,
149                requirement: "subgroup minimum must be strictly below modulus maximum",
150            });
151        }
152        super::ResourcePolicy::within(
153            super::resource_name::DH_MODULUS_BITS,
154            self.max_dh_modulus_bits,
155            crate::hard_limits::DH_MODULUS_BIT_CEILING,
156        )?;
157        super::ResourcePolicy::within(
158            super::resource_name::KEY_ESTABLISHMENT_MODULAR_WORK,
159            self.max_modular_work,
160            crate::hard_limits::KEY_ESTABLISHMENT_MODULAR_WORK_CEILING,
161        )?;
162        super::ResourcePolicy::within(
163            super::resource_name::KEY_ESTABLISHMENT_HASH_BLOCKS,
164            self.max_hash_blocks,
165            crate::hard_limits::KEY_ESTABLISHMENT_HASH_BLOCK_CEILING,
166        )?;
167        super::ResourcePolicy::within(
168            super::resource_name::KEY_ESTABLISHMENT_OWNED_BYTES,
169            self.max_owned_bytes,
170            crate::hard_limits::KEY_ESTABLISHMENT_BYTE_CEILING,
171        )
172    }
173
174    /// Enforce domain strength at import and again at operation dispatch.
175    pub(crate) fn check_dh_domain(
176        &self,
177        p_bits: usize,
178        q_bits: usize,
179    ) -> Result<(), PolicyViolation> {
180        for (actual_bits, minimum_bits, key_type) in [
181            (p_bits, self.minimum_dh_modulus_bits, "DH modulus"),
182            (q_bits, self.minimum_dh_subgroup_bits, "DH subgroup"),
183        ] {
184            if actual_bits < minimum_bits || actual_bits > self.max_dh_modulus_bits {
185                return Err(PolicyViolation::KeySize {
186                    operation: "key agreement",
187                    key_type,
188                    minimum_bits,
189                    maximum_bits: self.max_dh_modulus_bits,
190                    actual_bits,
191                });
192            }
193        }
194        Ok(())
195    }
196
197    /// Check permission independently of the selected provider's capability.
198    pub fn check_agreement(&self, algorithm: KeyAgreementAlgorithm) -> Result<(), PolicyViolation> {
199        let allowed = match &self.agreement_algorithms {
200            Some(allowed) => allowed.contains(&algorithm),
201            None => matches!(
202                algorithm,
203                KeyAgreementAlgorithm::EcdhEs | KeyAgreementAlgorithm::X25519
204            ),
205        };
206        permission(allowed, "key agreement", algorithm.uri())
207    }
208
209    /// Check KDF and its underlying digest independently. An HMAC URI does not
210    /// make a digest permission apply to signatures or certificate validation.
211    pub fn check_derivation(
212        &self,
213        algorithm: KeyDerivationAlgorithm,
214        digest: DigestAlgorithm,
215    ) -> Result<(), PolicyViolation> {
216        let allowed = match &self.derivation_algorithms {
217            Some(allowed) => allowed.contains(&algorithm),
218            None => algorithm != KeyDerivationAlgorithm::LegacyDh,
219        };
220        permission(allowed, "key derivation", algorithm.uri())?;
221        let allowed = match &self.digest_algorithms {
222            Some(allowed) => allowed.contains(&digest),
223            None => matches!(
224                digest,
225                DigestAlgorithm::Sha224
226                    | DigestAlgorithm::Sha256
227                    | DigestAlgorithm::Sha384
228                    | DigestAlgorithm::Sha512
229            ),
230        };
231        permission(allowed, "key derivation digest", digest.uri())
232    }
233}
234
235fn permission(allowed: bool, operation: &'static str, uri: &str) -> Result<(), PolicyViolation> {
236    if allowed {
237        Ok(())
238    } else {
239        Err(PolicyViolation::Algorithm {
240            operation,
241            algorithm: uri.to_owned(),
242        })
243    }
244}