Skip to main content

xml_sec/xmlenc/
key_establishment_budget.rs

1//! Operation-wide KDF preflight. No derived work precedes this gate.
2
3use crate::policy::{
4    KeyAgreementAlgorithm, KeyDerivationAlgorithm, KeyEstablishmentPolicy, PolicyViolation,
5};
6use crate::provider::{
7    CryptoProvider, KdfContext, KdfParameters, KeyAgreementKey, KeyAgreementParameters,
8    ProviderCapability, ProviderError, ProviderInputError, ProviderOperation,
9};
10use crate::xmldsig::{DigestAlgorithm, SignatureAlgorithm};
11
12use super::XmlEncError;
13
14/// Cumulative KDF allowance borrowed from one immutable operation policy.
15///
16/// Share this instance across every recipient, derived key and retry. Reservations
17/// are monotonic, including failed provider calls. Input slices remain borrowed;
18/// only the provider's returned key buffer is owned and zeroized by this API.
19pub struct KeyEstablishmentBudget<'a> {
20    policy: &'a KeyEstablishmentPolicy,
21    usage: KeyEstablishmentUsage,
22}
23
24/// Counters live with the operation, independently of the borrowed snapshot.
25/// This avoids cloning policy or resetting reservations in nested resolvers.
26#[derive(Debug, Default)]
27pub(super) struct KeyEstablishmentUsage {
28    hash_blocks: usize,
29    owned_bytes: usize,
30    modular_work: usize,
31}
32
33impl<'a> KeyEstablishmentBudget<'a> {
34    /// Validate the snapshot before compiling or executing key-establishment work.
35    pub fn new(policy: &'a KeyEstablishmentPolicy) -> Result<Self, PolicyViolation> {
36        policy.validate()?;
37        Ok(Self {
38            policy,
39            usage: KeyEstablishmentUsage::default(),
40        })
41    }
42
43    /// Derive through the selected provider only after syntax, permission,
44    /// capability and aggregate allocation/CPU reservations have succeeded.
45    pub fn derive_key(
46        &mut self,
47        provider: &dyn CryptoProvider,
48        parameters: &KdfParameters<'_>,
49        secret: &[u8],
50    ) -> Result<zeroize::Zeroizing<Vec<u8>>, XmlEncError> {
51        self.usage
52            .derive_key(self.policy, provider, parameters, secret)
53    }
54
55    /// Agree and derive using the same monotonic operation allowance. Permission
56    /// and both workspace/output reservations precede either provider callback.
57    pub fn agree_and_derive(
58        &mut self,
59        provider: &dyn CryptoProvider,
60        key: &dyn KeyAgreementKey,
61        agreement: &KeyAgreementParameters<'_>,
62        parameters: &KdfParameters<'_>,
63    ) -> Result<zeroize::Zeroizing<Vec<u8>>, XmlEncError> {
64        self.usage
65            .agree_and_derive(self.policy, provider, key, agreement, parameters)
66    }
67
68    /// Cumulative compression work reserved, including failed attempts.
69    pub const fn hash_blocks(&self) -> usize {
70        self.usage.hash_blocks
71    }
72
73    /// Cumulative output allocations reserved, including failed attempts.
74    pub const fn owned_bytes(&self) -> usize {
75        self.usage.owned_bytes
76    }
77
78    /// Cumulative finite-field validation and exponentiation work reserved.
79    pub const fn modular_work(&self) -> usize {
80        self.usage.modular_work
81    }
82
83    pub(crate) fn reserve_dh_import(
84        &mut self,
85        p_bits: usize,
86        q_bits: usize,
87        p_twos: usize,
88        q_twos: usize,
89    ) -> Result<(), XmlEncError> {
90        self.policy.validate()?;
91        if self
92            .policy
93            .check_agreement(KeyAgreementAlgorithm::DhEs)
94            .is_err()
95        {
96            self.policy
97                .check_agreement(KeyAgreementAlgorithm::LegacyDh)?;
98        }
99        self.policy.check_dh_domain(p_bits, q_bits)?;
100        reserve(
101            crate::policy::resource_name::DH_MODULUS_BITS,
102            0,
103            p_bits as u128,
104            self.policy.max_dh_modulus_bits,
105        )?;
106        // crypto-primes 0.7 MillerRabin holds its parameters across 64 rounds.
107        // Each round allocates a 16-entry power table plus conversion buffers,
108        // then at most v2(n-1) squares. Include boxed headers and public-domain
109        // import/division/Montgomery setup rather than charging payload alone.
110        let bytes = |bits: usize, twos: usize| {
111            let slot = bits.div_ceil(64) as u128 * 8 + 64;
112            slot * (512 + 64 * (64 + twos as u128))
113        };
114        self.usage.commit_dh_reservation(
115            self.policy,
116            modular_cost(p_bits, p_bits, 64)
117                + modular_cost(q_bits, q_bits, 64)
118                + modular_cost(p_bits, p_bits, 2),
119            bytes(p_bits, p_twos) + bytes(q_bits, q_twos),
120        )
121    }
122}
123
124fn modular_cost(bits: usize, exponent_bits: usize, count: usize) -> u128 {
125    let limbs = bits.div_ceil(usize::BITS as usize) as u128;
126    // Bound native-word product passes for the fixed-window exponentiation,
127    // Miller-Rabin's trailing squares and reduction, including window setup.
128    // Counting native words also preserves the bound on 32-bit targets.
129    (limbs * limbs).div_ceil(64) * (exponent_bits as u128 + 64) * 8 * count as u128
130}
131
132impl KeyEstablishmentUsage {
133    pub(super) fn agree_and_derive(
134        &mut self,
135        policy: &KeyEstablishmentPolicy,
136        provider: &dyn CryptoProvider,
137        key: &dyn KeyAgreementKey,
138        agreement: &KeyAgreementParameters<'_>,
139        parameters: &KdfParameters<'_>,
140    ) -> Result<zeroize::Zeroizing<Vec<u8>>, XmlEncError> {
141        policy.validate()?;
142        let mechanism = KeyAgreementAlgorithm::from_uri(agreement.algorithm)
143            .ok_or_else(|| XmlEncError::UnsupportedAlgorithm(agreement.algorithm.into()))?;
144        policy.check_agreement(mechanism)?;
145        let width = match (mechanism, agreement.peer_public_key) {
146            (KeyAgreementAlgorithm::X25519, bytes) if bytes.len() == 32 => 32,
147            (KeyAgreementAlgorithm::EcdhEs, [4, tail @ ..])
148                if matches!(tail.len(), 64 | 96 | 132) =>
149            {
150                tail.len() / 2
151            }
152            (KeyAgreementAlgorithm::EcdhEs, [2 | 3, tail @ ..])
153                if matches!(tail.len(), 32 | 48 | 66) =>
154            {
155                tail.len()
156            }
157            (KeyAgreementAlgorithm::DhEs | KeyAgreementAlgorithm::LegacyDh, bytes) => {
158                let (p_bits, q_bits) = key
159                    .dh_domain_bits()
160                    .ok_or_else(|| invalid(ProviderInputError::DhKey))?;
161                policy.check_dh_domain(p_bits, q_bits)?;
162                let width = p_bits.div_ceil(8);
163                if bytes.is_empty() || bytes.len() > width || width == 0 {
164                    return Err(invalid(ProviderInputError::DhKey));
165                }
166                width
167            }
168            _ => return Err(invalid(ProviderInputError::EcdhKey)),
169        };
170        // XMLEnc 1.1 §5.6.4 fixes ZZ to the field-element octet width. Reserve
171        // both ZZ and the consuming key before either provider callback; a
172        // denied KDF must not initiate otherwise permitted scalar multiplication.
173        // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-ECDH-ES
174        let (algorithm, digest, blocks) = preflight(parameters, width)?;
175        // The legacy agreement URI fixes its implicit KDF. DH-ES permits an
176        // explicit KDF, but replacing legacy derivation silently changes the
177        // wire algorithm, even if both mechanisms are separately permitted.
178        // XMLEnc 1.1 §§5.6.2.1–5.6.2.2:
179        // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-DHKeyAgreementLegacyKDF
180        if mechanism == KeyAgreementAlgorithm::LegacyDh
181            && algorithm != KeyDerivationAlgorithm::LegacyDh
182        {
183            return Err(invalid(ProviderInputError::LegacyDhKdfParameters));
184        }
185        policy.check_derivation(algorithm, digest)?;
186        provider.require_capability(ProviderCapability::KeyAgreement(agreement))?;
187        provider.require_capability(ProviderCapability::Kdf(parameters))?;
188        let dh = matches!(
189            mechanism,
190            KeyAgreementAlgorithm::DhEs | KeyAgreementAlgorithm::LegacyDh
191        );
192        let modular = if dh {
193            modular_cost(width * 8, width * 8, 2)
194        } else {
195            0
196        };
197        let workspace = if dh { (width as u128 + 64) * 256 } else { 0 };
198        self.commit_all(
199            policy,
200            blocks,
201            modular,
202            width as u128 + parameters.output_len as u128 + workspace,
203        )?;
204        let secret = zeroize::Zeroizing::new(provider.agree_key(key, agreement)?);
205        check_width(&secret, width)?;
206        let output = zeroize::Zeroizing::new(provider.derive_key(parameters, &secret)?);
207        check_width(&output, parameters.output_len)?;
208        Ok(output)
209    }
210
211    pub(super) fn derive_key(
212        &mut self,
213        policy: &KeyEstablishmentPolicy,
214        provider: &dyn CryptoProvider,
215        parameters: &KdfParameters<'_>,
216        secret: &[u8],
217    ) -> Result<zeroize::Zeroizing<Vec<u8>>, XmlEncError> {
218        policy.validate()?;
219        let (algorithm, digest, blocks) = preflight(parameters, secret.len())?;
220        policy.check_derivation(algorithm, digest)?;
221        provider.require_capability(ProviderCapability::Kdf(parameters))?;
222        self.commit_reservation(policy, blocks, parameters.output_len as u128)?;
223        let output = zeroize::Zeroizing::new(provider.derive_key(parameters, secret)?);
224        check_width(&output, parameters.output_len)?;
225        Ok(output)
226    }
227
228    fn commit_reservation(
229        &mut self,
230        policy: &KeyEstablishmentPolicy,
231        blocks: u128,
232        allocations: u128,
233    ) -> Result<(), XmlEncError> {
234        self.commit_all(policy, blocks, 0, allocations)
235    }
236
237    fn commit_dh_reservation(
238        &mut self,
239        policy: &KeyEstablishmentPolicy,
240        modular: u128,
241        allocations: u128,
242    ) -> Result<(), XmlEncError> {
243        self.commit_all(policy, 0, modular, allocations)
244    }
245
246    fn commit_all(
247        &mut self,
248        policy: &KeyEstablishmentPolicy,
249        blocks: u128,
250        modular: u128,
251        allocations: u128,
252    ) -> Result<(), XmlEncError> {
253        let work = reserve(
254            crate::policy::resource_name::KEY_ESTABLISHMENT_HASH_BLOCKS,
255            self.hash_blocks,
256            blocks,
257            policy.max_hash_blocks,
258        )?;
259        let bytes = reserve(
260            crate::policy::resource_name::KEY_ESTABLISHMENT_OWNED_BYTES,
261            self.owned_bytes,
262            allocations,
263            policy.max_owned_bytes,
264        )?;
265        let modular = reserve(
266            crate::policy::resource_name::KEY_ESTABLISHMENT_MODULAR_WORK,
267            self.modular_work,
268            modular,
269            policy.max_modular_work,
270        )?;
271        // Commit both reservations atomically before provider work. No recipient
272        // may recover exhausted operation allowance by moving to another source.
273        self.hash_blocks = work;
274        self.owned_bytes = bytes;
275        self.modular_work = modular;
276        Ok(())
277    }
278}
279
280fn check_width(bytes: &[u8], expected: usize) -> Result<(), XmlEncError> {
281    if bytes.len() != expected {
282        return Err(ProviderError::InvalidKeySize {
283            expected,
284            actual: bytes.len(),
285        }
286        .into());
287    }
288    Ok(())
289}
290
291fn reserve(
292    resource: &'static str,
293    used: usize,
294    count: u128,
295    maximum: usize,
296) -> Result<usize, PolicyViolation> {
297    if used > maximum || count > (maximum - used) as u128 {
298        return Err(PolicyViolation::ResourceLimit {
299            resource,
300            maximum,
301            actual: usize::try_from(used as u128 + count).unwrap_or(usize::MAX),
302        });
303    }
304    Ok(used + count as usize)
305}
306
307fn invalid(kind: ProviderInputError) -> XmlEncError {
308    ProviderError::InvalidInput(kind).into()
309}
310
311fn unsupported(uri: &str) -> XmlEncError {
312    ProviderError::Unsupported {
313        operation: ProviderOperation::Kdf,
314        algorithm: Some(uri.into()),
315    }
316    .into()
317}
318
319// SHA compression-block costs include padding and HMAC's inner/outer hashes,
320// not merely the user-supplied PBKDF2 iteration count. The bound follows the
321// provider's streaming framing without allocating concatenated input strings.
322// RFC 8018 §5.2, RFC 5869 §§2.2–2.3, XMLEnc 1.1 §§5.4.1 and 5.6.2.2:
323// https://www.rfc-editor.org/rfc/rfc8018.html#section-5.2
324// https://www.rfc-editor.org/rfc/rfc5869.html#section-2
325// https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-ConcatKDF
326fn preflight(
327    parameters: &KdfParameters<'_>,
328    secret_len: usize,
329) -> Result<(KeyDerivationAlgorithm, DigestAlgorithm, u128), XmlEncError> {
330    let algorithm = KeyDerivationAlgorithm::from_uri(parameters.algorithm)
331        .ok_or_else(|| unsupported(parameters.algorithm))?;
332    let uri = parameters
333        .digest
334        .ok_or_else(|| unsupported(parameters.algorithm))?;
335    let digest = match algorithm {
336        KeyDerivationAlgorithm::Hkdf | KeyDerivationAlgorithm::Pbkdf2 => {
337            match SignatureAlgorithm::from_uri(uri) {
338                Some(SignatureAlgorithm::HmacSha1) => DigestAlgorithm::Sha1,
339                Some(SignatureAlgorithm::HmacSha224) => DigestAlgorithm::Sha224,
340                Some(SignatureAlgorithm::HmacSha256) => DigestAlgorithm::Sha256,
341                Some(SignatureAlgorithm::HmacSha384) => DigestAlgorithm::Sha384,
342                Some(SignatureAlgorithm::HmacSha512) => DigestAlgorithm::Sha512,
343                _ => return Err(unsupported(uri)),
344            }
345        }
346        _ => match DigestAlgorithm::from_uri(uri) {
347            Some(
348                value @ (DigestAlgorithm::Sha1
349                | DigestAlgorithm::Sha224
350                | DigestAlgorithm::Sha256
351                | DigestAlgorithm::Sha384
352                | DigestAlgorithm::Sha512),
353            ) => value,
354            _ => return Err(unsupported(uri)),
355        },
356    };
357    let width = digest.output_len();
358    let block = if width <= 32 { 64u128 } else { 128 };
359    let padding = if width <= 32 { 9u128 } else { 17 };
360    let hash = |bytes: u128| (bytes + padding).div_ceil(block);
361    let hmac_key = |bytes: usize| {
362        if bytes as u128 > block {
363            hash(bytes as u128) + 2
364        } else {
365            2
366        }
367    };
368    let output_blocks = parameters.output_len.div_ceil(width) as u128;
369    let work = match algorithm {
370        KeyDerivationAlgorithm::Pbkdf2 => {
371            if parameters.iterations == 0
372                || parameters.output_len == 0
373                || octets(parameters.info).is_none_or(|info| !info.is_empty())
374                || output_blocks > u32::MAX as u128
375            {
376                return Err(invalid(ProviderInputError::Pbkdf2Parameters));
377            }
378            // The PRF and salt prefix are prepared once, then cloned. Account
379            // their actual compression work once, not once per output block.
380            let salt = parameters.salt.len() as u128;
381            hmac_key(secret_len)
382                + salt / block
383                + output_blocks
384                    * (hash(salt % block + 4) + 1 + 2 * (parameters.iterations as u128 - 1))
385        }
386        KeyDerivationAlgorithm::Hkdf => {
387            let info = octets(parameters.info)
388                .ok_or_else(|| invalid(ProviderInputError::HkdfParameters))?;
389            if parameters.iterations != 0 || output_blocks > 255 {
390                return Err(invalid(ProviderInputError::HkdfParameters));
391            }
392            // Conservatively include a previous full digest in every T_i,
393            // including T_1, which actually uses an empty previous value.
394            hmac_key(parameters.salt.len())
395                + hash(secret_len as u128)
396                + 1
397                + 2
398                + output_blocks * (hash(width as u128 + info.len() as u128 + 1) + 1)
399        }
400        KeyDerivationAlgorithm::ConcatKdf => {
401            if parameters.iterations != 0
402                || !parameters.salt.is_empty()
403                || parameters.output_len == 0
404                || output_blocks > u32::MAX as u128
405            {
406                return Err(invalid(ProviderInputError::ConcatKdfParameters));
407            }
408            let (bytes, bits) = match parameters.info {
409                KdfContext::Octets(bytes) => (bytes, bytes.len() as u128 * 8),
410                KdfContext::Bits { bytes, bit_len } => (bytes, bit_len as u128),
411                _ => return Err(invalid(ProviderInputError::ConcatKdfParameters)),
412            };
413            let used = (bits % 8) as usize;
414            if bits.div_ceil(8) != bytes.len() as u128
415                || (used != 0 && bytes[bytes.len() - 1] & (0xff >> used) != 0)
416            {
417                return Err(invalid(ProviderInputError::ConcatKdfParameters));
418            }
419            output_blocks * hash(4 + secret_len as u128 + bits.div_ceil(8))
420        }
421        KeyDerivationAlgorithm::LegacyDh => {
422            let KdfContext::LegacyDh {
423                encryption_algorithm,
424                nonce,
425            } = parameters.info
426            else {
427                return Err(invalid(ProviderInputError::LegacyDhKdfParameters));
428            };
429            if parameters.iterations != 0
430                || !parameters.salt.is_empty()
431                || parameters.output_len == 0
432                || output_blocks > 255
433                || parameters.output_len.checked_mul(8).is_none()
434            {
435                return Err(invalid(ProviderInputError::LegacyDhKdfParameters));
436            }
437            output_blocks
438                * hash(
439                    secret_len as u128
440                        + 2
441                        + encryption_algorithm.len() as u128
442                        + nonce.len() as u128
443                        + 40,
444                )
445        }
446    };
447    Ok((algorithm, digest, work))
448}
449
450fn octets(context: KdfContext<'_>) -> Option<&[u8]> {
451    match context {
452        KdfContext::Octets(bytes) => Some(bytes),
453        KdfContext::Bits { bytes, bit_len }
454            if bit_len.is_multiple_of(8) && bit_len / 8 == bytes.len() =>
455        {
456            Some(bytes)
457        }
458        _ => None,
459    }
460}