Skip to content

Commit 91a867f

Browse files
committed
feat(ssh-key): add ML-DSA public key type
1 parent ae800ad commit 91a867f

3 files changed

Lines changed: 142 additions & 1 deletion

File tree

ssh-key/src/public.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ mod ecdsa;
99
mod ed25519;
1010
mod key_data;
1111
#[cfg(feature = "alloc")]
12+
mod mldsa;
13+
#[cfg(feature = "alloc")]
1214
mod opaque;
1315
#[cfg(feature = "alloc")]
1416
mod rsa;
@@ -20,6 +22,7 @@ pub use self::{ed25519::Ed25519PublicKey, key_data::KeyData, sk::SkEd25519};
2022
#[cfg(feature = "alloc")]
2123
pub use self::{
2224
dsa::DsaPublicKey,
25+
mldsa::MlDsaPublicKey,
2326
opaque::{OpaquePublicKey, OpaquePublicKeyBytes},
2427
rsa::RsaPublicKey,
2528
};
@@ -448,6 +451,13 @@ impl From<Ed25519PublicKey> for PublicKey {
448451
}
449452
}
450453

454+
#[cfg(feature = "alloc")]
455+
impl From<MlDsaPublicKey> for PublicKey {
456+
fn from(public_key: MlDsaPublicKey) -> PublicKey {
457+
KeyData::from(public_key).into()
458+
}
459+
}
460+
451461
#[cfg(feature = "alloc")]
452462
impl From<RsaPublicKey> for PublicKey {
453463
fn from(public_key: RsaPublicKey) -> PublicKey {

ssh-key/src/public/key_data.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use encoding::{CheckedSum, Decode, Encode, Reader, Writer};
66

77
#[cfg(feature = "alloc")]
88
use {
9-
super::{DsaPublicKey, OpaquePublicKey, RsaPublicKey},
9+
super::{DsaPublicKey, MlDsaPublicKey, OpaquePublicKey, RsaPublicKey},
1010
crate::Certificate,
1111
alloc::boxed::Box,
1212
};
@@ -33,6 +33,10 @@ pub enum KeyData {
3333
#[cfg(feature = "alloc")]
3434
Rsa(RsaPublicKey),
3535

36+
/// ML-DSA public key data.
37+
#[cfg(feature = "alloc")]
38+
MlDsa(MlDsaPublicKey),
39+
3640
/// Security Key (FIDO/U2F) using ECDSA/NIST P-256 as specified in [PROTOCOL.u2f].
3741
///
3842
/// [PROTOCOL.u2f]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.u2f?annotate=HEAD
@@ -69,6 +73,8 @@ impl KeyData {
6973
Self::Ed25519(_) => Algorithm::Ed25519,
7074
#[cfg(feature = "alloc")]
7175
Self::Rsa(_) => Algorithm::Rsa { hash: None },
76+
#[cfg(feature = "alloc")]
77+
Self::MlDsa(key) => key.algorithm(),
7278
#[cfg(feature = "ecdsa")]
7379
Self::SkEcdsaSha2NistP256(_) => Algorithm::SkEcdsaSha2NistP256,
7480
Self::SkEd25519(_) => Algorithm::SkEd25519,
@@ -127,6 +133,16 @@ impl KeyData {
127133
}
128134
}
129135

136+
/// Get ML-DSA public key if this key is the correct type.
137+
#[cfg(feature = "alloc")]
138+
#[must_use]
139+
pub fn mldsa(&self) -> Option<&MlDsaPublicKey> {
140+
match self {
141+
Self::MlDsa(key) => Some(key),
142+
_ => None,
143+
}
144+
}
145+
130146
/// Get FIDO/U2F ECDSA/NIST P-256 public key if this key is the correct type.
131147
#[cfg(feature = "ecdsa")]
132148
#[must_use]
@@ -203,6 +219,13 @@ impl KeyData {
203219
matches!(self, Self::Rsa(_))
204220
}
205221

222+
/// Is this key an ML-DSA key?
223+
#[cfg(feature = "alloc")]
224+
#[must_use]
225+
pub fn is_mldsa(&self) -> bool {
226+
matches!(self, Self::MlDsa(_))
227+
}
228+
206229
/// Is this key a FIDO/U2F ECDSA/NIST P-256 key?
207230
#[cfg(feature = "ecdsa")]
208231
#[must_use]
@@ -248,6 +271,10 @@ impl KeyData {
248271
Algorithm::Ed25519 => Ed25519PublicKey::decode(reader).map(Self::Ed25519),
249272
#[cfg(feature = "alloc")]
250273
Algorithm::Rsa { .. } => RsaPublicKey::decode(reader).map(Self::Rsa),
274+
#[cfg(feature = "alloc")]
275+
Algorithm::MlDsa { params } => {
276+
MlDsaPublicKey::decode_as(reader, params).map(Self::MlDsa)
277+
}
251278
#[cfg(feature = "ecdsa")]
252279
Algorithm::SkEcdsaSha2NistP256 => {
253280
SkEcdsaSha2NistP256::decode(reader).map(Self::SkEcdsaSha2NistP256)
@@ -280,6 +307,8 @@ impl KeyData {
280307
Self::Ed25519(key) => key.encoded_len(),
281308
#[cfg(feature = "alloc")]
282309
Self::Rsa(key) => key.encoded_len(),
310+
#[cfg(feature = "alloc")]
311+
Self::MlDsa(key) => key.encoded_len(),
283312
#[cfg(feature = "ecdsa")]
284313
Self::SkEcdsaSha2NistP256(sk) => sk.encoded_len(),
285314
Self::SkEd25519(sk) => sk.encoded_len(),
@@ -300,6 +329,8 @@ impl KeyData {
300329
Self::Ed25519(key) => key.encode(writer),
301330
#[cfg(feature = "alloc")]
302331
Self::Rsa(key) => key.encode(writer),
332+
#[cfg(feature = "alloc")]
333+
Self::MlDsa(key) => key.encode(writer),
303334
#[cfg(feature = "ecdsa")]
304335
Self::SkEcdsaSha2NistP256(sk) => sk.encode(writer),
305336
Self::SkEd25519(sk) => sk.encode(writer),
@@ -380,6 +411,13 @@ impl From<RsaPublicKey> for KeyData {
380411
}
381412
}
382413

414+
#[cfg(feature = "alloc")]
415+
impl From<MlDsaPublicKey> for KeyData {
416+
fn from(public_key: MlDsaPublicKey) -> KeyData {
417+
Self::MlDsa(public_key)
418+
}
419+
}
420+
383421
#[cfg(feature = "ecdsa")]
384422
impl From<SkEcdsaSha2NistP256> for KeyData {
385423
fn from(public_key: SkEcdsaSha2NistP256) -> KeyData {

ssh-key/src/public/mldsa.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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::vec::Vec;
9+
use encoding::{Decode, Encode, Reader, Writer};
10+
11+
/// ML-DSA public key.
12+
///
13+
/// SSH encodings for ML-DSA public keys are described in
14+
/// [draft-sfluhrer-ssh-mldsa]:
15+
///
16+
/// Here, 'key' is the public key as described in [FIPS204].
17+
///
18+
/// This type represents the `key` portion of the encoding together with the
19+
/// [`MlDsaParams`] parameter set which is derived from the algorithm name.
20+
///
21+
/// [draft-sfluhrer-ssh-mldsa]: https://datatracker.ietf.org/doc/draft-sfluhrer-ssh-mldsa/
22+
/// [FIPS204]: https://csrc.nist.gov/pubs/fips/204/final
23+
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
24+
pub struct MlDsaPublicKey {
25+
/// ML-DSA parameter set.
26+
params: MlDsaParams,
27+
28+
/// Raw FIPS 204 public key bytes.
29+
/// https://sfluhrer.github.io/ssh-mldsa/draft-sfluhrer-ssh-mldsa.html#name-public-key-format
30+
key: Vec<u8>,
31+
}
32+
33+
impl MlDsaPublicKey {
34+
/// Create a new ML-DSA public key from raw public key bytes.
35+
///
36+
/// # Errors
37+
/// Returns an error if the key length does not match the parameter set's
38+
/// public key size.
39+
pub fn new(params: MlDsaParams, key: impl Into<Vec<u8>>) -> Result<Self> {
40+
let key = key.into();
41+
42+
if key.len() != params.public_key_size() {
43+
return Err(encoding::Error::Length.into());
44+
}
45+
46+
Ok(Self { params, key })
47+
}
48+
49+
/// Get the [`MlDsaParams`] parameter set for this public key.
50+
#[must_use]
51+
pub fn params(&self) -> MlDsaParams {
52+
self.params
53+
}
54+
55+
/// Get the [`Algorithm`] for this public key.
56+
#[must_use]
57+
pub fn algorithm(&self) -> Algorithm {
58+
Algorithm::MlDsa {
59+
params: self.params,
60+
}
61+
}
62+
63+
/// Get the raw FIPS 204 public key bytes.
64+
#[must_use]
65+
pub fn as_bytes(&self) -> &[u8] {
66+
&self.key
67+
}
68+
69+
/// Decode an ML-DSA public key for the given parameter set.
70+
///
71+
/// The parameter set is not encoded in the key body; it is taken from the
72+
/// SSH algorithm identifier (see [`MlDsaPublicKey`]).
73+
pub(crate) fn decode_as(reader: &mut impl Reader, params: MlDsaParams) -> Result<Self> {
74+
let key = Vec::decode(reader)?;
75+
Self::new(params, key)
76+
}
77+
}
78+
79+
impl AsRef<[u8]> for MlDsaPublicKey {
80+
fn as_ref(&self) -> &[u8] {
81+
self.as_bytes()
82+
}
83+
}
84+
85+
impl Encode for MlDsaPublicKey {
86+
fn encoded_len(&self) -> encoding::Result<usize> {
87+
self.key.encoded_len()
88+
}
89+
90+
fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> {
91+
self.key.encode(writer)
92+
}
93+
}

0 commit comments

Comments
 (0)