Skip to main content

xml_sec/provider/key_establishment/
dh.rs

1//! Validated finite-field DH. Public-domain checks are variable-time; private
2//! exponentiation uses the full, fixed modulus precision.
3
4use crypto_bigint::{
5    BoxedUint, NonZero, Odd,
6    modular::{BoxedMontyForm, BoxedMontyParams},
7};
8use crypto_primes::hazmat::MillerRabin;
9use zeroize::Zeroizing;
10
11use crate::provider::{
12    CryptoProvider, KeyAgreementKey, KeyAgreementParameters, ProviderError, ProviderInputError,
13    ProviderOperation,
14};
15use crate::xmlenc::{KeyEstablishmentBudget, XmlEncError};
16
17use super::{DH_ES_URI, DH_URI};
18
19/// Finite-field DH handle with validated prime-order domain parameters.
20/// Private scalars and shared secrets are never exposed by Debug.
21pub struct RustCryptoDhKey {
22    params: BoxedMontyParams,
23    order: BoxedUint,
24    generator: BoxedUint,
25    private: Zeroizing<BoxedUint>,
26    width: usize,
27    domain_bits: (usize, usize),
28}
29
30impl core::fmt::Debug for RustCryptoDhKey {
31    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
32        f.debug_struct("RustCryptoDhKey").finish_non_exhaustive()
33    }
34}
35
36impl RustCryptoDhKey {
37    /// Import unsigned big-endian p/q/g and private x. Validation is charged to
38    /// the supplied operation before allocating big integers or testing primes.
39    /// The provider supplies independent random Miller-Rabin bases; failures are
40    /// propagated, never replaced with deterministic or biased bases.
41    pub fn from_components(
42        provider: &dyn CryptoProvider,
43        budget: &mut KeyEstablishmentBudget<'_>,
44        p: &[u8],
45        q: &[u8],
46        generator: &[u8],
47        private: &[u8],
48    ) -> Result<Self, XmlEncError> {
49        let ceiling = crate::hard_limits::DH_MODULUS_BIT_CEILING / 8;
50        if p.len() > ceiling
51            || q.len() > p.len()
52            || generator.len() > p.len()
53            || private.len() > p.len()
54        {
55            return Err(invalid().into());
56        }
57        let p = significant(p);
58        let q = significant(q);
59        let generator = significant(generator);
60        let p_bits = bits(p);
61        let q_bits = bits(q);
62        // XMLEnc 1.1 §5.6.2 defines p/g minima. Prime-order q and the private
63        // interval follow RFC 2631 §2.2; these checks do not claim validation of
64        // generation seeds or a FIPS-approved parameter-generation procedure.
65        // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-DHKeyAgreement
66        // https://www.rfc-editor.org/rfc/rfc2631.html#section-2.2
67        if p_bits < 512
68            || q_bits < 160
69            || q_bits >= p_bits
70            || bits(generator) < 160
71            || p.last().is_none_or(|v| v & 1 == 0)
72            || q.last().is_none_or(|v| v & 1 == 0)
73        {
74            return Err(invalid().into());
75        }
76        budget.reserve_dh_import(p_bits, q_bits, twos_minus_one(p), twos_minus_one(q))?;
77        let precision = p_bits.div_ceil(64) as u32 * 64;
78        let p_value = decode(p, precision)?;
79        let q_small = decode(q, q_bits.div_ceil(64) as u32 * 64)?;
80        let order = decode(q, precision)?;
81        let one = BoxedUint::one_with_precision(precision);
82        let two = decode(&[2], precision)?;
83        let p_minus_one = p_value.wrapping_sub(&one);
84        let divisor = NonZero::new(order.clone()).ok_or_else(invalid)?;
85        if order >= p_minus_one
86            || p_minus_one.rem_vartime(&divisor) != BoxedUint::zero_with_precision(precision)
87        {
88            return Err(invalid().into());
89        }
90        let generator = decode(generator, precision)?;
91        let private = Zeroizing::new(decode(private, precision)?);
92        if generator <= one
93            || generator >= p_minus_one
94            || *private < two
95            || *private > order.wrapping_sub(&two)
96        {
97            return Err(invalid().into());
98        }
99        probable_prime(|bytes| provider.fill_random(bytes), &p_value)?;
100        probable_prime(|bytes| provider.fill_random(bytes), &q_small)?;
101        let params = BoxedMontyParams::new_vartime(Odd::new(p_value).ok_or_else(invalid)?);
102        if BoxedMontyForm::new(generator.clone(), &params)
103            .pow_bounded_exp(&order, q_bits as u32)
104            .retrieve()
105            != one
106        {
107            return Err(invalid().into());
108        }
109        Ok(Self {
110            params,
111            order,
112            generator,
113            private,
114            width: p.len(),
115            domain_bits: (p_bits, q_bits),
116        })
117    }
118
119    /// Export the public value with the same fixed octet width as p.
120    pub fn public_key(&self) -> Vec<u8> {
121        let value = Zeroizing::new(
122            BoxedMontyForm::new(self.generator.clone(), &self.params).pow(&self.private),
123        );
124        let integer = Zeroizing::new(value.retrieve());
125        let bytes = Zeroizing::new(integer.to_be_bytes());
126        bytes[bytes.len() - self.width..].to_vec()
127    }
128}
129
130impl KeyAgreementKey for RustCryptoDhKey {
131    fn dh_domain_bits(&self) -> Option<(usize, usize)> {
132        Some(self.domain_bits)
133    }
134
135    fn agree(&self, parameters: &KeyAgreementParameters<'_>) -> Result<Vec<u8>, ProviderError> {
136        if !matches!(parameters.algorithm, DH_URI | DH_ES_URI) {
137            return Err(ProviderError::Unsupported {
138                operation: ProviderOperation::KeyAgreement,
139                algorithm: Some(parameters.algorithm.into()),
140            });
141        }
142        if parameters.peer_public_key.is_empty() || parameters.peer_public_key.len() > self.width {
143            return Err(invalid());
144        }
145        let precision = self.generator.bits_precision();
146        let peer = decode(parameters.peer_public_key, precision)?;
147        let one = BoxedUint::one_with_precision(precision);
148        let two = decode(&[2], precision)?;
149        // RFC 2631 §2.1.5 allows this subgroup validation; our policy requires
150        // it before using an untrusted peer with a private exponent. The p-1
151        // endpoint cannot survive this check for the odd prime-order subgroup.
152        // https://www.rfc-editor.org/rfc/rfc2631.html#section-2.1.5
153        if peer < two || peer >= self.params.modulus().as_ref().wrapping_sub(&one) {
154            return Err(invalid());
155        }
156        let peer = BoxedMontyForm::new(peer, &self.params);
157        if peer
158            .pow_bounded_exp(&self.order, self.order.bits_vartime())
159            .retrieve()
160            != one
161        {
162            return Err(invalid());
163        }
164        // RFC 2631 §2.1.2 and XMLEnc 1.1 §5.6.2 require leading zero octets in
165        // ZZ. Never trim the integer or reveal private exponent bit length.
166        // https://www.rfc-editor.org/rfc/rfc2631.html#section-2.1.2
167        let secret = Zeroizing::new(peer.pow(&self.private));
168        let integer = Zeroizing::new(secret.retrieve());
169        let bytes = Zeroizing::new(integer.to_be_bytes());
170        Ok(bytes[bytes.len() - self.width..].to_vec())
171    }
172}
173
174fn invalid() -> ProviderError {
175    ProviderError::InvalidInput(ProviderInputError::DhKey)
176}
177
178fn significant(bytes: &[u8]) -> &[u8] {
179    let offset = bytes.iter().position(|&v| v != 0).unwrap_or(bytes.len());
180    &bytes[offset..]
181}
182
183fn bits(bytes: &[u8]) -> usize {
184    match bytes.first() {
185        Some(first) => bytes.len() * 8 - first.leading_zeros() as usize,
186        None => 0,
187    }
188}
189
190fn twos_minus_one(bytes: &[u8]) -> usize {
191    let last = bytes[bytes.len() - 1] - 1;
192    if last != 0 {
193        return last.trailing_zeros() as usize;
194    }
195    let mut count = 8;
196    for byte in bytes[..bytes.len() - 1].iter().rev() {
197        if *byte != 0 {
198            return count + byte.trailing_zeros() as usize;
199        }
200        count += 8;
201    }
202    count
203}
204
205fn decode(bytes: &[u8], precision: u32) -> Result<BoxedUint, ProviderError> {
206    BoxedUint::from_be_slice(bytes, precision).map_err(|_| invalid())
207}
208
209fn probable_prime(
210    mut fill_random: impl FnMut(&mut [u8]) -> Result<(), ProviderError>,
211    candidate: &BoxedUint,
212) -> Result<(), ProviderError> {
213    let test = MillerRabin::new(Odd::new(candidate.clone()).ok_or_else(invalid)?);
214    let precision = candidate.bits_precision();
215    let two = decode(&[2], precision)?;
216    let maximum = candidate.wrapping_sub(&two);
217    let maximum_bytes = maximum.to_be_bytes();
218    let mut bytes = vec![0; (candidate.bits_vartime() as usize).div_ceil(8)];
219    let maximum_bytes = &maximum_bytes[maximum_bytes.len() - bytes.len()..];
220    let excess = bytes.len() * 8 - candidate.bits_vartime() as usize;
221    // Bounded rejection sampling avoids both RNG-error suppression and modulo
222    // bias. 64 independent uniform bases bound composite acceptance by 2^-128;
223    // this is a probable-prime check, not a primality certificate.
224    for _ in 0..64 {
225        let mut selected = None;
226        for _ in 0..128 {
227            fill_random(&mut bytes)?;
228            bytes[0] &= 0xff >> excess;
229            let value = significant(&bytes);
230            if (value.len() > 1 || value.first().is_some_and(|v| *v >= 2))
231                && bytes.as_slice() <= maximum_bytes
232            {
233                selected = Some(decode(&bytes, precision)?);
234                break;
235            }
236        }
237        let base =
238            selected.ok_or_else(|| ProviderError::Random("DH base sampling exhausted".into()))?;
239        if test.test(&base).is_composite() {
240            return Err(invalid());
241        }
242    }
243    Ok(())
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn primality_randomness_errors_are_terminal() {
252        // Propagate the first RNG failure, never fall back to fixed bases.
253        let candidate = decode(&[251], 64).expect("prime fixture fits one limb");
254        let mut calls = 0;
255        let result = probable_prime(
256            |_| {
257                calls += 1;
258                Err(ProviderError::AuthenticationFailed)
259            },
260            &candidate,
261        );
262        assert!(matches!(result, Err(ProviderError::AuthenticationFailed)));
263        assert_eq!(calls, 1);
264    }
265
266    #[test]
267    fn unusable_randomness_has_a_finite_rejection_bound() {
268        // A stuck RNG terminates; modulo reduction would bias accepted bases.
269        let candidate = decode(&[251], 64).expect("prime fixture fits one limb");
270        let mut calls = 0;
271        let result = probable_prime(
272            |bytes| {
273                calls += 1;
274                bytes.fill(0);
275                Ok(())
276            },
277            &candidate,
278        );
279        assert!(matches!(result, Err(ProviderError::Random(_))));
280        assert_eq!(calls, 128);
281    }
282
283    #[test]
284    fn primality_rejects_composites_and_samples_all_rounds_for_primes() {
285        // 251 is prime; 253 = 11*23, witnessed by base two. This deterministic
286        // test source is not used as a cryptographic RNG in production.
287        for (candidate, expected, count) in [(251, true, 64), (253, false, 1)] {
288            let mut calls = 0;
289            let result = probable_prime(
290                |bytes| {
291                    calls += 1;
292                    bytes.fill(2);
293                    Ok(())
294                },
295                &decode(&[candidate], 64).expect("primality fixtures fit one limb"),
296            );
297            assert_eq!(result.is_ok(), expected);
298            assert_eq!(calls, count);
299        }
300    }
301}