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