Skip to main content

xml_sec/provider/
key_establishment.rs

1//! RustCrypto primitives. XML syntax, deployment policy and cumulative operation
2//! accounting remain in the caller; primitive protocol bounds are checked here.
3
4mod bit_hash;
5mod dh;
6pub use dh::RustCryptoDhKey;
7
8use crate::xmldsig::{DigestAlgorithm, SignatureAlgorithm};
9use p256::elliptic_curve::sec1::ToSec1Point;
10
11use super::{
12    KdfContext, KdfParameters, KeyAgreementKey, KeyAgreementParameters, ProviderError,
13    ProviderInputError, ProviderOperation,
14};
15
16pub(super) const X25519_URI: &str = "http://www.w3.org/2021/04/xmldsig-more#x25519";
17pub(super) const ECDH_URI: &str = "http://www.w3.org/2009/xmlenc11#ECDH-ES";
18pub(super) const DH_ES_URI: &str = "http://www.w3.org/2009/xmlenc11#dh-es";
19pub(super) const DH_URI: &str = "http://www.w3.org/2001/04/xmlenc#dh";
20const HKDF_URI: &str = "http://www.w3.org/2021/04/xmldsig-more#hkdf";
21const PBKDF2_URI: &str = "http://www.w3.org/2009/xmlenc11#pbkdf2";
22const CONCAT_KDF_URI: &str = "http://www.w3.org/2009/xmlenc11#ConcatKDF";
23const LEGACY_DH_URI: &str = "http://www.w3.org/2001/04/xmlenc#dh";
24
25/// Named curve used by an ECDH-ES private key. The peer must use this same curve.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum EcdhCurve {
28    /// NIST P-256 (32-byte scalar and shared secret).
29    P256,
30    /// NIST P-384 (48-byte scalar and shared secret).
31    P384,
32    /// NIST P-521 (66-byte scalar and shared secret).
33    P521,
34}
35
36enum EcdhSecret {
37    P256(p256::SecretKey),
38    P384(p384::SecretKey),
39    P521(p521::SecretKey),
40}
41
42/// Opaque RustCrypto ECDH key with a fixed named-curve domain.
43pub struct RustCryptoEcdhKey(EcdhSecret);
44
45impl core::fmt::Debug for RustCryptoEcdhKey {
46    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
47        formatter
48            .debug_struct("RustCryptoEcdhKey")
49            .finish_non_exhaustive()
50    }
51}
52
53impl RustCryptoEcdhKey {
54    /// Import a fixed-width big-endian private scalar. Zero and out-of-range
55    /// values are rejected, not reduced modulo the curve order.
56    pub fn from_scalar(curve: EcdhCurve, bytes: &[u8]) -> Result<Self, ProviderError> {
57        let width = match curve {
58            EcdhCurve::P256 => 32,
59            EcdhCurve::P384 => 48,
60            EcdhCurve::P521 => 66,
61        };
62        if bytes.len() != width {
63            return Err(ProviderError::InvalidKeySize {
64                expected: width,
65                actual: bytes.len(),
66            });
67        }
68        let invalid = |_| ProviderError::InvalidInput(ProviderInputError::EcdhKey);
69        Ok(Self(match curve {
70            EcdhCurve::P256 => {
71                EcdhSecret::P256(p256::SecretKey::from_slice(bytes).map_err(invalid)?)
72            }
73            EcdhCurve::P384 => {
74                EcdhSecret::P384(p384::SecretKey::from_slice(bytes).map_err(invalid)?)
75            }
76            EcdhCurve::P521 => {
77                EcdhSecret::P521(p521::SecretKey::from_slice(bytes).map_err(invalid)?)
78            }
79        }))
80    }
81
82    /// Export the public SEC1 uncompressed point, never private material.
83    pub fn public_key(&self) -> Vec<u8> {
84        match &self.0 {
85            EcdhSecret::P256(key) => key.public_key().to_sec1_point(false).as_bytes().to_vec(),
86            EcdhSecret::P384(key) => key.public_key().to_sec1_point(false).as_bytes().to_vec(),
87            EcdhSecret::P521(key) => key.public_key().to_sec1_point(false).as_bytes().to_vec(),
88        }
89    }
90}
91
92impl KeyAgreementKey for RustCryptoEcdhKey {
93    fn agree(&self, parameters: &KeyAgreementParameters<'_>) -> Result<Vec<u8>, ProviderError> {
94        if parameters.algorithm != ECDH_URI {
95            return Err(ProviderError::Unsupported {
96                operation: ProviderOperation::KeyAgreement,
97                algorithm: Some(parameters.algorithm.to_owned()),
98            });
99        }
100        macro_rules! agree {
101            ($key:expr, $public:ty) => {{
102                // XMLEnc 1.1 section 5.6.4 requires a validated peer and fixed-width
103                // shared-secret octets. The named-curve parser rejects infinity and
104                // off-curve points before multiplication; do not integer-trim ZZ.
105                // https://www.w3.org/TR/xmlenc-core1/#sec-ECDH-ES
106                let peer = <$public>::from_sec1_bytes(parameters.peer_public_key)
107                    .map_err(|_| ProviderError::InvalidInput(ProviderInputError::EcdhKey))?;
108                let shared = $key.diffie_hellman(&peer);
109                Ok(shared.raw_secret_bytes().to_vec())
110            }};
111        }
112        match &self.0 {
113            EcdhSecret::P256(key) => agree!(key, p256::PublicKey),
114            EcdhSecret::P384(key) => agree!(key, p384::PublicKey),
115            EcdhSecret::P521(key) => agree!(key, p521::PublicKey),
116        }
117    }
118}
119
120/// Opaque RustCrypto X25519 private key. Its scalar is zeroized on drop and is
121/// never exposed through Debug or a byte-export accessor.
122pub struct RustCryptoX25519Key(x25519_dalek::StaticSecret);
123
124impl core::fmt::Debug for RustCryptoX25519Key {
125    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
126        formatter
127            .debug_struct("RustCryptoX25519Key")
128            .finish_non_exhaustive()
129    }
130}
131
132impl RustCryptoX25519Key {
133    /// Import the 32-byte scalar encoding; clamping follows RFC 7748 section 5.
134    pub fn from_bytes(bytes: [u8; 32]) -> Self {
135        Self(x25519_dalek::StaticSecret::from(bytes))
136    }
137
138    /// Return the public u-coordinate encoding defined by RFC 7748 section 5.
139    pub fn public_key(&self) -> [u8; 32] {
140        x25519_dalek::PublicKey::from(&self.0).to_bytes()
141    }
142}
143
144impl KeyAgreementKey for RustCryptoX25519Key {
145    fn agree(&self, parameters: &KeyAgreementParameters<'_>) -> Result<Vec<u8>, ProviderError> {
146        if parameters.algorithm != X25519_URI {
147            return Err(ProviderError::Unsupported {
148                operation: ProviderOperation::KeyAgreement,
149                algorithm: Some(parameters.algorithm.to_owned()),
150            });
151        }
152        let bytes: [u8; 32] =
153            parameters
154                .peer_public_key
155                .try_into()
156                .map_err(|_| ProviderError::InvalidKeySize {
157                    expected: 32,
158                    actual: parameters.peer_public_key.len(),
159                })?;
160        let peer = x25519_dalek::PublicKey::from(bytes);
161        let secret = self.0.diffie_hellman(&peer);
162        // RFC 7748 section 6.1 permits the all-zero check. Our key-establishment
163        // contract requires contributory behavior, rather than deriving a key
164        // from a secret an untrusted low-order peer can force to zero.
165        // https://www.rfc-editor.org/rfc/rfc7748.html#section-6.1
166        if !secret.was_contributory() {
167            return Err(ProviderError::AuthenticationFailed);
168        }
169        Ok(secret.as_bytes().to_vec())
170    }
171}
172
173pub(super) fn supports_kdf(parameters: &KdfParameters<'_>) -> bool {
174    match parameters.algorithm {
175        HKDF_URI | PBKDF2_URI => hmac_prf(parameters).is_some(),
176        CONCAT_KDF_URI | LEGACY_DH_URI => concat_digest(parameters).is_some(),
177        _ => false,
178    }
179}
180
181fn hmac_prf(parameters: &KdfParameters<'_>) -> Option<SignatureAlgorithm> {
182    let algorithm = SignatureAlgorithm::from_uri(parameters.digest?)?;
183    match algorithm {
184        SignatureAlgorithm::HmacSha1
185        | SignatureAlgorithm::HmacSha224
186        | SignatureAlgorithm::HmacSha256
187        | SignatureAlgorithm::HmacSha384
188        | SignatureAlgorithm::HmacSha512 => Some(algorithm),
189        _ => None,
190    }
191}
192
193pub(super) fn derive_key(
194    parameters: &KdfParameters<'_>,
195    secret: &[u8],
196) -> Result<Vec<u8>, ProviderError> {
197    if parameters.algorithm == LEGACY_DH_URI {
198        return derive_legacy_dh(parameters, secret);
199    }
200    if parameters.algorithm == CONCAT_KDF_URI {
201        return derive_concat(parameters, secret);
202    }
203    if parameters.algorithm == PBKDF2_URI {
204        return derive_pbkdf2(parameters, secret);
205    }
206    if parameters.algorithm != HKDF_URI {
207        return Err(ProviderError::Unsupported {
208            operation: ProviderOperation::Kdf,
209            algorithm: Some(parameters.algorithm.to_owned()),
210        });
211    }
212    if parameters.iterations != 0 {
213        return Err(ProviderError::InvalidInput(
214            ProviderInputError::HkdfParameters,
215        ));
216    }
217    let info = context_octets(parameters.info).ok_or(ProviderError::InvalidInput(
218        ProviderInputError::HkdfParameters,
219    ))?;
220    macro_rules! derive {
221        ($digest:ty, $width:expr) => {{
222            // RFC 5869 section 2.3: L <= 255 * HashLen. Check before allocating;
223            // XML policy may further restrict this immutable primitive bound.
224            // https://www.rfc-editor.org/rfc/rfc5869.html#section-2.3
225            if parameters.output_len > 255 * $width {
226                return Err(ProviderError::InvalidInput(
227                    ProviderInputError::HkdfParameters,
228                ));
229            }
230            let kdf = hkdf::Hkdf::<$digest>::new(Some(parameters.salt), secret);
231            let mut output = zeroize::Zeroizing::new(vec![0; parameters.output_len]);
232            kdf.expand(info, &mut output)
233                .map_err(|_| ProviderError::InvalidInput(ProviderInputError::HkdfParameters))?;
234            Ok(core::mem::take(&mut *output))
235        }};
236    }
237    match hmac_prf(parameters) {
238        Some(SignatureAlgorithm::HmacSha1) => derive!(sha1::Sha1, 20),
239        Some(SignatureAlgorithm::HmacSha224) => derive!(sha2::Sha224, 28),
240        Some(SignatureAlgorithm::HmacSha256) => derive!(sha2::Sha256, 32),
241        Some(SignatureAlgorithm::HmacSha384) => derive!(sha2::Sha384, 48),
242        Some(SignatureAlgorithm::HmacSha512) => derive!(sha2::Sha512, 64),
243        _ => Err(ProviderError::Unsupported {
244            operation: ProviderOperation::Kdf,
245            algorithm: Some(parameters.algorithm.to_owned()),
246        }),
247    }
248}
249
250fn derive_pbkdf2(parameters: &KdfParameters<'_>, secret: &[u8]) -> Result<Vec<u8>, ProviderError> {
251    use hmac::Mac;
252    use zeroize::Zeroize;
253
254    if parameters.iterations == 0
255        || parameters.output_len == 0
256        || context_octets(parameters.info) != Some(&[][..])
257    {
258        return Err(ProviderError::InvalidInput(
259            ProviderInputError::Pbkdf2Parameters,
260        ));
261    }
262    macro_rules! derive {
263        ($digest:ty, $width:expr) => {{
264            // RFC 8018 section 5.2: dkLen <= (2^32 - 1) * hLen and the block
265            // index is big-endian. Validate before allocation or PRF work.
266            // https://www.rfc-editor.org/rfc/rfc8018.html#section-5.2
267            let blocks = parameters.output_len.div_ceil($width);
268            if u32::try_from(blocks).is_err() {
269                return Err(ProviderError::InvalidInput(
270                    ProviderInputError::Pbkdf2Parameters,
271                ));
272            }
273            let prf = <hmac::Hmac<$digest> as hmac::KeyInit>::new_from_slice(secret)
274                .map_err(|_| ProviderError::InvalidInput(ProviderInputError::Pbkdf2Parameters))?;
275            let mut salted = prf.clone();
276            salted.update(parameters.salt);
277            let mut output = zeroize::Zeroizing::new(vec![0; parameters.output_len]);
278            // The public parameter is u64: do not silently truncate it to the
279            // u32 counter accepted by the existing pbkdf2 convenience function.
280            // Only U_j/T_i live here; RustCrypto supplies every HMAC operation.
281            for (index, chunk) in output.chunks_mut($width).enumerate() {
282                let counter = u32::try_from(index + 1).map_err(|_| {
283                    ProviderError::InvalidInput(ProviderInputError::Pbkdf2Parameters)
284                })?;
285                let mut first = salted.clone();
286                first.update(&counter.to_be_bytes());
287                let mut previous = first.finalize().into_bytes();
288                chunk.copy_from_slice(&previous[..chunk.len()]);
289                for _ in 1..parameters.iterations {
290                    let mut next = prf.clone();
291                    next.update(&previous);
292                    previous.zeroize();
293                    previous = next.finalize().into_bytes();
294                    for (byte, value) in chunk.iter_mut().zip(previous.iter()) {
295                        *byte ^= value;
296                    }
297                }
298                previous.zeroize();
299            }
300            Ok(core::mem::take(&mut *output))
301        }};
302    }
303    match hmac_prf(parameters) {
304        Some(SignatureAlgorithm::HmacSha1) => derive!(sha1::Sha1, 20),
305        Some(SignatureAlgorithm::HmacSha224) => derive!(sha2::Sha224, 28),
306        Some(SignatureAlgorithm::HmacSha256) => derive!(sha2::Sha256, 32),
307        Some(SignatureAlgorithm::HmacSha384) => derive!(sha2::Sha384, 48),
308        Some(SignatureAlgorithm::HmacSha512) => derive!(sha2::Sha512, 64),
309        _ => Err(ProviderError::Unsupported {
310            operation: ProviderOperation::Kdf,
311            algorithm: Some(parameters.algorithm.to_owned()),
312        }),
313    }
314}
315
316fn context_octets(context: KdfContext<'_>) -> Option<&[u8]> {
317    match context {
318        KdfContext::Octets(bytes) => Some(bytes),
319        KdfContext::Bits { bytes, bit_len }
320            if bit_len.is_multiple_of(8) && bit_len / 8 == bytes.len() =>
321        {
322            Some(bytes)
323        }
324        KdfContext::Bits { .. } | KdfContext::LegacyDh { .. } => None,
325    }
326}
327
328fn concat_digest(parameters: &KdfParameters<'_>) -> Option<DigestAlgorithm> {
329    match DigestAlgorithm::from_uri(parameters.digest?)? {
330        algorithm @ (DigestAlgorithm::Sha1
331        | DigestAlgorithm::Sha224
332        | DigestAlgorithm::Sha256
333        | DigestAlgorithm::Sha384
334        | DigestAlgorithm::Sha512) => Some(algorithm),
335        _ => None,
336    }
337}
338
339fn derive_concat(parameters: &KdfParameters<'_>, secret: &[u8]) -> Result<Vec<u8>, ProviderError> {
340    let invalid = || ProviderError::InvalidInput(ProviderInputError::ConcatKdfParameters);
341    if parameters.iterations != 0 || !parameters.salt.is_empty() || parameters.output_len == 0 {
342        return Err(invalid());
343    }
344    let (info, bit_len) = match parameters.info {
345        KdfContext::Octets(bytes) => (bytes, bytes.len().checked_mul(8).ok_or_else(invalid)?),
346        KdfContext::Bits { bytes, bit_len } => (bytes, bit_len),
347        KdfContext::LegacyDh { .. } => return Err(invalid()),
348    };
349    if bit_len.div_ceil(8) != info.len() {
350        return Err(invalid());
351    }
352    let used_bits = bit_len % 8;
353    if used_bits != 0 && info[info.len() - 1] & (0xff >> used_bits) != 0 {
354        return Err(invalid());
355    }
356    let algorithm = concat_digest(parameters).ok_or_else(|| ProviderError::Unsupported {
357        operation: ProviderOperation::Kdf,
358        algorithm: Some(parameters.algorithm.to_owned()),
359    })?;
360    let width = algorithm.output_len();
361    if u32::try_from(parameters.output_len.div_ceil(width)).is_err() {
362        return Err(invalid());
363    }
364    let total_bits = 32 + secret.len() as u128 * 8 + bit_len as u128;
365    if width <= 32 && total_bits > u64::MAX as u128 {
366        return Err(invalid());
367    }
368    // XMLEnc 1.1 section 5.4.1 specifies a big-endian counter and unpadded
369    // OtherInfo bits. The bit-oriented path shares RustCrypto compression and
370    // never allocates a counter || secret || context concatenation per block.
371    // https://www.w3.org/TR/xmlenc-core1/#sec-ConcatKDF
372    let mut output = zeroize::Zeroizing::new(vec![0; parameters.output_len]);
373    for (index, chunk) in output.chunks_mut(width).enumerate() {
374        let counter = u32::try_from(index + 1)
375            .map_err(|_| invalid())?
376            .to_be_bytes();
377        let block = bit_hash::hash(algorithm, &[&counter, secret], info, bit_len)?;
378        chunk.copy_from_slice(&block[..chunk.len()]);
379    }
380    Ok(core::mem::take(&mut *output))
381}
382
383fn derive_legacy_dh(
384    parameters: &KdfParameters<'_>,
385    secret: &[u8],
386) -> Result<Vec<u8>, ProviderError> {
387    let invalid = || ProviderError::InvalidInput(ProviderInputError::LegacyDhKdfParameters);
388    let KdfContext::LegacyDh {
389        encryption_algorithm,
390        nonce,
391    } = parameters.info
392    else {
393        return Err(invalid());
394    };
395    if parameters.iterations != 0 || !parameters.salt.is_empty() || parameters.output_len == 0 {
396        return Err(invalid());
397    }
398    let algorithm = concat_digest(parameters).ok_or_else(|| ProviderError::Unsupported {
399        operation: ProviderOperation::Kdf,
400        algorithm: Some(parameters.algorithm.to_owned()),
401    })?;
402    let width = algorithm.output_len();
403    if parameters.output_len.div_ceil(width) > u8::MAX as usize {
404        return Err(invalid());
405    }
406    let mut key_bits = parameters.output_len.checked_mul(8).ok_or_else(invalid)?;
407    // usize has at most 39 decimal digits on supported 128-bit platforms.
408    // Format directly on the stack rather than allocating per hash block.
409    let mut decimal = [0u8; 40];
410    let mut start = decimal.len();
411    while key_bits != 0 {
412        start -= 1;
413        decimal[start] = b'0' + (key_bits % 10) as u8;
414        key_bits /= 10;
415    }
416    let total_bytes = secret.len() as u128
417        + 2
418        + encryption_algorithm.len() as u128
419        + nonce.len() as u128
420        + (decimal.len() - start) as u128;
421    if width <= 32 && total_bytes * 8 > u64::MAX as u128 {
422        return Err(invalid());
423    }
424    // XMLEnc 1.1 section 5.6.2.2 uses a ONE-BYTE counter encoded as two
425    // uppercase ASCII hex digits and an unpadded decimal KeySize in bits.
426    // This is neither ConcatKDF's BE32 counter nor the agreement algorithm URI.
427    // https://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/#sec-DHKeyAgreementLegacyKDF
428    const HEX: &[u8; 16] = b"0123456789ABCDEF";
429    let mut output = zeroize::Zeroizing::new(vec![0; parameters.output_len]);
430    for (index, chunk) in output.chunks_mut(width).enumerate() {
431        let counter = index + 1;
432        let ascii = [HEX[counter >> 4], HEX[counter & 15]];
433        let hash = bit_hash::hash(
434            algorithm,
435            &[
436                secret,
437                &ascii,
438                encryption_algorithm.as_bytes(),
439                nonce,
440                &decimal[start..],
441            ],
442            &[],
443            0,
444        )?;
445        chunk.copy_from_slice(&hash[..chunk.len()]);
446    }
447    Ok(core::mem::take(&mut *output))
448}