From 241213253a934a4436083e7936f447e6b0bc0e9b Mon Sep 17 00:00:00 2001 From: Ivan Sherov-Ignatev Date: Mon, 15 Jun 2026 21:39:12 +0200 Subject: [PATCH] Stash --- Cargo.lock | 303 ++++++++++++++++++ ssh-key/Cargo.toml | 3 +- ssh-key/src/algorithm.rs | 12 + ssh-key/src/private.rs | 2 + ssh-key/src/private/keypair.rs | 37 +++ ssh-key/src/private/mldsa44_ed25519.rs | 297 +++++++++++++++++ ssh-key/src/public.rs | 6 +- ssh-key/src/public/key_data.rs | 31 +- ssh-key/src/public/mldsa44_ed25519.rs | 129 ++++++++ ssh-key/tests/examples/id_mldsa44_ed25519 | 58 ++++ ssh-key/tests/examples/id_mldsa44_ed25519.pub | 1 + ssh-key/tests/private_key.rs | 34 ++ ssh-key/tests/public_key.rs | 31 ++ 13 files changed, 941 insertions(+), 3 deletions(-) create mode 100644 ssh-key/src/private/mldsa44_ed25519.rs create mode 100644 ssh-key/src/public/mldsa44_ed25519.rs create mode 100644 ssh-key/tests/examples/id_mldsa44_ed25519 create mode 100644 ssh-key/tests/examples/id_mldsa44_ed25519.pub diff --git a/Cargo.lock b/Cargo.lock index 86cbe750..a34e77db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,12 @@ dependencies = [ "cipher", ] +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "byteorder" version = "1.5.0" @@ -176,6 +182,17 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "core-models" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c406a8d3cdec6393dc8975b623d806ce2d586653c620f86f7fab2e043df1cb2" +dependencies = [ + "hax-lib", + "pastey", + "rand", +] + [[package]] name = "cpubits" version = "0.1.1" @@ -403,6 +420,30 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -452,6 +493,43 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "hax-lib" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543f93241d32b3f00569201bfce9d7a93c92c6421b23c77864ac929dc947b9fc" +dependencies = [ + "hax-lib-macros", + "num-bigint", + "num-traits", +] + +[[package]] +name = "hax-lib-macros" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8755751e760b11021765bb04cb4a6c4e24742688d9f3aa14c2079638f537b0f" +dependencies = [ + "hax-lib-macros-types", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "hax-lib-macros-types" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f177c9ae8ea456e2f71ff3c1ea47bf4464f772a05133fcbba56cd5ba169035a2" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "heck" version = "0.5.0" @@ -523,6 +601,17 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -535,6 +624,80 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libcrux-intrinsics" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b254f1797aecd888f76e9647e6bec7b4c26fb6d60a73fd9856e4a1e535c704" +dependencies = [ + "core-models", + "hax-lib", +] + +[[package]] +name = "libcrux-macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd6aa2dcd5be681662001b81d493f1569c6d49a32361f470b0c955465cd0338" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "libcrux-ml-dsa" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b067d3ea26eea867aad034b4567f7410ddf1807cab36aa82d3d610a18a06e70e" +dependencies = [ + "core-models", + "hax-lib", + "libcrux-intrinsics", + "libcrux-macros", + "libcrux-platform", + "libcrux-sha3", +] + +[[package]] +name = "libcrux-platform" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9e21d7ed31a92ac539bd69a8c970b183ee883872d2d19ce27036e24cb8ecc4" +dependencies = [ + "libc", +] + +[[package]] +name = "libcrux-secrets" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce650f3041b44ba40d4263852347d007cd2cd9d1cc856a6f6c8b2e10c3fd40b" +dependencies = [ + "hax-lib", +] + +[[package]] +name = "libcrux-sha3" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f314521d5115afff6466e35e82cecd3b3bdf1dbed80d69285769a0f51c74fcc7" +dependencies = [ + "hax-lib", + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-traits", +] + +[[package]] +name = "libcrux-traits" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2613bf3dbf3670777bd6ecc3bcdd2d7a642663656b35ed2823529c4f1db0c9e9" +dependencies = [ + "libcrux-secrets", + "rand", +] + [[package]] name = "libm" version = "0.2.16" @@ -553,6 +716,25 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -562,6 +744,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + [[package]] name = "p256" version = "0.14.0-rc.10" @@ -612,6 +800,12 @@ dependencies = [ "phc", ] +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "pbkdf2" version = "0.13.0" @@ -640,6 +834,12 @@ dependencies = [ "ctutils", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "poly1305" version = "0.9.0" @@ -695,6 +895,28 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -719,6 +941,17 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom", + "rand_core", +] + [[package]] name = "rand_core" version = "0.10.1" @@ -760,6 +993,12 @@ dependencies = [ "semver", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "sec1" version = "0.8.1" @@ -787,6 +1026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -864,6 +1104,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "ssh-cipher" version = "0.3.0-rc.10" @@ -919,6 +1165,7 @@ dependencies = [ "hex", "hex-literal", "hmac", + "libcrux-ml-dsa", "p256", "p384", "p521", @@ -988,6 +1235,17 @@ dependencies = [ "ctutils", ] +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasip2" version = "1.0.3+wasi-0.2.9" @@ -1006,6 +1264,51 @@ dependencies = [ "wit-bindgen 0.51.0", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wasm-encoder" version = "0.244.0" diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index 3d586ad1..8d29195d 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -50,6 +50,7 @@ rsa = { version = "0.10.0-rc.18", optional = true, default-features = false, fea sec1 = { version = "0.8", optional = true, default-features = false, features = ["point"] } serde = { version = "1.0.16", optional = true } sha1 = { version = "0.11", optional = true, default-features = false, features = ["oid"] } +libcrux-ml-dsa = { version = "0.0.9", optional = true } [dev-dependencies] hex-literal = "1" @@ -64,7 +65,7 @@ std = ["alloc"] crypto = ["ed25519", "p256", "p384", "p521", "rsa"] # NOTE: `dsa` is obsolete/weak dsa = ["dep:dsa", "dep:sha1", "alloc", "encoding/bigint", "signature/rand_core"] ecdsa = ["dep:sec1"] -ed25519 = ["dep:ed25519-dalek", "rand_core"] +ed25519 = ["dep:ed25519-dalek", "dep:libcrux-ml-dsa", "rand_core"] encryption = [ "dep:bcrypt-pbkdf", "alloc", diff --git a/ssh-key/src/algorithm.rs b/ssh-key/src/algorithm.rs index ac644928..1f54a109 100644 --- a/ssh-key/src/algorithm.rs +++ b/ssh-key/src/algorithm.rs @@ -86,6 +86,12 @@ const SK_ECDSA_SHA2_P256: &str = "sk-ecdsa-sha2-nistp256@openssh.com"; /// U2F/FIDO security key with Ed25519 const SK_SSH_ED25519: &str = "sk-ssh-ed25519@openssh.com"; +/// ML-DSA-44 + Ed25519 key +const SSH_MLDSA44_ED25519: &str = "ssh-mldsa44-ed25519@openssh.com"; + +/// OpenSSH certificate for ML-DSA-44 + Ed25519 key +const CERT_SSH_MLDSA44_ED25519: &str = "ssh-mldsa44-ed25519-cert-v01@openssh.com"; + /// SSH key algorithms, i.e. digital signature algorithms used with SSH private/public keys. #[derive(Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] #[non_exhaustive] @@ -121,6 +127,9 @@ pub enum Algorithm { /// FIDO/U2F key with Ed25519 SkEd25519, + /// ML-DSA-44 + Ed25519 + Mldsa44Ed25519, + /// Other #[cfg(feature = "alloc")] Other(AlgorithmName), @@ -215,6 +224,7 @@ impl Algorithm { }, Algorithm::SkEcdsaSha2NistP256 => SK_ECDSA_SHA2_P256, Algorithm::SkEd25519 => SK_SSH_ED25519, + Algorithm::Mldsa44Ed25519 => SSH_MLDSA44_ED25519, #[cfg(feature = "alloc")] Algorithm::Other(algorithm) => algorithm.as_str(), } @@ -247,6 +257,7 @@ impl Algorithm { } => CERT_RSA_SHA2_512, Algorithm::SkEcdsaSha2NistP256 => CERT_SK_ECDSA_SHA2_P256, Algorithm::SkEd25519 => CERT_SK_SSH_ED25519, + Algorithm::Mldsa44Ed25519 => CERT_SSH_MLDSA44_ED25519, Algorithm::Other(algorithm) => return algorithm.certificate_type(), } .to_owned() @@ -322,6 +333,7 @@ impl str::FromStr for Algorithm { SSH_RSA => Ok(Algorithm::Rsa { hash: None }), SK_ECDSA_SHA2_P256 => Ok(Algorithm::SkEcdsaSha2NistP256), SK_SSH_ED25519 => Ok(Algorithm::SkEd25519), + SSH_MLDSA44_ED25519 => Ok(Algorithm::Mldsa44Ed25519), #[cfg(feature = "alloc")] _ => Ok(Algorithm::Other(AlgorithmName::from_str(id)?)), #[cfg(not(feature = "alloc"))] diff --git a/ssh-key/src/private.rs b/ssh-key/src/private.rs index a7bf4a2a..57a26d23 100644 --- a/ssh-key/src/private.rs +++ b/ssh-key/src/private.rs @@ -105,10 +105,12 @@ mod opaque; mod rsa; #[cfg(feature = "alloc")] mod sk; +mod mldsa44_ed25519; pub use self::{ ed25519::{Ed25519Keypair, Ed25519PrivateKey}, keypair::KeypairData, + mldsa44_ed25519::{Mldsa44Ed25519Keypair, Mldsa44Ed25519PrivateKey}, }; #[cfg(feature = "alloc")] diff --git a/ssh-key/src/private/keypair.rs b/ssh-key/src/private/keypair.rs index f187bbaf..b12bd0d3 100644 --- a/ssh-key/src/private/keypair.rs +++ b/ssh-key/src/private/keypair.rs @@ -1,6 +1,7 @@ //! Private key pairs. use super::ed25519::Ed25519Keypair; +use super::mldsa44_ed25519::Mldsa44Ed25519Keypair; use crate::{Algorithm, Error, Result, public}; use ctutils::{Choice, CtEq}; use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; @@ -36,6 +37,9 @@ pub enum KeypairData { /// Ed25519 keypair. Ed25519(Ed25519Keypair), + /// ML-DSA-44 + Ed25519 keypair. + Mldsa44Ed25519(Mldsa44Ed25519Keypair), + /// Encrypted private key (ciphertext). #[cfg(feature = "alloc")] Encrypted(Vec), @@ -73,6 +77,7 @@ impl KeypairData { #[cfg(feature = "ecdsa")] Self::Ecdsa(key) => key.algorithm(), Self::Ed25519(_) => Algorithm::Ed25519, + Self::Mldsa44Ed25519(_) => Algorithm::Mldsa44Ed25519, #[cfg(feature = "alloc")] Self::Encrypted(_) => return Err(Error::Encrypted), #[cfg(feature = "alloc")] @@ -116,6 +121,16 @@ impl KeypairData { } } + /// Get ML-DSA-44 + Ed25519 private key if this key is the correct type. + #[must_use] + pub fn mldsa44ed25519(&self) -> Option<&Mldsa44Ed25519Keypair> { + match self { + Self::Mldsa44Ed25519(key) => Some(key), + #[allow(unreachable_patterns)] + _ => None, + } + } + /// Get the encrypted ciphertext if this key is encrypted. #[cfg(feature = "alloc")] #[must_use] @@ -186,6 +201,12 @@ impl KeypairData { matches!(self, Self::Ed25519(_)) } + /// Is this key an ML-DSA-44 + Ed25519 key? + #[must_use] + pub fn is_mldsa44ed25519(&self) -> bool { + matches!(self, Self::Mldsa44Ed25519(_)) + } + /// Is this key encrypted? #[cfg(not(feature = "alloc"))] #[must_use] @@ -239,6 +260,7 @@ impl KeypairData { #[cfg(feature = "ecdsa")] Self::Ecdsa(ecdsa) => ecdsa.private_key_bytes(), Self::Ed25519(ed25519) => ed25519.private.as_ref(), + Self::Mldsa44Ed25519(mldsa44_ed25519) => &mldsa44_ed25519.private.to_bytes(), // ??? #[cfg(feature = "alloc")] Self::Encrypted(ciphertext) => ciphertext.as_ref(), #[cfg(feature = "alloc")] @@ -276,6 +298,9 @@ impl KeypairData { _ => Err(Error::AlgorithmUnknown), }, Algorithm::Ed25519 => Ed25519Keypair::decode(reader).map(Self::Ed25519), + Algorithm::Mldsa44Ed25519 => { + Mldsa44Ed25519Keypair::decode(reader).map(Self::Mldsa44Ed25519) + } #[cfg(feature = "alloc")] Algorithm::Rsa { .. } => RsaKeypair::decode(reader).map(Self::Rsa), #[cfg(all(feature = "alloc", feature = "ecdsa"))] @@ -303,6 +328,7 @@ impl CtEq for KeypairData { #[cfg(feature = "ecdsa")] (Self::Ecdsa(a), Self::Ecdsa(b)) => a.ct_eq(b), (Self::Ed25519(a), Self::Ed25519(b)) => a.ct_eq(b), + (Self::Mldsa44Ed25519(a), Self::Mldsa44Ed25519(b)) => a.ct_eq(b), #[cfg(feature = "alloc")] (Self::Encrypted(a), Self::Encrypted(b)) => a.ct_eq(b), #[cfg(feature = "alloc")] @@ -359,6 +385,7 @@ impl Encode for KeypairData { #[cfg(feature = "ecdsa")] Self::Ecdsa(key) => key.encoded_len()?, Self::Ed25519(key) => key.encoded_len()?, + Self::Mldsa44Ed25519(key) => key.encoded_len()?, #[cfg(feature = "alloc")] Self::Encrypted(ciphertext) => return Ok(ciphertext.len()), #[cfg(feature = "alloc")] @@ -385,6 +412,7 @@ impl Encode for KeypairData { #[cfg(feature = "ecdsa")] Self::Ecdsa(key) => key.encode(writer)?, Self::Ed25519(key) => key.encode(writer)?, + Self::Mldsa44Ed25519(key) => key.encode(writer)?, #[cfg(feature = "alloc")] Self::Encrypted(ciphertext) => writer.write(ciphertext)?, #[cfg(feature = "alloc")] @@ -411,6 +439,9 @@ impl TryFrom<&KeypairData> for public::KeyData { #[cfg(feature = "ecdsa")] KeypairData::Ecdsa(ecdsa) => public::KeyData::Ecdsa(ecdsa.into()), KeypairData::Ed25519(ed25519) => public::KeyData::Ed25519(ed25519.into()), + KeypairData::Mldsa44Ed25519(mldsa44_ed25519) => { + public::KeyData::Mldsa44Ed25519(mldsa44_ed25519.into()) + } #[cfg(feature = "alloc")] KeypairData::Encrypted(_) => return Err(Error::Encrypted), #[cfg(feature = "alloc")] @@ -447,6 +478,12 @@ impl From for KeypairData { } } +impl From for KeypairData { + fn from(keypair: Mldsa44Ed25519Keypair) -> KeypairData { + Self::Mldsa44Ed25519(keypair) + } +} + #[cfg(feature = "alloc")] impl From for KeypairData { fn from(keypair: RsaKeypair) -> KeypairData { diff --git a/ssh-key/src/private/mldsa44_ed25519.rs b/ssh-key/src/private/mldsa44_ed25519.rs new file mode 100644 index 00000000..e87ff95c --- /dev/null +++ b/ssh-key/src/private/mldsa44_ed25519.rs @@ -0,0 +1,297 @@ +//! ML-DSA-44 + Ed25519 private keys. +//! +//! Based on draft-miller-sshm-mldsa44-ed25519-composite-sigs-00 + +// TODO: move from ed25519 to a separate feature flag + +use crate::{Error, Result, public::Mldsa44Ed25519PublicKey}; +use core::fmt; +use ctutils::{Choice, CtEq}; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; +use zeroize::{Zeroize, Zeroizing}; + +#[cfg(feature = "rand_core")] +use rand_core::CryptoRng; +use rand_core::Rng; +use crate::private::Ed25519PrivateKey; + +/// ML-DSA-44 + Ed25519 private key. +#[derive(Clone)] +pub struct Mldsa44Ed25519PrivateKey { + pub(crate) mldsa44_seed: [u8; Self::MLDSA44_SEED_SIZE], + pub(crate) ed25519_seed: [u8; Self::ED25519_SEED_SIZE], +} + +impl Mldsa44Ed25519PrivateKey { + /// Size of a composite ML-DSA-44 + Ed25519 private key in bytes. + pub const MLDSA44_SEED_SIZE: usize = 32; + pub const ED25519_SEED_SIZE: usize = 32; + pub const BYTE_SIZE: usize = Self::MLDSA44_SEED_SIZE + Self::ED25519_SEED_SIZE; + + /// Generate a random composite ML-DSA-44 + Ed25519 private key. + #[cfg(feature = "rand_core")] + pub fn random(rng: &mut R) -> Self { + let mut mldsa44_seed = [0u8; Self::MLDSA44_SEED_SIZE]; + let mut ed25519_seed = [0u8; Self::ED25519_SEED_SIZE]; + rng.fill_bytes(&mut mldsa44_seed); + rng.fill_bytes(&mut ed25519_seed); + Self{mldsa44_seed, ed25519_seed} + } + + /// Parse ML-DSA-44 + Ed25519 private key from bytes. + #[must_use] + pub fn from_bytes(bytes: &[u8; Self::BYTE_SIZE]) -> Self { + let (mldsa44_bytes, ed25519_bytes) = bytes.split_at(Mldsa44Ed25519PrivateKey::MLDSA44_SEED_SIZE); + let mldsa44_seed: [u8; Mldsa44Ed25519PrivateKey::MLDSA44_SEED_SIZE] = mldsa44_bytes.try_into().expect("Data copy error"); + let ed25519_seed: [u8; Mldsa44Ed25519PrivateKey::ED25519_SEED_SIZE] = ed25519_bytes.try_into().expect("Data copy error"); + Self{mldsa44_seed, ed25519_seed} + } + + /// Convert to the inner byte array. + #[must_use] + pub fn to_bytes(&self) -> [u8; Self::BYTE_SIZE] { + let mut bytes = [0u8; Self::BYTE_SIZE]; + bytes[..Self::MLDSA44_SEED_SIZE].copy_from_slice(&self.mldsa44_seed); + bytes[Self::MLDSA44_SEED_SIZE..].copy_from_slice(&self.ed25519_seed); + bytes + } +} + +impl CtEq for Mldsa44Ed25519PrivateKey { + fn ct_eq(&self, other: &Self) -> Choice { + self.mldsa44_seed.ct_eq(&other.mldsa44_seed) + & self.ed25519_seed.ct_eq(&other.ed25519_seed) + } +} + +impl Eq for Mldsa44Ed25519PrivateKey {} + +impl PartialEq for Mldsa44Ed25519PrivateKey { + fn eq(&self, other: &Self) -> bool { + self.ct_eq(other).into() + } +} + +impl TryFrom<&[u8]> for Mldsa44Ed25519PrivateKey { + type Error = Error; + + fn try_from(bytes: &[u8]) -> Result { + Ok(Mldsa44Ed25519PrivateKey::from_bytes(bytes.try_into()?)) + } +} + +impl fmt::Debug for Mldsa44Ed25519PrivateKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Mldsa44Ed25519PrivateKey").finish_non_exhaustive() + } +} + +impl fmt::LowerHex for Mldsa44Ed25519PrivateKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in self.as_ref() { + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} + +impl fmt::UpperHex for Mldsa44Ed25519PrivateKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in self.as_ref() { + write!(f, "{byte:02X}")?; + } + Ok(()) + } +} + +impl Drop for Mldsa44Ed25519PrivateKey { + fn drop(&mut self) { + self.0.zeroize(); + } +} + +#[cfg(feature = "ed25519")] +impl From for ed25519_dalek::SigningKey { + fn from(key: Mldsa44Ed25519PrivateKey) -> ed25519_dalek::SigningKey { + ed25519_dalek::SigningKey::from(&key) + } +} + +#[cfg(feature = "ed25519")] +impl From<&Mldsa44Ed25519PrivateKey> for ed25519_dalek::SigningKey { + fn from(key: &Mldsa44Ed25519PrivateKey) -> ed25519_dalek::SigningKey { + ed25519_dalek::SigningKey::from_bytes(key.as_ref()) + } +} + +#[cfg(feature = "ed25519")] +impl From for Mldsa44Ed25519PrivateKey { + fn from(key: ed25519_dalek::SigningKey) -> Mldsa44Ed25519PrivateKey { + Mldsa44Ed25519PrivateKey::from(&key) + } +} + +#[cfg(feature = "ed25519")] +impl From<&ed25519_dalek::SigningKey> for Mldsa44Ed25519PrivateKey { + fn from(key: &ed25519_dalek::SigningKey) -> Mldsa44Ed25519PrivateKey { + Mldsa44Ed25519PrivateKey(key.to_bytes()) + } +} + +#[cfg(feature = "ed25519")] +impl From for Mldsa44Ed25519PublicKey { + fn from(private: Mldsa44Ed25519PrivateKey) -> Mldsa44Ed25519PublicKey { + Mldsa44Ed25519PublicKey::from(&private) + } +} + +#[cfg(feature = "ed25519")] +impl From<&Mldsa44Ed25519PrivateKey> for Mldsa44Ed25519PublicKey { + fn from(private: &Mldsa44Ed25519PrivateKey) -> Mldsa44Ed25519PublicKey { + ed25519_dalek::SigningKey::from(private) + .verifying_key() + .into() + } +} + +/// Ed25519 private/public keypair. +#[derive(Clone)] +pub struct Mldsa44Ed25519Keypair { + /// Public key. + pub public: Mldsa44Ed25519PublicKey, + + /// Private key. + pub private: Mldsa44Ed25519PrivateKey, +} + +impl Mldsa44Ed25519Keypair { + /// Size of an Ed25519 keypair in bytes. + pub const BYTE_SIZE: usize = 64; + + /// Generate a random Ed25519 private keypair. + #[cfg(feature = "ed25519")] + pub fn random(rng: &mut R) -> Self { + let mut bytes = [0u8; Mldsa44Ed25519PrivateKey::BYTE_SIZE]; + rng.fill_bytes(&mut bytes); + Mldsa44Ed25519PrivateKey::from_bytes(&bytes).into() + } + + /// Expand a keypair from a 32-byte seed value. + #[cfg(feature = "ed25519")] + #[must_use] + pub fn from_seed(seed: &[u8; Mldsa44Ed25519PrivateKey::BYTE_SIZE]) -> Self { + Mldsa44Ed25519PrivateKey::from_bytes(seed).into() + } + + /// Parse ML-DSA-44 and Ed25519 seeds from 64-bytes private key + /// + /// # Errors + /// Returns [`Error::Crypto`] if the public key does not match the private key. + pub fn from_bytes(bytes: &[u8; Self::BYTE_SIZE]) -> Result { + let (mldsa44_bytes, ed25519_bytes) = bytes.split_at(Mldsa44Ed25519PrivateKey::MLDSA44_SEED_SIZE); + let mldsa44_seed: [u8; Mldsa44Ed25519PrivateKey::MLDSA44_SEED_SIZE] = mldsa44_bytes.try_into()?; + let ed25519_seed: [u8; Mldsa44Ed25519PrivateKey::ED25519_SEED_SIZE] = ed25519_bytes.try_into()?; + + let private = Mldsa44Ed25519PrivateKey{mldsa44_seed, ed25519_seed}; + let public = Mldsa44Ed25519PublicKey::from(&private); + + Ok(Mldsa44Ed25519Keypair { private, public }) + } + + /// Serialize an Ed25519 keypair as bytes. + #[must_use] + #[allow(clippy::integer_division_remainder_used, reason = "constant")] + pub fn to_bytes(&self) -> [u8; Self::BYTE_SIZE] { + let mut result = [0u8; Self::BYTE_SIZE]; + result[..(Self::BYTE_SIZE / 2)].copy_from_slice(self.private.as_ref()); + result[(Self::BYTE_SIZE / 2)..].copy_from_slice(self.public.as_ref()); + result + } +} + +impl CtEq for Mldsa44Ed25519Keypair { + fn ct_eq(&self, other: &Self) -> Choice { + Choice::from(u8::from(self.public == other.public)) & self.private.ct_eq(&other.private) + } +} + +impl Eq for Mldsa44Ed25519Keypair {} + +impl PartialEq for Mldsa44Ed25519Keypair { + fn eq(&self, other: &Self) -> bool { + self.ct_eq(other).into() + } +} + +impl Decode for Mldsa44Ed25519Keypair { + type Error = Error; + + fn decode(reader: &mut impl Reader) -> Result { + // Decode private key + let public = Mldsa44Ed25519PublicKey::decode(reader)?; + + // The OpenSSH serialization of Ed25519 keys is repetitive and includes + // a serialization of `private_key[32] || public_key[32]` immediately + // following the public key. + let mut bytes = Zeroizing::new([0u8; Self::BYTE_SIZE]); + reader.read_prefixed(|reader| reader.read(&mut *bytes))?; + + let keypair = Self::from_bytes(&bytes)?; + + // Ensure public key matches the one one the keypair + if keypair.public == public { + Ok(keypair) + } else { + Err(Error::Crypto) + } + } +} + +impl Encode for Mldsa44Ed25519Keypair { + fn encoded_len(&self) -> encoding::Result { + [4, self.public.encoded_len()?, Self::BYTE_SIZE].checked_sum() + } + + fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> { + self.public.encode(writer)?; + Zeroizing::new(self.to_bytes()).as_slice().encode(writer)?; + Ok(()) + } +} + +impl From for Mldsa44Ed25519PublicKey { + fn from(keypair: Mldsa44Ed25519Keypair) -> Mldsa44Ed25519PublicKey { + keypair.public + } +} + +impl From<&Mldsa44Ed25519Keypair> for Mldsa44Ed25519PublicKey { + fn from(keypair: &Mldsa44Ed25519Keypair) -> Mldsa44Ed25519PublicKey { + keypair.public + } +} + +impl TryFrom<&[u8]> for Mldsa44Ed25519Keypair { + type Error = Error; + + fn try_from(bytes: &[u8]) -> Result { + Mldsa44Ed25519Keypair::from_bytes(bytes.try_into()?) + } +} + +impl fmt::Debug for Mldsa44Ed25519Keypair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Mldsa44Ed25519Keypair") + .field("public", &self.public) + .finish_non_exhaustive() + } +} + +#[cfg(feature = "ed25519")] +impl From for Mldsa44Ed25519Keypair { + fn from(private: Mldsa44Ed25519PrivateKey) -> Mldsa44Ed25519Keypair { + let public = Mldsa44Ed25519PublicKey::from(&private); + Mldsa44Ed25519Keypair { private, public } + } +} + diff --git a/ssh-key/src/public.rs b/ssh-key/src/public.rs index 9dd0b106..93b4455b 100644 --- a/ssh-key/src/public.rs +++ b/ssh-key/src/public.rs @@ -14,8 +14,12 @@ mod opaque; mod rsa; mod sk; mod ssh_format; +mod mldsa44_ed25519; -pub use self::{ed25519::Ed25519PublicKey, key_data::KeyData, sk::SkEd25519}; +pub use self::{ + ed25519::Ed25519PublicKey, key_data::KeyData, mldsa44_ed25519::Mldsa44Ed25519PublicKey, + sk::SkEd25519, +}; #[cfg(feature = "alloc")] pub use self::{ diff --git a/ssh-key/src/public/key_data.rs b/ssh-key/src/public/key_data.rs index 5924ca98..96ce18cb 100644 --- a/ssh-key/src/public/key_data.rs +++ b/ssh-key/src/public/key_data.rs @@ -10,7 +10,7 @@ use { crate::Certificate, alloc::boxed::Box, }; - +use crate::public::mldsa44_ed25519::Mldsa44Ed25519PublicKey; #[cfg(feature = "ecdsa")] use super::{EcdsaPublicKey, SkEcdsaSha2NistP256}; @@ -29,6 +29,9 @@ pub enum KeyData { /// Ed25519 public key data. Ed25519(Ed25519PublicKey), + /// ML-DSA-44 + Ed25519 public key data. + Mldsa44Ed25519(Mldsa44Ed25519PublicKey), + /// RSA public key data. #[cfg(feature = "alloc")] Rsa(RsaPublicKey), @@ -67,6 +70,7 @@ impl KeyData { #[cfg(feature = "ecdsa")] Self::Ecdsa(key) => key.algorithm(), Self::Ed25519(_) => Algorithm::Ed25519, + Self::Mldsa44Ed25519(_) => Algorithm::Mldsa44Ed25519, #[cfg(feature = "alloc")] Self::Rsa(_) => Algorithm::Rsa { hash: None }, #[cfg(feature = "ecdsa")] @@ -109,6 +113,16 @@ impl KeyData { } } + /// Get ML-DSA-44 + Ed25519 public key if this key is the correct type. + #[must_use] + pub fn mldsa44ed25519(&self) -> Option<&Mldsa44Ed25519PublicKey> { + match self { + Self::Mldsa44Ed25519(key) => Some(key), + #[allow(unreachable_patterns)] + _ => None, + } + } + /// Compute key fingerprint. /// /// Use [`Default::default()`] to use the default hash function (SHA-256). @@ -196,6 +210,12 @@ impl KeyData { matches!(self, Self::Ed25519(_)) } + /// Is this key an ML-DSA-44 + Ed25519 key? + #[must_use] + pub fn is_mldsa44ed25519(&self) -> bool { + matches!(self, Self::Mldsa44Ed25519(_)) + } + /// Is this key an RSA key? #[cfg(feature = "alloc")] #[must_use] @@ -246,6 +266,7 @@ impl KeyData { _ => Err(Error::AlgorithmUnknown), }, Algorithm::Ed25519 => Ed25519PublicKey::decode(reader).map(Self::Ed25519), + Algorithm::Mldsa44Ed25519 => Mldsa44Ed25519PublicKey::decode(reader).map(Self::Mldsa44Ed25519), #[cfg(feature = "alloc")] Algorithm::Rsa { .. } => RsaPublicKey::decode(reader).map(Self::Rsa), #[cfg(feature = "ecdsa")] @@ -278,6 +299,7 @@ impl KeyData { #[cfg(feature = "ecdsa")] Self::Ecdsa(key) => key.encoded_len(), Self::Ed25519(key) => key.encoded_len(), + Self::Mldsa44Ed25519(key) => key.encoded_len(), #[cfg(feature = "alloc")] Self::Rsa(key) => key.encoded_len(), #[cfg(feature = "ecdsa")] @@ -298,6 +320,7 @@ impl KeyData { #[cfg(feature = "ecdsa")] Self::Ecdsa(key) => key.encode(writer), Self::Ed25519(key) => key.encode(writer), + Self::Mldsa44Ed25519(key) => key.encode(writer), #[cfg(feature = "alloc")] Self::Rsa(key) => key.encode(writer), #[cfg(feature = "ecdsa")] @@ -373,6 +396,12 @@ impl From for KeyData { } } +impl From for KeyData { + fn from(public_key: Mldsa44Ed25519PublicKey) -> KeyData { + Self::Mldsa44Ed25519(public_key) + } +} + #[cfg(feature = "alloc")] impl From for KeyData { fn from(public_key: RsaPublicKey) -> KeyData { diff --git a/ssh-key/src/public/mldsa44_ed25519.rs b/ssh-key/src/public/mldsa44_ed25519.rs new file mode 100644 index 00000000..c292bec1 --- /dev/null +++ b/ssh-key/src/public/mldsa44_ed25519.rs @@ -0,0 +1,129 @@ +//! ML-DSA-44 + Ed25519 public keys. +//! + +use crate::{Error, Result}; +use core::fmt; +#[cfg(feature = "ed25519")] +use libcrux_ml_dsa::ml_dsa_44::generate_key_pair; +use encoding::{CheckedSum, Decode, Encode, Reader, Writer}; +use crate::private::Mldsa44Ed25519PrivateKey; + +/// ML-DSA-44 + Ed25519 public key. +/// +/// Encodings for Ed25519 public keys are described in [RFC8709 § 4]: +/// +/// > The "ssh-ed25519" key format has the following encoding: +/// > +/// > **string** "ssh-ed25519" +/// > +/// > **string** key +/// > +/// > Here, 'key' is the 32-octet public key described in RFC8032 +/// +/// [RFC8709 § 4]: https://datatracker.ietf.org/doc/html/rfc8709#section-4 +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub struct Mldsa44Ed25519PublicKey(pub [u8; Self::BYTE_SIZE]); + +impl Mldsa44Ed25519PublicKey { + pub const MLDSA_SIZE: usize = 1312; + pub const ED25519_SIZE: usize = 32; + + /// Size of a composite ML-DSA-44 + Ed25519 public key in bytes. + pub const BYTE_SIZE: usize = Self::MLDSA_SIZE + Self::ED25519_SIZE; +} + +impl AsRef<[u8; Self::BYTE_SIZE]> for Mldsa44Ed25519PublicKey { + fn as_ref(&self) -> &[u8; Self::BYTE_SIZE] { + &self.0 + } +} + +impl Decode for Mldsa44Ed25519PublicKey { + type Error = Error; + + fn decode(reader: &mut impl Reader) -> Result { + let mut bytes = [0u8; Self::BYTE_SIZE]; + reader.read_prefixed(|reader| reader.read(&mut bytes))?; + Ok(Self(bytes)) + } +} + +impl Encode for Mldsa44Ed25519PublicKey { + fn encoded_len(&self) -> encoding::Result { + [4, Self::BYTE_SIZE].checked_sum() + } + + fn encode(&self, writer: &mut impl Writer) -> encoding::Result<()> { + self.0.as_slice().encode(writer)?; + Ok(()) + } +} + +impl TryFrom<&[u8]> for Mldsa44Ed25519PublicKey { + type Error = Error; + + fn try_from(bytes: &[u8]) -> Result { + Ok(Self(bytes.try_into()?)) + } +} + +impl fmt::Display for Mldsa44Ed25519PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:X}") + } +} + +impl fmt::LowerHex for Mldsa44Ed25519PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in self.as_ref() { + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} + +impl fmt::UpperHex for Mldsa44Ed25519PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in self.as_ref() { + write!(f, "{byte:02X}")?; + } + Ok(()) + } +} + +#[cfg(feature = "ed25519")] +impl TryFrom for ed25519_dalek::VerifyingKey { + type Error = Error; + + fn try_from(key: Mldsa44Ed25519PublicKey) -> Result { + ed25519_dalek::VerifyingKey::try_from(&key) + } +} + +#[cfg(feature = "ed25519")] +impl TryFrom<&Mldsa44Ed25519PublicKey> for ed25519_dalek::VerifyingKey { + type Error = Error; + + fn try_from(key: &Mldsa44Ed25519PublicKey) -> Result { + ed25519_dalek::VerifyingKey::from_bytes(key.as_ref()).map_err(|_| Error::Crypto) + } +} + +#[cfg(feature = "ed25519")] +impl From for Mldsa44Ed25519PublicKey { + fn from(key: Mldsa44Ed25519PrivateKey) -> Mldsa44Ed25519PublicKey { + Mldsa44Ed25519PublicKey::from(&key) + } +} + +#[cfg(feature = "ed25519")] +impl From<&Mldsa44Ed25519PrivateKey> for Mldsa44Ed25519PublicKey { + fn from(key: &Mldsa44Ed25519PrivateKey) -> Mldsa44Ed25519PublicKey { + let mldsa44_key_pair = generate_key_pair(key.mldsa44_seed); + let ed25519_key_pair = ed25519_dalek::SigningKey::from_bytes(&key.ed25519_seed); + let mut public_key = [0u8; Mldsa44Ed25519PublicKey::BYTE_SIZE]; + public_key[..Mldsa44Ed25519PublicKey::MLDSA_SIZE].copy_from_slice(mldsa44_key_pair.verification_key); + public_key[Mldsa44Ed25519PublicKey::MLDSA_SIZE..].copy_from_slice(ed25519_key_pair.verifying_key().as_bytes()); + Mldsa44Ed25519PublicKey(public_key) + } +} diff --git a/ssh-key/tests/examples/id_mldsa44_ed25519 b/ssh-key/tests/examples/id_mldsa44_ed25519 new file mode 100644 index 00000000..6b4026cd --- /dev/null +++ b/ssh-key/tests/examples/id_mldsa44_ed25519 @@ -0,0 +1,58 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +MzAAAAEHVzZXJAZXhhbXBsZS5jb20B +-----END OPENSSH PRIVATE KEY----- diff --git a/ssh-key/tests/examples/id_mldsa44_ed25519.pub b/ssh-key/tests/examples/id_mldsa44_ed25519.pub new file mode 100644 index 00000000..83abd7d5 --- /dev/null +++ b/ssh-key/tests/examples/id_mldsa44_ed25519.pub @@ -0,0 +1 @@ +ssh-mldsa44-ed25519@openssh.com AAAAH3NzaC1tbGRzYTQ0LWVkMjU1MTlAb3BlbnNzaC5jb20AAAVAniHueEjKxQWUbV3FNx5sFS97kQzItTbZlyYb/Avbfcd9m8q2YwZk/+e7TDKXtM4jIRbYcEJ04tykbTBnX+a7Te2GjmTTjuOc/KJqa/k2XgtpypqgAt+qc95K2OyfWWBmjjs10samUN8gYuUbTJOuXyAoyAYtVFXk5RT5GAU46wO8dFwsikR+op+xO4vqWN14nuRIwq66/zbech6HIeEW2LHLAY/tFWETwvKpfstkEl6C0WpPp59yKEzDWykVkOqpk0qtg+eAfCEMnD8K2pJfC9QxAeaKxEX+VY+2SKLKtua8nOc1iSnOjkN1avTy3OVqvM95zYOBBNQZdC/Zh4Uxwo+4518OhYB5P486wEQY3OmP5zoapkuHJ7XtIZg4+JHBGq94Ut37MFHN4x1/rg/pzKvQJO4EgKqd5lXADJzYhN7jUdE+HwicoZ96kM7nT+RJ4h9C7X12ucTxzhC7L31YtNO02qTBQakY2iPcL8e9sm93Npra97PkdsSrTjUMsdGxNofzaeKDvGz3Jvge6+sqNE7pvyazAK2hvxMCBPUDfuoK6nLWRYkJVTZhan9bvsI8BrbpOTh0ZXQoXCVVUWE9RMHn/jnrcF1kVeWZbRnqPEWwE4rPAWWcRiw0uubx5V6+BonwLnDCppTaheQSYl9O6IJGNOZwtQH8zgMGbHPfOgFxvHJV9bbkbrv8NUppDC8uRg44ZcaV8dBvGRIYDjY+EVCXLwLAZDfP2bHx8GqUcViDhAElNNdXHSlnurP5RPcrO0tpEIq+bXSZZZynI4u85o2s4m37sko4PcIg8fXjqDgNFw2lIzUhQe6OK8RYFliNAl4FO9gI3kk9kF/1ptJ+MQFhKh7urby5iAirvmIwfhZ+CPjraBMF8DkB4EeKh4btjxLwC2wSPAo0WeuJBj7v9lRw1l/BzDzHDzoS+/liYzTMvsV7wIcbJucumbWuggk0znRPM6rNSQqxxCIiBi2+bp5Ng578h75DET/fkOk8e3v4JdZobHm1WNiDhleLBZO/zEu/g85xFncILUDfpDW0QawDcZ8hs8Zzau9Eks+NxPEpJls2iiAh8+EBgEuf3b5bCpSVqfIPa9lpnLcKpeZMCeJx21LlhqAjhner01HIwtcJ4TYhhqdwVB4pgHRD+dpv1cORxXqkzWxlF4UMeFwTF6n1sbtiiWn0GyTgdtZ2MU1L5n2GcyiCHCRQpAE2vC7V9Uhu3v6NrdlAFPboQ623S9POo2NidfeVbRVpFR2MPFtipBBMKYI3v0vBnZPt1KGvyPjsQmRqiD4QKHWX8+n9++KBLDdfaYd2N31TkyY6+MHzDFfqQMUlZbk0vGGGuA1JO6JivCtBZL0m6EXddF5hnSDihVMwQsJpha+lvbHA4hGt8yJkik7SzzwoO6neUe6hHwwDwdQuQ2LsAAiD8v71FnEEehXNI3PrpBuwfxKUtDe0TMu3kWtOUXpvkjUQCHihhLNlZfR51M8tfJ5avBaal6S3MQ2NNC04QU5FVNE8meSQKmfq+Oeom22BjLE991+SH8fTMBz+RW2fDiiW8SgUQJ3JTlhLSn3DJFwby2KfL8TXRxsIUd6GYnibU93S/WSROROmzYgNUg1gB7jchtA11dNEkuRskYPhn6e6vQrag4ypRSHpylLo8QSyYEjkgrPG1Ow/3StSiLoXe1JfJiTyewM0QKE2hsQzrNE7b/fCONGy/rm0C5diego9IBh1yVxlzHW9umUNuiPfgBp2qgNfkbBcieue5XC0DzmTTjc0BJG/JNx4L4QOpO37dGU3kNw/ user@example.com diff --git a/ssh-key/tests/private_key.rs b/ssh-key/tests/private_key.rs index 30d33570..ccc5b60a 100644 --- a/ssh-key/tests/private_key.rs +++ b/ssh-key/tests/private_key.rs @@ -50,6 +50,9 @@ const OPENSSH_ECDSA_P521_EXAMPLE: &str = include_str!("examples/id_ecdsa_p521"); /// Ed25519 OpenSSH-formatted private key const OPENSSH_ED25519_EXAMPLE: &str = include_str!("examples/id_ed25519"); +/// ML-DSA-44 + Ed25519 OpenSSH-formatted private key +const OPENSSH_MLDSA44_ED25519_EXAMPLE: &str = include_str!("examples/id_mldsa44_ed25519"); + /// Same key, converted by puttygen #[cfg(all(feature = "ppk", feature = "ed25519"))] const PPK_ED25519_EXAMPLE: &str = include_str!("examples/id_ed25519.ppk"); @@ -330,6 +333,11 @@ fn decode_ed25519_openssh() { validate_ed25519(PrivateKey::from_openssh(OPENSSH_ED25519_EXAMPLE).unwrap()); } +#[test] +fn decode_mldsa44_ed25519_openssh() { + validate_mldsa44_ed25519(PrivateKey::from_openssh(OPENSSH_MLDSA44_ED25519_EXAMPLE).unwrap()); +} + #[test] #[cfg(all(feature = "ppk", feature = "ed25519"))] fn decode_ed25519_ppk() { @@ -364,6 +372,26 @@ fn validate_ed25519(key: PrivateKey) { assert_eq!(key.comment().as_bytes(), b"user@example.com"); } +fn validate_mldsa44_ed25519(key: PrivateKey) { + assert_eq!(Algorithm::Mldsa44Ed25519, key.algorithm()); + assert_eq!(Cipher::None, key.cipher()); + assert_eq!(KdfAlg::None, key.kdf().algorithm()); + assert!(key.kdf().is_none()); + + let mldsa44_ed25519_keypair = key.key_data().mldsa44ed25519().unwrap(); + // assert_eq!( + // &hex!("b33eaef37ea2df7caa010defdea34e241f65f1b529a4f43ed14327f5c54aab62"), + // mldsa44_ed25519_keypair.public.as_ref(), + // ); + // assert_eq!( + // &hex!("b606c222d10c16dae16c70a4d45173472ec617e05c656920d26e56c08fb591ed"), + // mldsa44_ed25519_keypair.private.as_ref(), + // ); + + #[cfg(feature = "alloc")] + assert_eq!(key.comment().as_bytes(), b"user@example.com"); +} + /// Test alternative PEM line wrappings (64 columns). #[test] fn decode_ed25519_openssh_64cols() { @@ -643,6 +671,12 @@ fn encode_ed25519_openssh() { encoding_test(OPENSSH_ED25519_EXAMPLE); } +#[cfg(feature = "alloc")] +#[test] +fn encode_mldsa44_ed25519_openssh() { + encoding_test(OPENSSH_MLDSA44_ED25519_EXAMPLE); +} + #[cfg(feature = "alloc")] #[test] fn encode_rsa_3072_openssh() { diff --git a/ssh-key/tests/public_key.rs b/ssh-key/tests/public_key.rs index 3e777393..13d892cf 100644 --- a/ssh-key/tests/public_key.rs +++ b/ssh-key/tests/public_key.rs @@ -33,6 +33,9 @@ const OPENSSH_ECDSA_P521_EXAMPLE: &str = include_str!("examples/id_ecdsa_p521.pu /// Ed25519 OpenSSH-formatted public key const OPENSSH_ED25519_EXAMPLE: &str = include_str!("examples/id_ed25519.pub"); +/// ML-DSA-44 + Ed25519 OpenSSH-formatted public key +const OPENSSH_MLDSA44_ED25519_EXAMPLE: &str = include_str!("examples/id_mldsa44_ed25519.pub"); + /// RSA (3072-bit) OpenSSH-formatted public key #[cfg(feature = "alloc")] const OPENSSH_RSA_3072_EXAMPLE: &str = include_str!("examples/id_rsa_3072.pub"); @@ -214,6 +217,27 @@ fn decode_ed25519_openssh() { ); } +#[test] +fn decode_mldsa44_ed25519_openssh() { + let key = PublicKey::from_openssh(OPENSSH_MLDSA44_ED25519_EXAMPLE).unwrap(); + + assert_eq!(Algorithm::Mldsa44Ed25519, key.key_data().algorithm()); + + // TODO fix: + // assert_eq!( + // &hex!("b33eaef37ea2df7caa010defdea34e241f65f1b529a4f43ed14327f5c54aab62"), + // key.key_data().ed25519().unwrap().as_ref(), + // ); + + #[cfg(feature = "alloc")] + assert_eq!(b"user@example.com", key.comment().as_bytes()); + + assert_eq!( + "SHA256:41XYQTAqXq5f2wChH+sjXQ1YhsyzS2JmWrXzPaOmOts", + &key.fingerprint(Default::default()).to_string(), + ); +} + #[cfg(feature = "alloc")] #[test] fn decode_rsa_3072_openssh() { @@ -417,6 +441,13 @@ fn encode_ed25519_openssh() { assert_eq!(OPENSSH_ED25519_EXAMPLE.trim_end(), &key.to_string()); } +#[cfg(feature = "alloc")] +#[test] +fn encode_mldsa44_ed25519_openssh() { + let key = PublicKey::from_openssh(OPENSSH_MLDSA44_ED25519_EXAMPLE).unwrap(); + assert_eq!(OPENSSH_MLDSA44_ED25519_EXAMPLE.trim_end(), &key.to_string()); +} + #[cfg(feature = "alloc")] #[test] fn encode_rsa_3072_openssh() {