Skip to content

Commit fee28fe

Browse files
committed
Implement ML-DSA
1 parent 7a1c757 commit fee28fe

11 files changed

Lines changed: 851 additions & 4 deletions

File tree

Cargo.lock

Lines changed: 77 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ssh-key/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ dsa = { version = "0.7.0-rc.16", optional = true, default-features = false, feat
3232
ed25519-dalek = { version = "3.0.0-rc.1", optional = true, default-features = false }
3333
hex = { version = "0.4", optional = true, default-features = false, features = ["alloc"] }
3434
hmac = { version = "0.13", optional = true }
35+
ml-dsa = { version = "0.1", optional = true, default-features = false, features = ["alloc", "zeroize"] }
3536
p256 = { version = "0.14.0-rc.15", optional = true, default-features = false, features = ["ecdsa"] }
3637
p384 = { version = "0.14.0-rc.15", optional = true, default-features = false, features = ["ecdsa"] }
3738
p521 = { version = "0.14.0-rc.15", optional = true, default-features = false, features = ["ecdsa"] }
@@ -51,7 +52,7 @@ default = ["ecdsa", "rand_core", "std"]
5152
alloc = ["encoding/alloc", "signature/alloc", "zeroize/alloc", ]
5253
std = ["alloc"]
5354

54-
crypto = ["ed25519", "p256", "p384", "p521", "rsa"] # NOTE: `dsa` is obsolete/weak
55+
crypto = ["ed25519", "mldsa", "p256", "p384", "p521", "rsa"] # NOTE: `dsa` is obsolete/weak
5556
dsa = ["dep:dsa", "dep:sha1", "alloc", "encoding/bigint", "signature/rand_core"]
5657
ecdsa = ["dep:sec1"]
5758
ed25519 = ["dep:ed25519-dalek", "rand_core"]
@@ -63,6 +64,7 @@ encryption = [
6364
"rand_core"
6465
]
6566
getrandom = ["cipher/getrandom", "rand_core"]
67+
mldsa = ["dep:ml-dsa", "alloc", "rand_core"]
6668
p256 = ["dep:p256", "ecdsa"]
6769
p384 = ["dep:p384", "ecdsa"]
6870
p521 = ["dep:p521", "ecdsa"]

ssh-key/src/algorithm.rs

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ const CERT_SK_ECDSA_SHA2_P256: &str = "sk-ecdsa-sha2-nistp256-cert-v01@openssh.c
4747
/// OpenSSH certificate for Ed25519 U2F/FIDO security key
4848
const CERT_SK_SSH_ED25519: &str = "sk-ssh-ed25519-cert-v01@openssh.com";
4949

50+
/// OpenSSH certificate for ML-DSA-44 public key
51+
const CERT_MLDSA_44: &str = "ssh-mldsa-44-cert-v01@openssh.com";
52+
53+
/// OpenSSH certificate for ML-DSA-65 public key
54+
const CERT_MLDSA_65: &str = "ssh-mldsa-65-cert-v01@openssh.com";
55+
56+
/// OpenSSH certificate for ML-DSA-87 public key
57+
const CERT_MLDSA_87: &str = "ssh-mldsa-87-cert-v01@openssh.com";
58+
5059
/// ECDSA with SHA-256 + NIST P-256
5160
const ECDSA_SHA2_P256: &str = "ecdsa-sha2-nistp256";
5261

@@ -86,6 +95,15 @@ const SK_ECDSA_SHA2_P256: &str = "sk-ecdsa-sha2-nistp256@openssh.com";
8695
/// U2F/FIDO security key with Ed25519
8796
const SK_SSH_ED25519: &str = "sk-ssh-ed25519@openssh.com";
8897

98+
/// ML-DSA-44 (FIPS 204, security category 2)
99+
const SSH_MLDSA_44: &str = "ssh-mldsa-44";
100+
101+
/// ML-DSA-65 (FIPS 204, security category 3)
102+
const SSH_MLDSA_65: &str = "ssh-mldsa-65";
103+
104+
/// ML-DSA-87 (FIPS 204, security category 5)
105+
const SSH_MLDSA_87: &str = "ssh-mldsa-87";
106+
89107
/// SSH key algorithms, i.e. digital signature algorithms used with SSH private/public keys.
90108
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)]
91109
#[non_exhaustive]
@@ -121,6 +139,12 @@ pub enum Algorithm {
121139
/// FIDO/U2F key with Ed25519
122140
SkEd25519,
123141

142+
/// ML-DSA
143+
MlDsa {
144+
/// ML-DSA parameter set to use.
145+
params: MlDsaParams,
146+
},
147+
124148
/// Other
125149
#[cfg(feature = "alloc")]
126150
Other(AlgorithmName),
@@ -138,6 +162,9 @@ impl Algorithm {
138162
/// - `ssh-rsa`
139163
/// - `sk-ecdsa-sha2-nistp256@openssh.com` (FIDO/U2F key)
140164
/// - `sk-ssh-ed25519@openssh.com` (FIDO/U2F key)
165+
/// - `ssh-mldsa-44`
166+
/// - `ssh-mldsa-65`
167+
/// - `ssh-mldsa-87`
141168
///
142169
/// Any other algorithms are mapped to the [`Algorithm::Other`] variant.
143170
///
@@ -161,6 +188,9 @@ impl Algorithm {
161188
/// - `ssh-ed25519-cert-v01@openssh.com`
162189
/// - `sk-ecdsa-sha2-nistp256-cert-v01@openssh.com` (FIDO/U2F key)
163190
/// - `sk-ssh-ed25519-cert-v01@openssh.com` (FIDO/U2F key)
191+
/// - `ssh-mldsa-44-cert-v01@openssh.com`
192+
/// - `ssh-mldsa-65-cert-v01@openssh.com`
193+
/// - `ssh-mldsa-87-cert-v01@openssh.com`
164194
///
165195
/// Any other algorithms are mapped to the [`Algorithm::Other`] variant.
166196
///
@@ -190,6 +220,15 @@ impl Algorithm {
190220
}),
191221
CERT_SK_ECDSA_SHA2_P256 => Ok(Algorithm::SkEcdsaSha2NistP256),
192222
CERT_SK_SSH_ED25519 => Ok(Algorithm::SkEd25519),
223+
CERT_MLDSA_44 => Ok(Algorithm::MlDsa {
224+
params: MlDsaParams::MlDsa44,
225+
}),
226+
CERT_MLDSA_65 => Ok(Algorithm::MlDsa {
227+
params: MlDsaParams::MlDsa65,
228+
}),
229+
CERT_MLDSA_87 => Ok(Algorithm::MlDsa {
230+
params: MlDsaParams::MlDsa87,
231+
}),
193232
#[cfg(feature = "alloc")]
194233
_ => Ok(Algorithm::Other(AlgorithmName::from_certificate_type(id)?)),
195234
#[cfg(not(feature = "alloc"))]
@@ -215,6 +254,11 @@ impl Algorithm {
215254
},
216255
Algorithm::SkEcdsaSha2NistP256 => SK_ECDSA_SHA2_P256,
217256
Algorithm::SkEd25519 => SK_SSH_ED25519,
257+
Algorithm::MlDsa { params } => match params {
258+
MlDsaParams::MlDsa44 => SSH_MLDSA_44,
259+
MlDsaParams::MlDsa65 => SSH_MLDSA_65,
260+
MlDsaParams::MlDsa87 => SSH_MLDSA_87,
261+
},
218262
#[cfg(feature = "alloc")]
219263
Algorithm::Other(algorithm) => algorithm.as_str(),
220264
}
@@ -247,6 +291,11 @@ impl Algorithm {
247291
} => CERT_RSA_SHA2_512,
248292
Algorithm::SkEcdsaSha2NistP256 => CERT_SK_ECDSA_SHA2_P256,
249293
Algorithm::SkEd25519 => CERT_SK_SSH_ED25519,
294+
Algorithm::MlDsa { params } => match params {
295+
MlDsaParams::MlDsa44 => CERT_MLDSA_44,
296+
MlDsaParams::MlDsa65 => CERT_MLDSA_65,
297+
MlDsaParams::MlDsa87 => CERT_MLDSA_87,
298+
},
250299
Algorithm::Other(algorithm) => return algorithm.certificate_type(),
251300
}
252301
.to_owned()
@@ -276,6 +325,12 @@ impl Algorithm {
276325
matches!(self, Algorithm::Rsa { .. })
277326
}
278327

