Skip to content

Commit e860090

Browse files
committed
feat(ssh-key): add ML-DSA public key type
1 parent ce1e453 commit e860090

3 files changed

Lines changed: 162 additions & 0 deletions

File tree

ssh-key/src/public.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ mod dsa;
88
mod ecdsa;
99
mod ed25519;
1010
mod key_data;
11+
#[cfg(feature = "mldsa")]
12+
mod mldsa;
1113
#[cfg(feature = "alloc")]
1214
mod opaque;
1315
#[cfg(feature = "alloc")]
@@ -27,6 +29,9 @@ pub use self::{
2729
#[cfg(feature = "ecdsa")]
2830
pub use self::{ecdsa::EcdsaPublicKey, sk::SkEcdsaSha2NistP256};
2931

32+
#[cfg(feature = "mldsa")]
33+
pub use self::mldsa::MlDsaPublicKey;
34+
3035
pub(crate) use self::ssh_format::SshFormat;
3136

3237
use crate::{Algorithm, Error, Fingerprint, HashAlg, Result};
@@ -448,6 +453,13 @@ impl From<Ed25519PublicKey> for PublicKey {
448453
}
449454
}
450455

456+
#[cfg(feature = "mldsa")]
457+
impl From<MlDsaPublicKey> for PublicKey {
458+
fn from(public_key: MlDsaPublicKey) -> PublicKey {
459+
KeyData::from(public_key).into()
460+
}
461+
}
462+
451463
#[cfg(feature = "alloc")]
452464
impl From<RsaPublicKey> for PublicKey {
453465
fn from(public_key: RsaPublicKey) -> PublicKey {

ssh-key/src/public/key_data.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ use {
1414
#[cfg(feature = "ecdsa")]
1515
use super::{EcdsaPublicKey, SkEcdsaSha2NistP256};
1616

17+
#[cfg(feature = "mldsa")]
18+
use super::MlDsaPublicKey;
19+
1720
/// Public key data.
1821
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
1922
#[non_exhaustive]
@@ -33,6 +36,10 @@ pub enum KeyData {
3336
#[cfg(feature = "alloc")]
3437
Rsa(RsaPublicKey),
3538

39+
/// ML-DSA public key data.
40+
#[cfg(feature = "mldsa")]
41+
MlDsa(MlDsaPublicKey),
42+
3643
/// Security Key (FIDO/U2F) using ECDSA/NIST P-256 as specified in [PROTOCOL.u2f].
3744
///
3845
/// [PROTOCOL.u2f]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.u2f?annotate=HEAD
@@ -69,6 +76,8 @@ impl KeyData {
6976
Self::Ed25519(_) => Algorithm::Ed25519,
7077
#[cfg(feature = "alloc")]
7178
Self::Rsa(_) => Algorithm::Rsa { hash: None },
79+
#[cfg(feature = "mldsa")]
80+
Self::MlDsa(key) => key.algorithm(),
7281
#[cfg(feature = "ecdsa")]
7382
Self::SkEcdsaSha2NistP256(_) => Algorithm::SkEcdsaSha2NistP256,
7483
Self::SkEd25519(_) => Algorithm::SkEd25519,
@@ -127,6 +136,16 @@ impl KeyData {
127136
}
128137
}
129138

139+
/// Get ML-DSA public key if this key is the correct type.
140+
#[cfg(feature = "mldsa")]
141+
#[must_use]
142+
pub fn mldsa(&self) -> Option<&MlDsaPublicKey> {
143+
match self {
144+
Self::MlDsa(key) => Some(key),
145+
_ => None,
146+
}
147+
}
148+
130149
/// Get FIDO/U2F ECDSA/NIST P-256 public key if this key is the correct type.
131150
#[cfg(feature = "ecdsa")]
132151
#[must_use]
@@ -203,6 +222,13 @@ impl KeyData {
203222
matches!(self, Self::Rsa(_))
204223
}
205224

225+
/// Is this key an ML-DSA key?
226+
#[cfg(feature = "mldsa")]
227+
#[must_use]
228+
pub fn is_mldsa(&self) -> bool {
229+
matches!(self, Self::MlDsa(_))
230+
}
231+
206232
/// Is this key a FIDO/U2F ECDSA/NIST P-256 key?
207233
#[cfg(feature = "ecdsa")]
208234
#[must_use]
@@ -248,6 +274,10 @@ impl KeyData {
248274
Algorithm::Ed25519 => Ed25519PublicKey::decode(reader).map(Self::Ed25519),
249275
#[cfg(feature = "alloc")]
250276
Algorithm::Rsa { .. } => RsaPublicKey::decode(reader).map(Self::Rsa),
277+
#[cfg(feature = "mldsa")]
278+
Algorithm::MlDsa { params } => {
279+
MlDsaPublicKey::decode_as(reader, params).map(Self::MlDsa)
280+
}
251281
#[cfg(feature = "ecdsa")]
252282
Algorithm::SkEcdsaSha2NistP256 => {
253283
SkEcdsaSha2NistP256::decode(reader).map(Self::SkEcdsaSha2NistP256)
@@ -280,6 +310,8 @@ impl KeyData {
280310
Self::Ed25519(key) => key.encoded_len(),
281311
#[cfg(feature = "alloc")]
282312
Self::Rsa(key) => key.encoded_len(),
313+
#[cfg(feature = "mldsa")]
314+
Self::MlDsa(key) => key.encoded_len(),
283315
#[cfg(feature = "ecdsa")]
284316
Self::SkEcdsaSha2NistP256(sk) => sk.encoded_len(),
285317
Self::SkEd25519(sk) => sk.encoded_len(),
@@ -300,6 +332,8 @@ impl KeyData {
300332
Self::Ed25519(key) => key.encode(writer),
301333
#[cfg(feature = "alloc")]
302334
Self::Rsa(key) => key.encode(writer),
335+
#[cfg(feature = "mldsa")]
336+
Self::MlDsa(key) => key.encode(writer),
303337
#[cfg(feature = "ecdsa")]
304338
Self::SkEcdsaSha2NistP256(sk) => sk.encode(writer),
305339
Self::SkEd25519(sk) => sk.encode(writer),
@@ -380,6 +414,13 @@ impl From<RsaPublicKey> for KeyData {
380414
}
381415
}
382416

417+
#[cfg(feature = "mldsa")]
418+
impl From<MlDsaPublicKey> for KeyData {
419+
fn from(public_key: MlDsaPublicKey) -> KeyData {
420+
Self::MlDsa(public_key)
421+
}
422+
}
423+
383424
#[cfg(feature = "ecdsa")]
384425
impl From<SkEcdsaSha2NistP256> for KeyData {
385426
fn from(public_key: SkEcdsaSha2NistP256) -> KeyData {

ssh-key/src/public/mldsa.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//! ML-DSA public keys.
2+
//!
3+
//! Module-Lattice-Based Digital Signature Algorithm (ML-DSA) as specified in [FIPS204].
4+
//!
5+
//! [FIPS204]: https://csrc.nist.gov/pubs/fips/204/final
6+
7+
use crate::{Algorithm, MlDsaParams, Result};
8+
use alloc::boxed::Box;
9+
use encoding::{Encode, Reader, Writer};
10+
use ml_dsa::{EncodedVerifyingKey, MlDsa44, MlDsa65, MlDsa87};
11+
12+
/// ML-DSA public key.
13+
///
14+
///
15+
/// [draft-sfluhrer-ssh-mldsa]: https://datatracker.ietf.org/doc/draft-sfluhrer-ssh-mldsa/
16+
/// [FIPS204]: https://csrc.nist.gov/pubs/fips/204/final
17+
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
18+
pub enum MlDsaPublicKey {
19+
/// ML-DSA-44 public key.
20+
MlDsa44(Box<EncodedVerifyingKey<MlDsa44>>),
21+
22+
/// ML-DSA-65 public key.
23+
MlDsa65(Box<EncodedVerifyingKey<MlDsa65>>),
24+
25+
/// ML-DSA-87 public key.
26+
MlDsa87(Box<EncodedVerifyingKey<MlDsa87>>),
27+
}
28+
29+
impl MlDsaPublicKey {
30+
/// Maximum size in bytes of a FIPS 204 public key across all parameter sets
31+
/// (i.e. the ML-DSA-87 public key size).
32+
const MAX_SIZE: usize = 2592;
33+
34+
/// Create a new ML-DSA public key from raw FIPS 204 public key bytes for the
35+
/// given parameter set.
36+
///
37+
/// # Errors
38+
/// Returns [`Error::Encoding`] with [`encoding::Error::Length`] if the key
39+
/// length does not match the parameter set's public key size.
40+
pub fn new(params: MlDsaParams, key: impl AsRef<[u8]>) -> Result<Self> {
41+
let key = key.as_ref();
42+
43+
Ok(match params {
44+
MlDsaParams::MlDsa44 => Self::MlDsa44(Box::new(
45+
EncodedVerifyingKey::<MlDsa44>::try_from(key).map_err(|_| encoding::Error::Length)?,
46+
)),
47+
MlDsaParams::MlDsa65 => Self::MlDsa65(Box::new(
48+
EncodedVerifyingKey::<MlDsa65>::try_from(key).map_err(|_| encoding::Error::Length)?,
49+
)),
50+
MlDsaParams::MlDsa87 => Self::MlDsa87(Box::new(
51+
EncodedVerifyingKey::<MlDsa87>::try_from(key).map_err(|_| encoding::Error::Length)?,
52+
)),
53+
})
54+
}
55+
56+
/// Get the [`MlDsaParams`] parameter set for this public key.
57+
#[must_use]
58+
pub fn params(&self) -> MlDsaParams {
59+
match self {
60+
Self::MlDsa44(_) => MlDsaParams::MlDsa44,
61+
Self::MlDsa65(_) => MlDsaParams::MlDsa65,
62+
Self::MlDsa87(_) => MlDsaParams::MlDsa87,
63+
}
64+
}
65+
66+
/// Get the [`Algorithm`] for this public key.
67+
#[must_use]
68+
pub fn algorithm(&self) -> Algorithm {
69+
Algorithm::MlDsa {
70+
params: self.params(),
71+
}
72+
}
73+
74+
/// Get the raw FIPS 204 public key bytes.
75+
#[must_use]
76+
pub fn as_bytes(&self) -> &[u8] {
77+
match self {
78+
Self::MlDsa44(key) => key.as_slice(),
79+
Self::MlDsa65(key) => key.as_slice(),
80+
Self::MlDsa87(key) => key.as_slice(),
81+
}
82+
}
83+
84+
/// Decode an ML-DSA public key for the given parameter set.
85+
///
86+
/// The parameter set is not encoded in the key body; it is taken from the
87+
/// SSH algorithm identifier (see [`MlDsaPublicKey`]).
88+
pub(crate) fn decode_as(reader: &mut impl Reader, params: MlDsaParams) -> Result<Self> {
89+
let mut buf = [0u8; Self::MAX_SIZE];
90+
let key = reader.read_byten(&mut buf)?;
91+
Self::new(params, key)
92+
}
93+
}
94+
95+
impl AsRef<[u8]> for MlDsaPublicKey {
96+
fn as_ref(&self) -> &[u8] {
97+
self.as_bytes()
98+
}
99+
}
100+
101+
impl Encode for MlDsaPublicKey {
102+
fn encoded_len(&self) -> encoding::Result<usize> {
103+
self.as_bytes().encoded_len()
104+
}
105+
106+
fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> {
107+
self.as_bytes().encode(writer)
108+
}
109+
}

0 commit comments

Comments
 (0)