Skip to content

Commit 865970a

Browse files
committed
feat: add mldsa44_ed25519 key support
1 parent 19aa613 commit 865970a

27 files changed

Lines changed: 1140 additions & 683 deletions

ssh-key/Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ default = ["ecdsa", "rand_core", "std"]
5252
alloc = ["encoding/alloc", "signature/alloc", "zeroize/alloc", ]
5353
std = ["alloc"]
5454

55-
crypto = ["ed25519", "mldsa", "p256", "p384", "p521", "rsa"] # NOTE: `dsa` is obsolete/weak
55+
crypto = ["ed25519", "mldsa-eddsa", "p256", "p384", "p521", "rsa"] # NOTE: `dsa` is obsolete/weak
5656
dsa = ["dep:dsa", "dep:sha1", "alloc", "encoding/bigint", "signature/rand_core"]
5757
ecdsa = ["dep:sec1"]
5858
ed25519 = ["dep:ed25519-dalek", "rand_core"]
@@ -64,8 +64,9 @@ encryption = [
6464
"rand_core"
6565
]
6666
getrandom = ["cipher/getrandom", "rand_core"]
67-
# This is based on draft-sfluhrer-ssh-mldsa, which is still in draft and has not been adopted
68-
mldsa = ["dep:ml-dsa", "alloc", "rand_core"]
67+
# MLDSA44-Ed25519-SHA512 composite signatures, as implemented by OpenSSH. Based on
68+
# draft-miller-sshm-mldsa44-ed25519-composite-sigs, which is still a draft
69+
mldsa-eddsa = ["dep:ml-dsa", "ed25519", "alloc", "rand_core"]
6970
p256 = ["dep:p256", "ecdsa"]
7071
p384 = ["dep:p384", "ecdsa"]
7172
p521 = ["dep:p521", "ecdsa"]

ssh-key/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ respective SSH key algorithm.
4242
- [x] OpenSSH certificate authority (CA) support i.e. cert builder/signer
4343
- [x] Private key encryption/decryption (`bcrypt-pbkdf` + `aes256-ctr` only)
4444
- [x] Private key generation support: DSA, Ed25519, ECDSA (P-256/P-384/P-521),
45-
and RSA
45+
RSA, and MLDSA44-Ed25519
4646
- [x] FIDO/U2F key support (`sk-*`) as specified in [PROTOCOL.u2f]
4747
- [x] Fingerprint support
4848
- [x] "randomart" fingerprint visualizations
@@ -73,6 +73,7 @@ respective SSH key algorithm.
7373
| `ssh‑rsa` |||| ✅️ | ✅️ || `rsa` | `alloc` |
7474
| `sk‑ecdsa‑sha2‑nistp256@openssh.com` ||||| ⛔️ | ✅️ || `alloc` |
7575
| `sk‑ssh‑ed25519@openssh.com` ||||| ⛔️ | ✅️️ | `ed25519` | `alloc` |
76+
| `ssh‑mldsa44‑ed25519@openssh.com` |||| ✅️ | ✅️ | ✅️ | `mldsa-eddsa` | `alloc` |
7677

7778
By default *no SSH signature algorithms are enabled* and you will get an
7879
`Error::AlgorithmUnsupported` error if you try to use them.

ssh-key/src/algorithm.rs

Lines changed: 15 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,8 @@ 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";
50+
/// OpenSSH certificate for MLDSA44-Ed25519 composite public key
51+
const CERT_MLDSA44_ED25519: &str = "ssh-mldsa44-ed25519-cert-v01@openssh.com";
5852

5953
/// ECDSA with SHA-256 + NIST P-256
6054
const ECDSA_SHA2_P256: &str = "ecdsa-sha2-nistp256";
@@ -95,14 +89,8 @@ const SK_ECDSA_SHA2_P256: &str = "sk-ecdsa-sha2-nistp256@openssh.com";
9589
/// U2F/FIDO security key with Ed25519
9690
const SK_SSH_ED25519: &str = "sk-ssh-ed25519@openssh.com";
9791

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";
92+
/// MLDSA44-Ed25519-SHA512 composite signature algorithm
93+
const SSH_MLDSA44_ED25519: &str = "ssh-mldsa44-ed25519@openssh.com";
10694

