From 3138dd20e3bccab26405abf76711f27c4471887f Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Sat, 10 Jan 2026 14:09:23 -0700 Subject: [PATCH] ml-dsa: rename and deprecate `ExpandedSigningKey` (#1145) Renames `EncodedSigningKey` to `ExpandedSigningKey` to contrast it with seeds, the preferred API. Also renames the `decode` and `encode` functions to `from_expanded` and `to_expanded`, similar to the changes made to `ml-kem`'s `DecapsulationKey` in RustCrypto/KEMs#163, which also deprecated these APIs. We don't properly implement validation of such keys which can lead to panics in the event they were improperly or maliciously generated (#1133), a problem avoided by using seeds. For now, this merely documents the panic condition. Most implementers have opted not to provide support for this key format due to these problems (it's also actually more expensive to validate an expanded key than it is to use a seed), and also where seeds are the same size regardless of security level, the expanded keys vary in size. --- ml-dsa/benches/ml_dsa.rs | 7 +-- ml-dsa/src/lib.rs | 103 +++++++++++++++++++++++---------------- ml-dsa/src/param.rs | 10 ++-- ml-dsa/tests/key-gen.rs | 12 +++-- ml-dsa/tests/sig-gen.rs | 6 ++- 5 files changed, 80 insertions(+), 58 deletions(-) diff --git a/ml-dsa/benches/ml_dsa.rs b/ml-dsa/benches/ml_dsa.rs index 9ccce31..bc06f97 100644 --- a/ml-dsa/benches/ml_dsa.rs +++ b/ml-dsa/benches/ml_dsa.rs @@ -9,6 +9,7 @@ pub fn rand(rng: &mut R) -> Array { val } +#[allow(deprecated)] // TODO(tarcieri): stop using expanded signing keys fn criterion_benchmark(c: &mut Criterion) { let mut rng = getrandom::SysRng.unwrap_err(); let xi: B32 = rand(&mut rng); @@ -20,7 +21,7 @@ fn criterion_benchmark(c: &mut Criterion) { let vk = kp.verifying_key(); let sig = sk.sign_deterministic(&m, &ctx).unwrap(); - let sk_bytes = sk.encode(); + let sk_bytes = sk.to_expanded(); let vk_bytes = vk.encode(); let sig_bytes = sig.encode(); @@ -28,7 +29,7 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("keygen", |b| { b.iter(|| { let kp = MlDsa65::from_seed(&xi); - let _sk_bytes = kp.signing_key().encode(); + let _sk_bytes = kp.signing_key().to_expanded(); let _vk_bytes = kp.verifying_key().encode(); }) }); @@ -36,7 +37,7 @@ fn criterion_benchmark(c: &mut Criterion) { // Signing c.bench_function("sign", |b| { b.iter(|| { - let sk = SigningKey::::decode(&sk_bytes); + let sk = SigningKey::::from_expanded(&sk_bytes); let _sig = sk.sign_deterministic(&m, &ctx); }) }); diff --git a/ml-dsa/src/lib.rs b/ml-dsa/src/lib.rs index 1d04607..3069c7b 100644 --- a/ml-dsa/src/lib.rs +++ b/ml-dsa/src/lib.rs @@ -78,7 +78,7 @@ use crate::sampling::{expand_a, expand_mask, expand_s, sample_in_ball}; use crate::util::B64; use core::fmt; -pub use crate::param::{EncodedSignature, EncodedSigningKey, EncodedVerifyingKey, MlDsaParams}; +pub use crate::param::{EncodedSignature, EncodedVerifyingKey, ExpandedSigningKey, MlDsaParams}; pub use crate::util::B32; pub use signature::{self, Error}; @@ -488,28 +488,37 @@ impl SigningKey

{ Ok(self.sign_mu_deterministic(&mu)) } - /// Encode the key in a fixed-size byte array. - // Algorithm 24 skEncode - pub fn encode(&self) -> EncodedSigningKey

- where - P: MlDsaParams, - { - let s1_enc = P::encode_s1(&self.s1); - let s2_enc = P::encode_s2(&self.s2); - let t0_enc = P::encode_t0(&self.t0); - P::concat_sk( - self.rho.clone(), - self.K.clone(), - self.tr.clone(), - s1_enc, - s2_enc, - t0_enc, - ) + /// This auxiliary function derives a `VerifyingKey` from a bare + /// `SigningKey` (even in the absence of the original seed). + /// + /// This is a utility function that is useful when importing the private key + /// from an external source which does not export the seed and does not + /// provide the precomputed public key associated with the private key + /// itself. + /// + /// `SigningKey` implements `signature::Keypair`: this inherent method is + /// retained for convenience, so it is available for callers even when the + /// `signature::Keypair` trait is out-of-scope. + pub fn verifying_key(&self) -> VerifyingKey

{ + let kp: &dyn signature::Keypair> = self; + kp.verifying_key() } - /// Decode the key from an appropriately sized byte array. + /// DEPRECATED: decode the key from an appropriately sized byte array. + /// + /// Note that this form is deprecated in practice; prefer to use [`SigningKey::from_seed`]. + /// + ///

+ /// Panics + /// + /// This API does not validate expanded signing keys and can potentially panic if keys are + /// malformed or maliciously generated. + /// + /// To avoid panics, use [`SigningKey::from_seed`] instead. + ///
// Algorithm 25 skDecode - pub fn decode(enc: &EncodedSigningKey

) -> Self + #[deprecated(since = "0.1.0", note = "use `SigningKey::from_seed` instead")] + pub fn from_expanded(enc: &ExpandedSigningKey

) -> Self where P: MlDsaParams, { @@ -525,21 +534,26 @@ impl SigningKey

{ ) } - /// This auxiliary function derives a `VerifyingKey` from a bare - /// `SigningKey` (even in the absence of the original seed). + /// DEPRECATED: encode the key in a fixed-size byte array. /// - /// This is a utility function that is useful when importing the private key - /// from an external source which does not export the seed and does not - /// provide the precomputed public key associated with the private key - /// itself. - /// - /// `SigningKey` implements `signature::Keypair`: this inherent method is - /// retained for convenience, so it is available for callers even when the - /// `signature::Keypair` trait is out-of-scope. - pub fn verifying_key(&self) -> VerifyingKey

