@@ -1,10 +1,4 @@
-#![no_std]
-#![doc = include_str!("../README.md")]
-#![doc(
- html_logo_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg",
- html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg"
-)]
-#![cfg_attr(docsrs, feature(doc_cfg))]
+#![doc = include_str!("README.md")]
#![allow(non_snake_case)] // Allow notation matching the spec
#![allow(clippy::similar_names)] // Allow notation matching the spec
#![allow(clippy::many_single_char_names)] // Allow notation matching the spec
@@ -25,8 +19,7 @@
//! - [`Signature`]: ML-DSA signature generated by a `SigningKey`, and verifiable by a
//! `VerifyingKey`. Implements the [`SignatureEncoding`] trait.
//!
-#![cfg_attr(feature = "getrandom", doc = "```")]
-#![cfg_attr(not(feature = "getrandom"), doc = "```ignore")]
+//! ```ignore
//! # fn main() -> Result<(), signature::Error> {
//! // NOTE: requires the `getrandom` feature is enabled
//! use ml_dsa::{MlDsa65, Generate, Keypair, SigningKey, Signer, Verifier};
@@ -40,10 +33,10 @@
//! # Ok(()) }
//! ```
-#[cfg(feature = "alloc")]
+#[cfg(feature = "embedded")]
extern crate alloc;
-#[cfg(feature = "pkcs8")]
+#[cfg(feature = "embedded")]
pub mod pkcs8;
mod algebra;
@@ -56,7 +49,7 @@
mod signing;
mod verifying;
-pub use crate::{
+pub use self::{
param::{EncodedSignature, EncodedVerifyingKey, ExpandedSigningKeyBytes, MlDsaParams},
signing::{ExpandedSigningKey, SigningKey},
verifying::VerifyingKey,
@@ -64,10 +57,10 @@
pub use common::{self, KeyExport, KeyInit, KeySizeUser};
pub use signature::{self, Error, Keypair, SignatureEncoding, Signer, Verifier};
-#[cfg(feature = "rand_core")]
+#[cfg(feature = "embedded")]
pub use common::Generate;
-use crate::{
+use self::{
algebra::{AlgebraExt, Vector},
crypto::H,
hint::Hint,
@@ -254,8 +247,8 @@
#[cfg(test)]
mod test {
+ use super::param::*;
use super::*;
- use crate::param::*;
use hybrid_array::typenum::Unsigned;
use signature::Keypair;
@@ -532,7 +525,7 @@
}
#[test]
- #[cfg(feature = "alloc")]
+ #[cfg(feature = "embedded")]
fn debug_implementations() {
extern crate alloc;
use core::fmt::Write;
@@ -268,7 +268,7 @@
mod test {
use super::*;
- use crate::{MlDsa65, ParameterSet};
+ use super::super::{MlDsa65, ParameterSet};
type Mod = <MlDsa65 as ParameterSet>::TwoGamma2;
const MOD: u32 = Mod::U32;
@@ -61,8 +61,8 @@
#[cfg(test)]
mod test {
+ use super::super::B32;
use super::*;
- use crate::B32;
use hex_literal::hex;
#[test]
@@ -5,7 +5,7 @@
};
use module_lattice::{ArraySize, Encode, EncodingSize, VectorEncodingSize};
-use crate::algebra::{Elem, Polynomial, Vector};
+use super::algebra::{Elem, Polynomial, Vector};
/// A pair of integers that describes a range
pub trait RangeEncodingSize {
@@ -108,8 +108,8 @@
#[cfg(test)]
#[allow(clippy::integer_division_remainder_used, reason = "tests")]
pub(crate) mod tests {
+ use super::super::algebra::*;
use super::*;
- use crate::algebra::*;
use core::ops::Rem;
use getrandom::{
SysRng,
@@ -1,4 +1,4 @@
-use crate::{
+use super::{
algebra::{AlgebraExt, BaseField, Decompose, Elem, Polynomial, Vector},
param::{EncodedHint, SignatureParams},
};
@@ -164,8 +164,8 @@
#[cfg(test)]
#[allow(clippy::integer_division_remainder_used, reason = "tests")]
mod test {
+ use super::super::{MlDsa44, MlDsa65, ParameterSet};
use super::*;
- use crate::{MlDsa44, MlDsa65, ParameterSet};
#[test]
fn use_hint_arithmetic() {
@@ -1,4 +1,4 @@
-use crate::algebra::{BaseField, Elem, NttPolynomial, NttVector, Polynomial, Vector};
+use super::algebra::{BaseField, Elem, NttPolynomial, NttVector, Polynomial, Vector};
use module_lattice::{ArraySize, Field, MultiplyNtt};
// Since the powers of zeta used in the NTT and MultiplyNTTs are fixed, we use pre-computed tables
@@ -183,7 +183,7 @@
typenum::{U2, U3},
};
- use crate::algebra::*;
+ use super::super::algebra::*;
// Multiplication in R_q, modulo X^256 + 1
fn poly_mul(lhs: &Polynomial, rhs: &Polynomial) -> Polynomial {
@@ -8,7 +8,7 @@
//! know any details about object sizes. For example, `VectorEncodingSize::flatten` needs to know
//! that the size of an encoded vector is `K` times the size of an encoded polynomial.
-use crate::{
+use super::{
B32, B64,
algebra::{Polynomial, Vector},
encode::{BitPack, RangeEncodedPolynomialSize, RangeEncodedVectorSize, RangeEncodingSize},
@@ -1,6 +1,6 @@
//! PKCS#8 private key encoding support.
-use crate::{
+use super::{
EncodedVerifyingKey, ExpandedSigningKey, MlDsa44, MlDsa65, MlDsa87, MlDsaParams, Signature,
SigningKey, VerifyingKey,
};
@@ -14,7 +14,7 @@
};
use const_oid::db::fips204;
-#[cfg(feature = "alloc")]
+#[cfg(feature = "embedded")]
use pkcs8::{
der::{
Encode, TagMode,
@@ -66,7 +66,7 @@
const ALGORITHM_IDENTIFIER: AlgorithmIdentifierRef<'static> = P::ALGORITHM_IDENTIFIER;
}
-#[cfg(feature = "alloc")]
+#[cfg(feature = "embedded")]
impl<P: MlDsaParams> SignatureBitStringEncoding for Signature<P> {
fn to_bitstring(&self) -> der::Result<BitString> {
BitString::new(0, self.encode().to_vec())
@@ -110,7 +110,7 @@
}
}
-#[cfg(feature = "alloc")]
+#[cfg(feature = "embedded")]
impl<P> EncodePrivateKey for SigningKey<P>
where
P: MlDsaParams,
@@ -164,7 +164,7 @@
Signature::<P>::ALGORITHM_IDENTIFIER;
}
-#[cfg(feature = "alloc")]
+#[cfg(feature = "embedded")]
impl<P> EncodePublicKey for VerifyingKey<P>
where
P: MlDsaParams,
@@ -1,11 +1,11 @@
-use crate::{
+use super::{
algebra::{BaseField, Elem, Int, NttMatrix, NttPolynomial, NttVector, Polynomial, Vector},
crypto::{G, H},
param::{Eta, MaskSamplingSize},
};
use hybrid_array::Array;
use module_lattice::{ArraySize, Field, Truncate};
-#[cfg(feature = "zeroize")]
+#[cfg(feature = "embedded")]
use zeroize::Zeroize;
// Algorithm 13 BytesToBits
@@ -105,7 +105,7 @@
let mut buf = [0u8; 840];
ctx.squeeze(&mut buf);
- for chunk in buf.chunks_exact(3) {
+ for chunk in buf.as_chunks::<3>().0 {
if let Some(x) = coeff_from_three_bytes([chunk[0], chunk[1], chunk[2]]) {
a.0[j] = x;
j += 1;
@@ -124,7 +124,7 @@
j += 1;
}
}
- #[cfg(feature = "zeroize")]
+ #[cfg(feature = "embedded")]
{
buf.zeroize();
tmp.zeroize();
@@ -169,14 +169,14 @@
a.0[j] = x;
j += 1;
}
- if j < 256 {
- if let Some(x) = z1 {
- a.0[j] = x;
- j += 1;
- }
+ if j < 256
+ && let Some(x) = z1
+ {
+ a.0[j] = x;
+ j += 1;
}
}
- #[cfg(feature = "zeroize")]
+ #[cfg(feature = "embedded")]
{
buf.zeroize();
tmp.zeroize();
@@ -2,7 +2,7 @@
//!
//! These types implement signature generation.
-use crate::{
+use super::{
B32, B64, ExpandedSigningKeyBytes, MlDsaParams, MuBuilder, Seed, Signature, VerifyingKey,
algebra::{AlgebraExt, NttMatrix, NttVector, Vector},
crypto::H,
@@ -19,7 +19,7 @@
use shake::Shake256;
use signature::{DigestSigner, Error, MultipartSigner, Signer};
-#[cfg(feature = "rand_core")]
+#[cfg(feature = "embedded")]
use {
common::Generate,
signature::{
@@ -28,7 +28,7 @@
},
};
-#[cfg(feature = "zeroize")]
+#[cfg(feature = "embedded")]
use zeroize::{Zeroize, ZeroizeOnDrop};
/// ML-DSA signing key (i.e. private/secret key).
@@ -43,7 +43,7 @@
seed: MaybeBox<Seed>,
/// When the `alloc` feature is available, precompute the [`VerifyingKey`].
- #[cfg(feature = "alloc")]
+ #[cfg(feature = "embedded")]
verifying_key: VerifyingKey<P>,
}
@@ -79,13 +79,13 @@
let tr: B64 = H::default().absorb(&enc).squeeze_new();
let expanded_key = ExpandedSigningKey::new(rho, K, tr, s1, s2, t0, A_hat);
- #[cfg(feature = "alloc")]
+ #[cfg(feature = "embedded")]
let verifying_key = expanded_key.verifying_key();
SigningKey {
expanded_key: MaybeBox::new(expanded_key),
seed: MaybeBox::new(xi.clone()),
- #[cfg(feature = "alloc")]
+ #[cfg(feature = "embedded")]
verifying_key,
}
}
@@ -141,7 +141,7 @@
}
/// Algorithm 1: `ML-DSA.KeyGen()`.
-#[cfg(feature = "rand_core")]
+#[cfg(feature = "embedded")]
impl<P: MlDsaParams> Generate for SigningKey<P> {
fn try_generate_from_rng<R: TryCryptoRng + ?Sized>(rng: &mut R) -> Result<Self, R::Error> {
let seed = Seed::try_generate_from_rng(rng)?;
@@ -158,7 +158,7 @@
// NOTE: when the `alloc` feature is enabled, we receive a blanket impl of `Keypair` via the impl
// of the `KeypairRef` trait which simply clones the precomputed verifying key, providing equivalent
// functionality and thus this is actually still an additive use of features.
-#[cfg(not(feature = "alloc"))]
+#[cfg(not(feature = "embedded"))]
impl<P: MlDsaParams> signature::Keypair for SigningKey<P> {
type VerifyingKey = VerifyingKey<P>;
fn verifying_key(&self) -> VerifyingKey<P> {
@@ -166,14 +166,14 @@
}
}
-#[cfg(feature = "alloc")]
+#[cfg(feature = "embedded")]
impl<P: MlDsaParams> AsRef<VerifyingKey<P>> for SigningKey<P> {
fn as_ref(&self) -> &VerifyingKey<P> {
&self.verifying_key
}
}
-#[cfg(feature = "alloc")]
+#[cfg(feature = "embedded")]
impl<P: MlDsaParams> signature::KeypairRef for SigningKey<P> {
type VerifyingKey = VerifyingKey<P>;
}
@@ -222,12 +222,12 @@
impl<P: MlDsaParams> Drop for SigningKey<P> {
fn drop(&mut self) {
// NOTE: `expanded_key` has its own zeroizing `Drop` impl so we just need to clear `seed`
- #[cfg(feature = "zeroize")]
+ #[cfg(feature = "embedded")]
self.seed.zeroize();
}
}
-#[cfg(feature = "zeroize")]
+#[cfg(feature = "embedded")]
impl<P: MlDsaParams> ZeroizeOnDrop for SigningKey<P> {}
/// An ML-DSA signing key
@@ -373,7 +373,7 @@
/// This method will return an opaque error if the context string is more than 255 bytes long,
/// or if it fails to get enough randomness.
// Algorithm 2 ML-DSA.Sign
- #[cfg(feature = "rand_core")]
+ #[cfg(feature = "embedded")]
pub fn sign_randomized<R: TryCryptoRng + ?Sized>(
&self,
M: &[u8],
@@ -383,7 +383,7 @@
self.raw_sign_randomized(&[M], ctx, rng)
}
- #[cfg(feature = "rand_core")]
+ #[cfg(feature = "embedded")]
fn raw_sign_randomized<R: TryCryptoRng + ?Sized>(
&self,
Mp: &[&[u8]],
@@ -407,7 +407,7 @@
///
/// This method can return an opaque error if it fails to get enough randomness.
// Algorithm 2 ML-DSA.Sign (optional pre-computed μ variant)
- #[cfg(feature = "rand_core")]
+ #[cfg(feature = "embedded")]
pub fn sign_mu_randomized<R: TryCryptoRng + ?Sized>(
&self,
mu: &B64,
@@ -491,6 +491,59 @@
)
}
+ /// Decode an expanded signing key and check its internal consistency.
+ ///
+ /// Seed-based import remains preferred. This method supports expanded-only
+ /// keys without allowing malformed packed coefficients to panic.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if a secret coefficient encoding is out of range, or if
+ /// the stored low-order bits or public-key hash do not match the secret key.
+ pub fn try_from_expanded(enc: &ExpandedSigningKeyBytes<P>) -> Result<Self, Error> {
+ let (rho, K, tr, s1_enc, s2_enc, t0_enc) = P::split_sk(enc);
+ // FIPS 204 section 7.2, Algorithms 19 and 24-25: skEncode's secret
+ // coefficient codes occupy [0, 2*eta]. BitUnpack also decodes unused
+ // codes outside that range; check these before the trusted decoder.
+ // https://doi.org/10.6028/NIST.FIPS.204
+ let eta = P::Eta::U16;
+ let bits = if eta == 2 { 3 } else { 4 };
+ if !valid_secret_encoding(s1_enc.as_slice(), bits, 2 * eta)
+ || !valid_secret_encoding(s2_enc.as_slice(), bits, 2 * eta)
+ {
+ return Err(Error::new());
+ }
+ let key = Self::new_expand_a(
+ rho.clone(),
+ K.clone(),
+ tr.clone(),
+ P::decode_s1(s1_enc),
+ P::decode_s2(s2_enc),
+ P::decode_t0(t0_enc),
+ );
+ // FIPS 204 Algorithm 6 derives t0 and tr from A*s1+s2. Checking them
+ // does not claim that the original seed can be reconstructed.
+ let As1_hat = &key.A_hat * &key.s1_hat;
+ let As1 = As1_hat.ntt_inverse();
+ let t = &As1 + &key.s2;
+ let (t1, t0) = t.power2round();
+ let t0_matches = t0.ct_eq(&key.t0);
+ let mut As1_hat = As1_hat;
+ let mut As1 = As1;
+ let mut t = t;
+ let mut t0 = t0;
+ As1_hat.zeroize();
+ As1.zeroize();
+ t.zeroize();
+ t0.zeroize();
+ let public = VerifyingKey::<P>::encode_internal(&key.rho, &t1);
+ let expected_tr: B64 = H::default().absorb(&public).squeeze_new();
+ if !bool::from(t0_matches.and(expected_tr.ct_eq(&key.tr))) {
+ return Err(Error::new());
+ }
+ Ok(key)
+ }
+
/// DEPRECATED: encode the key in a fixed-size byte array.
///
/// Note that this form is deprecated in practice; prefer to use [`SigningKey::to_seed`].
@@ -514,6 +567,25 @@
}
}
+/// Check packed secret coefficients without allocating or invoking BitUnPack.
+fn valid_secret_encoding(bytes: &[u8], bits: u32, maximum: u16) -> bool {
+ let mask = (1u16 << bits) - 1;
+ let mut pending = 0u16;
+ let mut width = 0;
+ let mut invalid = 0u16;
+ for &byte in bytes {
+ pending |= u16::from(byte) << width;
+ width += 8;
+ while width >= bits {
+ invalid |= u16::from((pending & mask) > maximum);
+ pending >>= bits;
+ width -= bits;
+ }
+ }
+ debug_assert_eq!(width, 0, "ML-DSA polynomials contain 256 coefficients");
+ invalid == 0
+}
+
/// The `Signer` implementation for `ExpandedSigningKey` uses the optional deterministic variant of ML-DSA, and
/// only supports signing with an empty context string. If you would like to include a context
/// string, use the [`ExpandedSigningKey::sign_deterministic`] method.
@@ -571,7 +643,7 @@
/// The `RandomizedSigner` implementation for `ExpandedSigningKey` only supports signing with an empty
/// context string. If you would like to include a context string, use the
/// [`ExpandedSigningKey::sign_randomized`] method.
-#[cfg(feature = "rand_core")]
+#[cfg(feature = "embedded")]
impl<P: MlDsaParams> RandomizedSigner<Signature<P>> for ExpandedSigningKey<P> {
fn try_sign_with_rng<R: TryCryptoRng + ?Sized>(
&self,
@@ -585,7 +657,7 @@
/// The `RandomizedSigner` implementation for `ExpandedSigningKey` only supports signing with an empty
/// context string. If you would like to include a context string, use the
/// [`ExpandedSigningKey::sign_randomized`] method.
-#[cfg(feature = "rand_core")]
+#[cfg(feature = "embedded")]
impl<P: MlDsaParams> RandomizedMultipartSigner<Signature<P>> for ExpandedSigningKey<P> {
fn try_multipart_sign_with_rng<R: TryCryptoRng + ?Sized>(
&self,
@@ -599,7 +671,7 @@
/// The `RandomizedSigner` implementation for `ExpandedSigningKey` only supports signing with an empty
/// context string. If you would like to include a context string, use the
/// [`ExpandedSigningKey::sign_mu_randomized`] method.
-#[cfg(feature = "rand_core")]
+#[cfg(feature = "embedded")]
impl<P: MlDsaParams> RandomizedDigestSigner<Shake256, Signature<P>> for ExpandedSigningKey<P> {
fn try_sign_digest_with_rng<
R: TryCryptoRng + ?Sized,
@@ -643,7 +715,7 @@
impl<P: MlDsaParams> Drop for ExpandedSigningKey<P> {
fn drop(&mut self) {
- #[cfg(feature = "zeroize")]
+ #[cfg(feature = "embedded")]
{
self.rho.zeroize();
self.K.zeroize();
@@ -658,5 +730,5 @@
}
}
-#[cfg(feature = "zeroize")]
+#[cfg(feature = "embedded")]
impl<P: MlDsaParams> ZeroizeOnDrop for ExpandedSigningKey<P> {}
@@ -1,6 +1,6 @@
//! ML-DSA signature verification.
-use crate::{
+use super::{
B32, B64, EncodedVerifyingKey, MlDsaParams, MuBuilder, Signature,
algebra::{Elem, NttMatrix, NttVector, Vector},
crypto::H,