328+
/// Is the algorithm ML-DSA?
329+
#[must_use]
330+
pub fn is_mldsa(self) -> bool {
331+
matches!(self, Algorithm::MlDsa { .. })
332+
}
333+
279334
/// Return an error indicating this algorithm is unsupported.
280335
#[allow(dead_code)]
281336
pub(crate) fn unsupported_error(self) -> Error {
@@ -322,6 +377,15 @@ impl str::FromStr for Algorithm {
322377
SSH_RSA => Ok(Algorithm::Rsa { hash: None }),
323378
SK_ECDSA_SHA2_P256 => Ok(Algorithm::SkEcdsaSha2NistP256),
324379
SK_SSH_ED25519 => Ok(Algorithm::SkEd25519),
380+
SSH_MLDSA_44 => Ok(Algorithm::MlDsa {
381+
params: MlDsaParams::MlDsa44,
382+
}),
383+
SSH_MLDSA_65 => Ok(Algorithm::MlDsa {
384+
params: MlDsaParams::MlDsa65,
385+
}),
386+
SSH_MLDSA_87 => Ok(Algorithm::MlDsa {
387+
params: MlDsaParams::MlDsa87,
388+
}),
325389
#[cfg(feature = "alloc")]
326390
_ => Ok(Algorithm::Other(AlgorithmName::from_str(id)?)),
327391
#[cfg(not(feature = "alloc"))]
@@ -412,6 +476,108 @@ impl str::FromStr for EcdsaCurve {
412476
}
413477
}
414478

479+
/// ML-DSA parameter sets supported for use with SSH as specified in [FIPS204].
480+
///
481+
/// Each parameter set corresponds to a NIST security category.
482+
///
483+
/// [FIPS204]: https://csrc.nist.gov/pubs/fips/204/final
484+
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
485+
pub enum MlDsaParams {
486+
/// ML-DSA-44 (security category 2).
487+
MlDsa44,
488+
489+
/// ML-DSA-65 (security category 3), the recommended parameter set.
490+
MlDsa65,
491+
492+
/// ML-DSA-87 (security category 5).
493+
MlDsa87,
494+
}
495+
496+
impl MlDsaParams {
497+
/// Decode an ML-DSA parameter set from the given SSH algorithm identifier.
498+
///
499+
/// # Supported identifiers
500+
///
501+
/// - `ssh-mldsa-44`
502+
/// - `ssh-mldsa-65`
503+
/// - `ssh-mldsa-87`
504+
///
505+
/// # Errors
506+
/// Returns [`Error::Encoding`] in the event the identifier is not known.
507+
pub fn new(id: &str) -> Result<Self> {
508+
Ok(id.parse()?)
509+
}
510+
511+
/// Get the SSH algorithm identifier which corresponds to this parameter set.
512+
#[must_use]
513+
pub fn as_str(self) -> &'static str {
514+
match self {
515+
MlDsaParams::MlDsa44 => SSH_MLDSA_44,
516+
MlDsaParams::MlDsa65 => SSH_MLDSA_65,
517+
MlDsaParams::MlDsa87 => SSH_MLDSA_87,
518+
}
519+
}
520+
521+
/// Size in bytes of a FIPS 204 public key for this parameter set.
522+
#[must_use]
523+
pub const fn public_key_size(self) -> usize {
524+
match self {
525+
MlDsaParams::MlDsa44 => 1312,
526+
MlDsaParams::MlDsa65 => 1952,
527+
MlDsaParams::MlDsa87 => 2592,
528+
}
529+
}
530+
531+
/// Size in bytes of a FIPS 204 signature for this parameter set.
532+
#[must_use]
533+
pub const fn signature_size(self) -> usize {
534+
match self {
535+
MlDsaParams::MlDsa44 => 2420,
536+
MlDsaParams::MlDsa65 => 3309,
537+
MlDsaParams::MlDsa87 => 4627,
538+
}
539+
}
540+
541+
/// Size in bytes of the seed (ξ) used to derive an ML-DSA key.
542+
///
543+
/// This is 32 bytes for all parameter sets.
544+
#[must_use]
545+
pub const fn seed_size(self) -> usize {
546+
32
547+
}
548+
}
549+
550+
impl AsRef<str> for MlDsaParams {
551+
fn as_ref(&self) -> &str {
552+
self.as_str()
553+
}
554+
}
555+
556+
impl From<MlDsaParams> for Algorithm {
557+
fn from(params: MlDsaParams) -> Algorithm {
558+
Algorithm::MlDsa { params }
559+
}
560+
}
561+
562+
impl fmt::Display for MlDsaParams {
563+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
564+
f.write_str(self.as_str())
565+
}
566+
}
567+
568+
impl str::FromStr for MlDsaParams {
569+
type Err = LabelError;
570+
571+
fn from_str(id: &str) -> core::result::Result<Self, LabelError> {
572+
match id {
573+
SSH_MLDSA_44 => Ok(MlDsaParams::MlDsa44),
574+
SSH_MLDSA_65 => Ok(MlDsaParams::MlDsa65),
575+
SSH_MLDSA_87 => Ok(MlDsaParams::MlDsa87),
576+
_ => Err(LabelError::new(id)),
577+
}
578+
}
579+
}
580+
415581
/// Hashing algorithms a.k.a. digest functions.
416582
#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)]
417583
#[non_exhaustive]

0 commit comments

Comments
 (0)