10795
/// SSH key algorithms, i.e. digital signature algorithms used with SSH private/public keys.
10896
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)]
@@ -139,11 +127,8 @@ pub enum Algorithm {
139127
/// FIDO/U2F key with Ed25519
140128
SkEd25519,
141129

142-
/// ML-DSA
143-
MlDsa {
144-
/// ML-DSA parameter set to use.
145-
params: MlDsaParams,
146-
},
130+
/// MLDSA44-Ed25519-SHA512 composite signature algorithm.
131+
MlDsa44Ed25519,
147132

148133
/// Other
149134
#[cfg(feature = "alloc")]
@@ -162,9 +147,7 @@ impl Algorithm {
162147
/// - `ssh-rsa`
163148
/// - `sk-ecdsa-sha2-nistp256@openssh.com` (FIDO/U2F key)
164149
/// - `sk-ssh-ed25519@openssh.com` (FIDO/U2F key)
165-
/// - `ssh-mldsa-44`
166-
/// - `ssh-mldsa-65`
167-
/// - `ssh-mldsa-87`
150+
/// - `ssh-mldsa44-ed25519@openssh.com`
168151
///
169152
/// Any other algorithms are mapped to the [`Algorithm::Other`] variant.
170153
///
@@ -188,9 +171,7 @@ impl Algorithm {
188171
/// - `ssh-ed25519-cert-v01@openssh.com`
189172
/// - `sk-ecdsa-sha2-nistp256-cert-v01@openssh.com` (FIDO/U2F key)
190173
/// - `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`
174+
/// - `ssh-mldsa44-ed25519-cert-v01@openssh.com`
194175
///
195176
/// Any other algorithms are mapped to the [`Algorithm::Other`] variant.
196177
///
@@ -220,15 +201,7 @@ impl Algorithm {
220201
}),
221202
CERT_SK_ECDSA_SHA2_P256 => Ok(Algorithm::SkEcdsaSha2NistP256),
222203
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-
}),
204+
CERT_MLDSA44_ED25519 => Ok(Algorithm::MlDsa44Ed25519),
232205
#[cfg(feature = "alloc")]
233206
_ => Ok(Algorithm::Other(AlgorithmName::from_certificate_type(id)?)),
234207
#[cfg(not(feature = "alloc"))]
@@ -254,11 +227,7 @@ impl Algorithm {
254227
},
255228
Algorithm::SkEcdsaSha2NistP256 => SK_ECDSA_SHA2_P256,
256229
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-
},
230+
Algorithm::MlDsa44Ed25519 => SSH_MLDSA44_ED25519,
262231
#[cfg(feature = "alloc")]
263232
Algorithm::Other(algorithm) => algorithm.as_str(),
264233
}
@@ -291,11 +260,7 @@ impl Algorithm {
291260
} => CERT_RSA_SHA2_512,
292261
Algorithm::SkEcdsaSha2NistP256 => CERT_SK_ECDSA_SHA2_P256,
293262
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-
},
263+
Algorithm::MlDsa44Ed25519 => CERT_MLDSA44_ED25519,
299264
Algorithm::Other(algorithm) => return algorithm.certificate_type(),
300265
}
301266
.to_owned()
@@ -325,10 +290,10 @@ impl Algorithm {
325290
matches!(self, Algorithm::Rsa { .. })
326291
}
327292

328-
/// Is the algorithm ML-DSA?
293+
/// Is the algorithm the MLDSA44-Ed25519 composite scheme?
329294
#[must_use]
330-
pub fn is_mldsa(self) -> bool {
331-
matches!(self, Algorithm::MlDsa { .. })
295+
pub fn is_mldsa44_ed25519(self) -> bool {
296+
matches!(self, Algorithm::MlDsa44Ed25519)
332297
}
333298

334299
/// Return an error indicating this algorithm is unsupported.
@@ -377,15 +342,7 @@ impl str::FromStr for Algorithm {
377342
SSH_RSA => Ok(Algorithm::Rsa { hash: None }),
378343
SK_ECDSA_SHA2_P256 => Ok(Algorithm::SkEcdsaSha2NistP256),
379344
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-
}),
345+
SSH_MLDSA44_ED25519 => Ok(Algorithm::MlDsa44Ed25519),
389346
#[cfg(feature = "alloc")]
390347
_ => Ok(Algorithm::Other(AlgorithmName::from_str(id)?)),
391348
#[cfg(not(feature = "alloc"))]
@@ -476,108 +433,6 @@ impl str::FromStr for EcdsaCurve {
476433
}
477434
}
478435

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-
581436
/// Hashing algorithms a.k.a. digest functions.
582437
#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)]
583438
#[non_exhaustive]

ssh-key/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ mod kdf;
139139
mod comment;
140140
#[cfg(feature = "std")]
141141
mod dot_ssh;
142+
#[cfg(feature = "mldsa-eddsa")]
143+
mod mldsa_eddsa;
142144
#[cfg(feature = "ppk")]
143145
mod ppk;
144146
#[cfg(feature = "alloc")]
@@ -147,7 +149,7 @@ mod signature;
147149
mod sshsig;
148150

149151
pub use crate::{
150-
algorithm::{Algorithm, AssociatedHashAlg, EcdsaCurve, HashAlg, KdfAlg, MlDsaParams},
152+
algorithm::{Algorithm, AssociatedHashAlg, EcdsaCurve, HashAlg, KdfAlg},
151153
authorized_keys::AuthorizedKeys,
152154
error::{Error, Result},
153155
fingerprint::Fingerprint,

0 commit comments

Comments
 (0)