Skip to content

Commit ce1e453

Browse files
committed
feat(ssh-key): add ML-DSA algorithm identifiers and parameter sets
1 parent ac72201 commit ce1e453

2 files changed

Lines changed: 167 additions & 1 deletion

File tree

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)
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]

ssh-key/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ mod signature;
147147
mod sshsig;
148148

149149
pub use crate::{
150-
algorithm::{Algorithm, AssociatedHashAlg, EcdsaCurve, HashAlg, KdfAlg},
150+
algorithm::{Algorithm, AssociatedHashAlg, EcdsaCurve, HashAlg, KdfAlg, MlDsaParams},
151151
authorized_keys::AuthorizedKeys,
152152
error::{Error, Result},
153153
fingerprint::Fingerprint,

0 commit comments

Comments
 (0)