{ - let kp: &dyn signature::Keypair> = self; - - kp.verifying_key() + /// Note that this form is deprecated in practice; prefer to use [`KeyPair::to_seed`]. + // Algorithm 24 skEncode + #[deprecated(since = "0.1.0", note = "use `KeyPair::to_seed` instead")] + pub fn to_expanded(&self) -> ExpandedSigningKey

+ where + P: MlDsaParams, + { + let s1_enc = P::encode_s1(&self.s1); + let s2_enc = P::encode_s2(&self.s2); + let t0_enc = P::encode_t0(&self.t0); + P::concat_sk( + self.rho.clone(), + self.K.clone(), + self.tr.clone(), + s1_enc, + s2_enc, + t0_enc, + ) } } @@ -966,16 +980,19 @@ mod test { let vk2 = VerifyingKey::

::decode(&vk_bytes); assert!(vk == vk2); - let sk_bytes = sk.encode(); - let sk2 = SigningKey::

::decode(&sk_bytes); - assert!(sk == sk2); + #[allow(deprecated)] + { + let sk_bytes = sk.to_expanded(); + let sk2 = SigningKey::

::from_expanded(&sk_bytes); + assert!(sk == sk2); - let M = b"Hello world"; - let rnd = Array([0u8; 32]); - let sig = sk.sign_internal(&[M], &rnd); - let sig_bytes = sig.encode(); - let sig2 = Signature::

::decode(&sig_bytes).unwrap(); - assert!(sig == sig2); + let M = b"Hello world"; + let rnd = Array([0u8; 32]); + let sig = sk.sign_internal(&[M], &rnd); + let sig_bytes = sig.encode(); + let sig2 = Signature::

::decode(&sig_bytes).unwrap(); + assert!(sig == sig2); + } } #[test] diff --git a/ml-dsa/src/param.rs b/ml-dsa/src/param.rs index f04f448..a233630 100644 --- a/ml-dsa/src/param.rs +++ b/ml-dsa/src/param.rs @@ -137,9 +137,9 @@ pub trait SigningKeyParams: ParameterSet { s1: EncodedS1, s2: EncodedS2, t0: EncodedT0, - ) -> EncodedSigningKey; + ) -> ExpandedSigningKey; fn split_sk( - enc: &EncodedSigningKey, + enc: &ExpandedSigningKey, ) -> ( &B32, &B32, @@ -157,7 +157,7 @@ pub(crate) type EncodedT0

