Skip to main content

xml_sec/xmldsig/
modern.rs

1//! RustCrypto-owned modern key handles used only behind provider dispatch.
2
3use super::{SignatureAlgorithm, SigningKey, SigningKeyError, SigningPublicKeyInfo};
4use pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey};
5use sha2::digest::Update;
6
7#[expect(
8    clippy::large_enum_variant,
9    reason = "fixed-size inline private keys avoid a second secret allocation and indirection"
10)]
11enum EdDsaPrivateKey {
12    Ed25519(ed25519_dalek::SigningKey),
13    Ed448(ed448_goldilocks::SigningKey),
14}
15
16/// Opaque EdDSA private key. Private bytes are zeroized by the primitive on drop.
17pub struct EdDsaSigningKey(EdDsaPrivateKey);
18
19impl EdDsaSigningKey {
20    /// Import a strict RFC 8410 PKCS#8 key for the explicitly selected algorithm.
21    pub fn from_pkcs8_der(
22        algorithm: SignatureAlgorithm,
23        der: &[u8],
24    ) -> Result<Self, SigningKeyError> {
25        let key = match algorithm.eddsa_key_algorithm() {
26            Some(SignatureAlgorithm::Ed25519) => EdDsaPrivateKey::Ed25519(
27                ed25519_dalek::SigningKey::from_pkcs8_der(der)
28                    .map_err(|_| SigningKeyError::InvalidKeyDer)?,
29            ),
30            Some(SignatureAlgorithm::Ed448) => EdDsaPrivateKey::Ed448(
31                ed448_goldilocks::SigningKey::from_pkcs8_der(der)
32                    .map_err(|_| SigningKeyError::InvalidKeyDer)?,
33            ),
34            _ => {
35                return Err(SigningKeyError::UnsupportedAlgorithm {
36                    uri: algorithm.uri().to_owned(),
37                });
38            }
39        };
40        Ok(Self(key))
41    }
42
43    /// Export private PKCS#8 DER in a zeroizing secret document.
44    pub fn to_pkcs8_der(&self) -> Result<pkcs8::SecretDocument, SigningKeyError> {
45        match &self.0 {
46            EdDsaPrivateKey::Ed25519(key) => key.to_pkcs8_der(),
47            EdDsaPrivateKey::Ed448(key) => key.to_pkcs8_der(),
48        }
49        .map_err(|_| SigningKeyError::InvalidKeyDer)
50    }
51
52    fn algorithm(&self) -> SignatureAlgorithm {
53        match &self.0 {
54            EdDsaPrivateKey::Ed25519(_) => SignatureAlgorithm::Ed25519,
55            EdDsaPrivateKey::Ed448(_) => SignatureAlgorithm::Ed448,
56        }
57    }
58}
59
60impl SigningKey for EdDsaSigningKey {
61    fn sign_with_provider_context(
62        &self,
63        provider: &dyn crate::provider::CryptoProvider,
64        algorithm: SignatureAlgorithm,
65        context: &super::SignatureContext,
66        message: &[u8],
67    ) -> Result<Vec<u8>, SigningKeyError> {
68        if algorithm.eddsa_key_algorithm() != Some(self.algorithm()) {
69            return Err(SigningKeyError::UnsupportedAlgorithm {
70                uri: algorithm.uri().to_owned(),
71            });
72        }
73        match &self.0 {
74            EdDsaPrivateKey::Ed448(key) if algorithm == SignatureAlgorithm::Ed448 => key
75                .sign_ctx(context.as_bytes(), message)
76                .map(|sig| sig.to_bytes().to_vec())
77                .map_err(|_| SigningKeyError::InvalidKeyDer),
78            EdDsaPrivateKey::Ed448(key) => {
79                use ed448_goldilocks::{PreHasherXof, shake::Shake256};
80                key.sign_prehashed::<PreHasherXof<Shake256>>(
81                    Some(context.as_bytes()),
82                    Shake256::default().chain(message).into(),
83                )
84                .map(|sig| sig.to_bytes().to_vec())
85                .map_err(|_| SigningKeyError::InvalidKeyDer)
86            }
87            EdDsaPrivateKey::Ed25519(key) if algorithm == SignatureAlgorithm::Ed25519Ctx => {
88                Ok(sign_ed25519ctx(key, context, message).to_bytes().to_vec())
89            }
90            EdDsaPrivateKey::Ed25519(key) if algorithm == SignatureAlgorithm::Ed25519Ph => {
91                use sha2::Digest;
92                key.sign_prehashed(
93                    sha2::Sha512::new_with_prefix(message),
94                    Some(context.as_bytes()),
95                )
96                .map(|sig| sig.to_bytes().to_vec())
97                .map_err(|_| SigningKeyError::InvalidKeyDer)
98            }
99            EdDsaPrivateKey::Ed25519(_) if context.as_bytes().is_empty() => {
100                self.sign_with_provider(provider, algorithm, message)
101            }
102            _ => Err(SigningKeyError::UnsupportedAlgorithm {
103                uri: algorithm.uri().to_owned(),
104            }),
105        }
106    }
107    fn sign(
108        &self,
109        algorithm: SignatureAlgorithm,
110        message: &[u8],
111    ) -> Result<Vec<u8>, SigningKeyError> {
112        crate::provider::default_provider().sign(self, algorithm, message)
113    }
114
115    fn sign_with_provider(
116        &self,
117        _provider: &dyn crate::provider::CryptoProvider,
118        algorithm: SignatureAlgorithm,
119        message: &[u8],
120    ) -> Result<Vec<u8>, SigningKeyError> {
121        if algorithm != self.algorithm() {
122            return self.sign_with_provider_context(
123                _provider,
124                algorithm,
125                &super::SignatureContext::default(),
126                message,
127            );
128        }
129        // RFC 8032 sections 5.1.6 and 5.2.6 hash internally with different
130        // domain separation: https://www.rfc-editor.org/rfc/rfc8032.html#section-5
131        // This callback is the opaque key primitive, invoked by CryptoProvider.
132        Ok(match &self.0 {
133            EdDsaPrivateKey::Ed25519(key) => {
134                use signature::Signer;
135                key.sign(message).to_bytes().to_vec()
136            }
137            EdDsaPrivateKey::Ed448(key) => key.sign_raw(message).to_bytes().to_vec(),
138        })
139    }
140
141    fn public_key_info(&self) -> Result<SigningPublicKeyInfo, SigningKeyError> {
142        let public = match &self.0 {
143            EdDsaPrivateKey::Ed25519(key) => key.verifying_key().to_public_key_der(),
144            EdDsaPrivateKey::Ed448(key) => key.verifying_key().to_public_key_der(),
145        }
146        .map_err(|_| SigningKeyError::PublicKeyEncodingFailed)?;
147        Ok(SigningPublicKeyInfo::EdDsa {
148            algorithm: self.algorithm(),
149            spki_der: public.into_vec(),
150        })
151    }
152}
153
154pub(crate) fn validate_public_key(
155    algorithm: SignatureAlgorithm,
156    der: &[u8],
157) -> Result<(), super::SignatureVerificationError> {
158    if let SignatureAlgorithm::PostQuantum(algorithm) = algorithm {
159        #[cfg(feature = "experimental-pq")]
160        {
161            return super::post_quantum::validate_public_key(algorithm, der);
162        }
163        #[cfg(not(feature = "experimental-pq"))]
164        {
165            return Err(super::SignatureVerificationError::UnsupportedAlgorithm {
166                uri: algorithm.uri().to_owned(),
167            });
168        }
169    }
170    match algorithm.eddsa_key_algorithm() {
171        Some(SignatureAlgorithm::Ed25519) => {
172            ed25519_dalek::VerifyingKey::from_public_key_der(der).map(|_| ())
173        }
174        Some(SignatureAlgorithm::Ed448) => {
175            ed448_goldilocks::VerifyingKey::from_public_key_der(der).map(|_| ())
176        }
177        _ => {
178            return Err(super::SignatureVerificationError::UnsupportedAlgorithm {
179                uri: algorithm.uri().to_owned(),
180            });
181        }
182    }
183    .map_err(|_| super::SignatureVerificationError::InvalidKeyDer)
184}
185
186pub(crate) fn verify(
187    algorithm: SignatureAlgorithm,
188    der: &[u8],
189    message: &[u8],
190    signature: &[u8],
191) -> Result<bool, super::SignatureVerificationError> {
192    verify_with_context(
193        algorithm,
194        der,
195        &super::SignatureContext::default(),
196        message,
197        signature,
198    )
199}
200
201pub(crate) fn verify_with_context(
202    algorithm: SignatureAlgorithm,
203    der: &[u8],
204    context: &super::SignatureContext,
205    message: &[u8],
206    signature: &[u8],
207) -> Result<bool, super::SignatureVerificationError> {
208    use super::SignatureVerificationError as Error;
209    if let SignatureAlgorithm::PostQuantum(algorithm) = algorithm {
210        #[cfg(feature = "experimental-pq")]
211        {
212            return super::post_quantum::verify(algorithm, der, context, message, signature);
213        }
214        #[cfg(not(feature = "experimental-pq"))]
215        {
216            return Err(Error::UnsupportedAlgorithm {
217                uri: algorithm.uri().to_owned(),
218            });
219        }
220    }
221    match algorithm {
222        SignatureAlgorithm::Ed25519
223        | SignatureAlgorithm::Ed25519Ctx
224        | SignatureAlgorithm::Ed25519Ph => {
225            let key = ed25519_dalek::VerifyingKey::from_public_key_der(der)
226                .map_err(|_| Error::InvalidKeyDer)?;
227            let signature = ed25519_dalek::Signature::try_from(signature)
228                .map_err(|_| Error::InvalidSignatureFormat)?;
229            Ok(match algorithm {
230                SignatureAlgorithm::Ed25519 if context.as_bytes().is_empty() => {
231                    key.verify_strict(message, &signature).is_ok()
232                }
233                SignatureAlgorithm::Ed25519Ctx => {
234                    verify_ed25519ctx(&key, context, message, &signature)
235                }
236                SignatureAlgorithm::Ed25519Ph => {
237                    use sha2::Digest;
238                    key.verify_prehashed_strict(
239                        sha2::Sha512::new_with_prefix(message),
240                        Some(context.as_bytes()),
241                        &signature,
242                    )
243                    .is_ok()
244                }
245                _ => {
246                    return Err(Error::UnsupportedAlgorithm {
247                        uri: algorithm.uri().to_owned(),
248                    });
249                }
250            })
251        }
252        SignatureAlgorithm::Ed448 | SignatureAlgorithm::Ed448Ph => {
253            let key = ed448_goldilocks::VerifyingKey::from_public_key_der(der)
254                .map_err(|_| Error::InvalidKeyDer)?;
255            let signature = ed448_goldilocks::Signature::try_from(signature)
256                .map_err(|_| Error::InvalidSignatureFormat)?;
257            Ok(if algorithm == SignatureAlgorithm::Ed448 {
258                key.verify_ctx(&signature, context.as_bytes(), message)
259                    .is_ok()
260            } else {
261                use ed448_goldilocks::{PreHasherXof, shake::Shake256};
262                key.verify_prehashed::<PreHasherXof<Shake256>>(
263                    &signature,
264                    Some(context.as_bytes()),
265                    Shake256::default().chain(message).into(),
266                )
267                .is_ok()
268            })
269        }
270        _ => Err(Error::UnsupportedAlgorithm {
271            uri: algorithm.uri().to_owned(),
272        }),
273    }
274}
275
276fn ed25519ctx_hash(context: &super::SignatureContext) -> sha2::Sha512 {
277    use sha2::Digest;
278    // RFC 8032 sections 2 and 5.1.6: dom2(0, C) precedes both nonce
279    // and challenge hashes, never the private-key expansion hash.
280    // https://www.rfc-editor.org/rfc/rfc8032.html#section-5.1.6
281    sha2::Sha512::new_with_prefix(b"SigEd25519 no Ed25519 collisions")
282        .chain_update([0, context.as_bytes().len() as u8])
283        .chain_update(context.as_bytes())
284}
285
286fn sign_ed25519ctx(
287    key: &ed25519_dalek::SigningKey,
288    context: &super::SignatureContext,
289    message: &[u8],
290) -> ed25519_dalek::Signature {
291    use curve25519_dalek::{edwards::EdwardsPoint, scalar::Scalar};
292    use sha2::Digest;
293    use zeroize::Zeroizing;
294    let expansion = Zeroizing::new(<[u8; 64]>::from(sha2::Sha512::digest(key.as_bytes())));
295    let secret = ed25519_dalek::hazmat::ExpandedSecretKey::from_bytes(&expansion);
296    let nonce_hash = Zeroizing::new(<[u8; 64]>::from(
297        ed25519ctx_hash(context)
298            .chain_update(secret.hash_prefix)
299            .chain_update(message)
300            .finalize(),
301    ));
302    let nonce = Zeroizing::new(Scalar::from_bytes_mod_order_wide(&nonce_hash));
303    let r = EdwardsPoint::mul_base(&nonce).compress().to_bytes();
304    let challenge = Scalar::from_bytes_mod_order_wide(
305        &ed25519ctx_hash(context)
306            .chain_update(r)
307            .chain_update(key.verifying_key().as_bytes())
308            .chain_update(message)
309            .finalize()
310            .into(),
311    );
312    let s = Zeroizing::new(*nonce + challenge * secret.scalar);
313    ed25519_dalek::Signature::from_components(r, s.to_bytes())
314}
315
316fn verify_ed25519ctx(
317    key: &ed25519_dalek::VerifyingKey,
318    context: &super::SignatureContext,
319    message: &[u8],
320    signature: &ed25519_dalek::Signature,
321) -> bool {
322    use curve25519_dalek::{
323        edwards::{CompressedEdwardsY, EdwardsPoint},
324        scalar::Scalar,
325    };
326    use sha2::Digest;
327    let Some(s) = Option::<Scalar>::from(Scalar::from_canonical_bytes(*signature.s_bytes())) else {
328        return false;
329    };
330    let encoded_r = CompressedEdwardsY(*signature.r_bytes());
331    let Some(r) = encoded_r.decompress() else {
332        return false;
333    };
334    // RFC 8032 section 5.1.7 permits the uncofactored equation. Additionally
335    // reject small-order points, matching the existing strict Ed25519 key API.
336    // https://www.rfc-editor.org/rfc/rfc8032.html#section-5.1.7
337    if key.is_weak() || r.is_small_order() || r.compress() != encoded_r {
338        return false;
339    }
340    let challenge = Scalar::from_bytes_mod_order_wide(
341        &ed25519ctx_hash(context)
342            .chain_update(signature.r_bytes())
343            .chain_update(key.as_bytes())
344            .chain_update(message)
345            .finalize()
346            .into(),
347    );
348    EdwardsPoint::vartime_double_scalar_mul_basepoint(&challenge, &(-key.to_edwards()), &s)
349        .compress()
350        == encoded_r
351}