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.
This commit is contained in:
Tony Arcieri
2026-01-10 14:09:23 -07:00
committed by GitHub
parent 035d9eef98
commit 3138dd20e3
5 changed files with 80 additions and 58 deletions
+4 -3
View File
@@ -9,6 +9,7 @@ pub fn rand<L: ArraySize, R: CryptoRng + ?Sized>(rng: &mut R) -> Array<u8, L> {
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::<MlDsa65>::decode(&sk_bytes);
let sk = SigningKey::<MlDsa65>::from_expanded(&sk_bytes);
let _sig = sk.sign_deterministic(&m, &ctx);
})
});
+60 -43
View File
@@ -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<P: MlDsaParams> SigningKey<P> {
Ok(self.sign_mu_deterministic(&mu))
}
/// Encode the key in a fixed-size byte array.
// Algorithm 24 skEncode
pub fn encode(&self) -> EncodedSigningKey<P>
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<P> {
let kp: &dyn signature::Keypair<VerifyingKey = VerifyingKey<P>> = 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`].
///
/// <div class="warning">
/// <b>Panics</b>
///
/// 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.
/// </div>
// Algorithm 25 skDecode
pub fn decode(enc: &EncodedSigningKey<P>) -> Self
#[deprecated(since = "0.1.0", note = "use `SigningKey::from_seed` instead")]
pub fn from_expanded(enc: &ExpandedSigningKey<P>) -> Self
where
P: MlDsaParams,
{
@@ -525,21 +534,26 @@ impl<P: MlDsaParams> SigningKey<P> {
)
}
/// 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<P> {
let kp: &dyn signature::Keypair<VerifyingKey = VerifyingKey<P>> = 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<P>
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::<P>::decode(&vk_bytes);
assert!(vk == vk2);
let sk_bytes = sk.encode();
let sk2 = SigningKey::<P>::decode(&sk_bytes);
assert!(sk == sk2);
#[allow(deprecated)]
{
let sk_bytes = sk.to_expanded();
let sk2 = SigningKey::<P>::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::<P>::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::<P>::decode(&sig_bytes).unwrap();
assert!(sig == sig2);
}
}
#[test]
+5 -5
View File
@@ -137,9 +137,9 @@ pub trait SigningKeyParams: ParameterSet {
s1: EncodedS1<Self>,
s2: EncodedS2<Self>,
t0: EncodedT0<Self>,
) -> EncodedSigningKey<Self>;
) -> ExpandedSigningKey<Self>;
fn split_sk(
enc: &EncodedSigningKey<Self>,
enc: &ExpandedSigningKey<Self>,
) -> (
&B32,
&B32,
@@ -157,7 +157,7 @@ pub(crate) type EncodedT0<P> = Array<u8, <P as SigningKeyParams>::T0Size>;
pub(crate) type SigningKeySize<P> = <P as SigningKeyParams>::SigningKeySize;
/// A signing key encoded as a byte array
pub type EncodedSigningKey<P> = Array<u8, SigningKeySize<P>>;
pub type ExpandedSigningKey<P> = Array<u8, SigningKeySize<P>>;
impl<P> SigningKeyParams for P
where
@@ -250,12 +250,12 @@ where
s1: EncodedS1<Self>,
s2: EncodedS2<Self>,
t0: EncodedT0<Self>,
) -> EncodedSigningKey<Self> {
) -> ExpandedSigningKey<Self> {
rho.concat(K).concat(tr).concat(s1).concat(s2).concat(t0)
}
fn split_sk(
enc: &EncodedSigningKey<Self>,
enc: &ExpandedSigningKey<Self>,
) -> (
&B32,
&B32,
+7 -5
View File
@@ -29,19 +29,21 @@ fn verify<P: MlDsaParams>(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::<P>::try_from(tc.pk.as_slice()).unwrap();
let sk_bytes = EncodedSigningKey::<P>::try_from(tc.sk.as_slice()).unwrap();
let sk_bytes = ExpandedSigningKey::<P>::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::<P>::decode(&vk_bytes));
// Verify correctness via deserialization
assert!(sk == SigningKey::<P>::decode(&sk_bytes));
assert!(vk == VerifyingKey::<P>::decode(&vk_bytes));
#[allow(deprecated)]
{
assert_eq!(sk.to_expanded(), sk_bytes);
assert!(sk == SigningKey::<P>::from_expanded(&sk_bytes));
}
}
mod acvp {
+4 -2
View File
@@ -26,8 +26,10 @@ fn acvp_sig_gen() {
fn verify<P: MlDsaParams>(tc: &acvp::TestCase, deterministic: bool) {
// Import the signing key
let sk_bytes = EncodedSigningKey::<P>::try_from(tc.sk.as_slice()).unwrap();
let sk = SigningKey::<P>::decode(&sk_bytes);
let sk_bytes = ExpandedSigningKey::<P>::try_from(tc.sk.as_slice()).unwrap();
#[allow(deprecated)]
let sk = SigningKey::<P>::from_expanded(&sk_bytes);
// Verify correctness
let rnd = if deterministic {