= Array::T0Size>; pub(crate) type SigningKeySize

=

::SigningKeySize; /// A signing key encoded as a byte array -pub type EncodedSigningKey

= Array>; +pub type ExpandedSigningKey

= Array>; impl

SigningKeyParams for P where @@ -250,12 +250,12 @@ where s1: EncodedS1, s2: EncodedS2, t0: EncodedT0, - ) -> EncodedSigningKey { + ) -> ExpandedSigningKey { rho.concat(K).concat(tr).concat(s1).concat(s2).concat(t0) } fn split_sk( - enc: &EncodedSigningKey, + enc: &ExpandedSigningKey, ) -> ( &B32, &B32, diff --git a/ml-dsa/tests/key-gen.rs b/ml-dsa/tests/key-gen.rs index 5a3fda4..4b26f74 100644 --- a/ml-dsa/tests/key-gen.rs +++ b/ml-dsa/tests/key-gen.rs @@ -29,19 +29,21 @@ fn verify(tc: &acvp::TestCase) { // Import test data into the relevant array structures let seed = Array::try_from(tc.seed.as_slice()).unwrap(); let vk_bytes = EncodedVerifyingKey::

::try_from(tc.pk.as_slice()).unwrap(); - let sk_bytes = EncodedSigningKey::

::try_from(tc.sk.as_slice()).unwrap(); + let sk_bytes = ExpandedSigningKey::

::try_from(tc.sk.as_slice()).unwrap(); let kp = P::from_seed(&seed); let sk = kp.signing_key().clone(); let vk = kp.verifying_key().clone(); - // Verify correctness via serialization - assert_eq!(sk.encode(), sk_bytes); assert_eq!(vk.encode(), vk_bytes); + assert!(vk == VerifyingKey::

::decode(&vk_bytes)); // Verify correctness via deserialization - assert!(sk == SigningKey::

::decode(&sk_bytes)); - assert!(vk == VerifyingKey::

::decode(&vk_bytes)); + #[allow(deprecated)] + { + assert_eq!(sk.to_expanded(), sk_bytes); + assert!(sk == SigningKey::

::from_expanded(&sk_bytes)); + } } mod acvp { diff --git a/ml-dsa/tests/sig-gen.rs b/ml-dsa/tests/sig-gen.rs index dcf1871..55b35b7 100644 --- a/ml-dsa/tests/sig-gen.rs +++ b/ml-dsa/tests/sig-gen.rs @@ -26,8 +26,10 @@ fn acvp_sig_gen() { fn verify(tc: &acvp::TestCase, deterministic: bool) { // Import the signing key - let sk_bytes = EncodedSigningKey::

::try_from(tc.sk.as_slice()).unwrap(); - let sk = SigningKey::

::decode(&sk_bytes); + let sk_bytes = ExpandedSigningKey::

::try_from(tc.sk.as_slice()).unwrap(); + + #[allow(deprecated)] + let sk = SigningKey::

::from_expanded(&sk_bytes); // Verify correctness let rnd = if deterministic {