From e2497344b275d427fd06d21f5bd109bcfdf25852 Mon Sep 17 00:00:00 2001 From: Louis Hampton Date: Mon, 6 Jul 2026 16:22:02 +0100 Subject: [PATCH] Add support for X448/Ed448 and P521 --- Cargo.lock | 107 +++++++++++++++++- Cargo.toml | 3 + src/kx.rs | 42 ++++++- src/lib.rs | 3 +- src/sign.rs | 12 +- src/sign/ecdsa.rs | 1 + src/sign/eddsa.rs | 45 ++++++++ src/verify.rs | 9 +- src/verify/ecdsa.rs | 1 + src/verify/eddsa.rs | 30 +++++ validation/local_ping_pong_openssl/src/lib.rs | 2 - 11 files changed, 242 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2aa1cbe..6fe7efd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,9 +66,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.65" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "shlex", @@ -286,6 +286,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed448" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae112a25f86ae3598d4e8533ed1e65149cec6eb21918e7a6f4c06dddec370263" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed448-goldilocks" +version = "0.14.0-pre.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b805154de2e68f59874ec217ca36790dcffe500cd872c60fe509d28d0814a74d" +dependencies = [ + "ed448", + "elliptic-curve", + "hash2curve", + "rand_core", + "shake", + "signature", + "subtle", +] + [[package]] name = "elliptic-curve" version = "0.14.1" @@ -373,6 +398,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "hash2curve" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eaf40612d7d854743e7189228a6d528f0f6e8502cf6a0cb831d28a218b7f3f6" +dependencies = [ + "digest", + "elliptic-curve", +] + [[package]] name = "hkdf" version = "0.13.0" @@ -411,6 +446,16 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures", +] + [[package]] name = "libc" version = "0.2.186" @@ -465,6 +510,20 @@ dependencies = [ "sha2", ] +[[package]] +name = "p521" +version = "0.14.0-rc.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff42e4ace5424e3b6d7cb82514be89866b85af87015f80341e37dcc21a66ce6e" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2", +] + [[package]] name = "paste" version = "1.0.15" @@ -663,10 +722,12 @@ dependencies = [ "digest", "ecdsa", "ed25519-dalek", + "ed448-goldilocks", "getrandom 0.4.3", "hmac", "p256", "p384", + "p521", "paste", "pkcs8", "rsa", @@ -676,6 +737,7 @@ dependencies = [ "sha2", "signature", "x25519-dalek", + "x448", ] [[package]] @@ -759,6 +821,17 @@ dependencies = [ "digest", ] +[[package]] +name = "shake" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" +dependencies = [ + "digest", + "keccak", + "sponge-cursor", +] + [[package]] name = "shlex" version = "2.0.1" @@ -785,6 +858,12 @@ dependencies = [ "der", ] +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + [[package]] name = "subtle" version = "2.6.1" @@ -931,8 +1010,32 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x448" +version = "0.14.0-pre.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c166d06b9fa4328c890d46bda797269fb6aeca96393aeaad0e74b4b11550e06" +dependencies = [ + "ed448-goldilocks", + "zeroize", +] + [[package]] name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 8cd76fc..9a6591a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ getrandom = { version = "0.4", default-features = false, features = ["sys_rng"] hmac = { version = "0.13", default-features = false } p256 = { version = "0.14", default-features = false, features = ["pem", "ecdsa", "ecdh"] } p384 = { version = "0.14.0-rc.15", default-features = false, features = ["pem", "ecdsa", "ecdh"] } +p521 = { version = "0.14.0-rc.15", default-features = false, features = ["pem", "ecdsa", "ecdh"] } paste = { version = "1", default-features = false } pkcs8 = { version = "0.11", default-features = false } pki-types = { package = "rustls-pki-types", version = "1", default-features = false } @@ -39,6 +40,8 @@ sec1 = { version = "0.8", default-features = false } sha2 = { version = "0.11", default-features = false } signature = { version = "3", default-features = false } x25519-dalek = { version = "3.0.0-rc.1", default-features = false } +ed448-goldilocks = { version = "0.14.0-pre.15", default-features = false, features = ["signing", "pkcs8"] } +x448 = { version = "0.14.0-pre.12", default-features = false } [features] default = ["std", "tls12", "zeroize"] diff --git a/src/kx.rs b/src/kx.rs index 3f8a4dd..5352731 100644 --- a/src/kx.rs +++ b/src/kx.rs @@ -49,6 +49,44 @@ impl crypto::ActiveKeyExchange for X25519KeyExchange { } } +#[derive(Debug)] +pub struct X448; + +impl crypto::SupportedKxGroup for X448 { + fn name(&self) -> rustls::NamedGroup { + rustls::NamedGroup::X448 + } + + fn start(&self) -> Result, rustls::Error> { + let mut rng = UnwrapErr(getrandom::SysRng); + let priv_key = x448::EphemeralSecret::try_generate_from_rng(&mut rng) + .map_err(|_| rustls::Error::from(rustls::PeerMisbehaved::InvalidKeyShare))?; + let pub_key = (&priv_key).into(); + Ok(Box::new(X448KeyExchange { priv_key, pub_key })) + } +} + +pub struct X448KeyExchange { + priv_key: x448::EphemeralSecret, + pub_key: x448::PublicKey, +} + +impl crypto::ActiveKeyExchange for X448KeyExchange { + fn complete(self: Box, peer: &[u8]) -> Result { + let peer = x448::PublicKey::from_bytes(peer) + .ok_or_else(|| rustls::Error::from(rustls::PeerMisbehaved::InvalidKeyShare))?; + Ok(self.priv_key.diffie_hellman(&peer).as_bytes()[..].into()) + } + + fn pub_key(&self) -> &[u8] { + self.pub_key.as_bytes() + } + + fn group(&self) -> rustls::NamedGroup { + X448.name() + } +} + macro_rules! impl_kx { ($name:ident, $kx_name:ty, $secret:ty, $public_key:ty) => { paste! { @@ -108,5 +146,7 @@ macro_rules! impl_kx { impl_kx! {SecP256R1, rustls::NamedGroup::secp256r1, p256::ecdh::EphemeralSecret, p256::PublicKey} impl_kx! {SecP384R1, rustls::NamedGroup::secp384r1, p384::ecdh::EphemeralSecret, p384::PublicKey} +impl_kx! {SecP521R1, rustls::NamedGroup::secp521r1, p521::ecdh::EphemeralSecret, p521::PublicKey} -pub const ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[&X25519, &SecP256R1, &SecP384R1]; +pub const ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = + &[&X25519, &X448, &SecP256R1, &SecP384R1, &SecP521R1]; diff --git a/src/lib.rs b/src/lib.rs index 6332bca..f7e1c78 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,11 +80,12 @@ impl KeyProvider for Provider { } #[cfg(feature = "tls12")] -const TLS12_ECDSA_SCHEMES: [SignatureScheme; 4] = [ +const TLS12_ECDSA_SCHEMES: [SignatureScheme; 5] = [ SignatureScheme::ECDSA_NISTP256_SHA256, SignatureScheme::ECDSA_NISTP384_SHA384, SignatureScheme::ECDSA_NISTP521_SHA512, SignatureScheme::ED25519, + SignatureScheme::ED448, ]; #[cfg(feature = "tls12")] diff --git a/src/sign.rs b/src/sign.rs index cf30cc5..39cf145 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -2,8 +2,8 @@ use alloc::{sync::Arc, vec::Vec}; use core::marker::PhantomData; -use self::ecdsa::{EcdsaSigningKeyP256, EcdsaSigningKeyP384}; -use self::eddsa::Ed25519SigningKey; +use self::ecdsa::{EcdsaSigningKeyP256, EcdsaSigningKeyP384, EcdsaSigningKeyP521}; +use self::eddsa::{Ed25519SigningKey, Ed448SigningKey}; use self::rsa::RsaSigningKey; use getrandom::rand_core::UnwrapErr; @@ -89,7 +89,8 @@ pub fn any_supported_type(der: &PrivateKeyDer<'_>) -> Result pub fn any_ecdsa_type(der: &PrivateKeyDer<'_>) -> Result, rustls::Error> { let p256 = |_| EcdsaSigningKeyP256::try_from(der).map(|x| Arc::new(x) as _); let p384 = |_| EcdsaSigningKeyP384::try_from(der).map(|x| Arc::new(x) as _); - p256(()).or_else(p384) + let p521 = |_| EcdsaSigningKeyP521::try_from(der).map(|x| Arc::new(x) as _); + p256(()).or_else(p384).or_else(p521) } /// Extract any supported EDDSA key from the given DER input. @@ -98,8 +99,9 @@ pub fn any_ecdsa_type(der: &PrivateKeyDer<'_>) -> Result, ru /// /// Returns an error if the key couldn't be decoded. pub fn any_eddsa_type(der: &PrivateKeyDer<'_>) -> Result, rustls::Error> { - // TODO: Add support for Ed448 - Ed25519SigningKey::try_from(der).map(|x| Arc::new(x) as _) + let ed25519 = |_| Ed25519SigningKey::try_from(der).map(|x| Arc::new(x) as _); + let ed448 = |_| Ed448SigningKey::try_from(der).map(|x| Arc::new(x) as _); + ed25519(()).or_else(ed448) } pub mod ecdsa; diff --git a/src/sign/ecdsa.rs b/src/sign/ecdsa.rs index 347bc25..6e9dfc2 100644 --- a/src/sign/ecdsa.rs +++ b/src/sign/ecdsa.rs @@ -69,3 +69,4 @@ macro_rules! impl_ecdsa { impl_ecdsa! {P256, SignatureScheme::ECDSA_NISTP256_SHA256, p256::ecdsa::SigningKey, p256::ecdsa::DerSignature} impl_ecdsa! {P384, SignatureScheme::ECDSA_NISTP384_SHA384, p384::ecdsa::SigningKey, p384::ecdsa::DerSignature} +impl_ecdsa! {P521, SignatureScheme::ECDSA_NISTP521_SHA512, p521::ecdsa::SigningKey, p521::ecdsa::DerSignature} diff --git a/src/sign/eddsa.rs b/src/sign/eddsa.rs index 15f4fe8..e8cb34f 100644 --- a/src/sign/eddsa.rs +++ b/src/sign/eddsa.rs @@ -51,3 +51,48 @@ impl SigningKey for Ed25519SigningKey { SignatureAlgorithm::ED25519 } } + +#[derive(Debug)] +pub struct Ed448SigningKey { + key: Arc, + scheme: SignatureScheme, +} + +impl TryFrom<&PrivateKeyDer<'_>> for Ed448SigningKey { + type Error = rustls::Error; + + fn try_from(value: &PrivateKeyDer<'_>) -> Result { + let pkey = match value { + PrivateKeyDer::Pkcs8(der) => { + ed448_goldilocks::SigningKey::from_pkcs8_der(der.secret_pkcs8_der()) + .map_err(|e| format!("failed to decrypt private key: {e}")) + } + PrivateKeyDer::Pkcs1(_) => Err("ED448 does not support PKCS#1 key".to_string()), + PrivateKeyDer::Sec1(_) => Err("ED448 does not support SEC1 key".to_string()), + _ => Err("not supported".into()), + }; + pkey.map(|kp| Self { + key: Arc::new(kp), + scheme: SignatureScheme::ED448, + }) + .map_err(rustls::Error::General) + } +} + +impl SigningKey for Ed448SigningKey { + fn choose_scheme(&self, offered: &[SignatureScheme]) -> Option> { + if offered.contains(&self.scheme) { + Some(Box::new(super::GenericSigner { + _marker: PhantomData, + key: self.key.clone(), + scheme: self.scheme, + })) + } else { + None + } + } + + fn algorithm(&self) -> SignatureAlgorithm { + SignatureAlgorithm::ED448 + } +} diff --git a/src/verify.rs b/src/verify.rs index 2043b64..713ace7 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -1,8 +1,10 @@ use rustls::crypto::WebPkiSupportedAlgorithms; use rustls::SignatureScheme; -use self::ecdsa::{ECDSA_P256_SHA256, ECDSA_P256_SHA384, ECDSA_P384_SHA256, ECDSA_P384_SHA384}; -use self::eddsa::ED25519; +use self::ecdsa::{ + ECDSA_P256_SHA256, ECDSA_P256_SHA384, ECDSA_P384_SHA256, ECDSA_P384_SHA384, ECDSA_P521_SHA512, +}; +use self::eddsa::{ED25519, ED448}; use self::rsa::{ RSA_PKCS1_SHA256, RSA_PKCS1_SHA384, RSA_PKCS1_SHA512, RSA_PSS_SHA256, RSA_PSS_SHA384, RSA_PSS_SHA512, @@ -15,6 +17,7 @@ pub static ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms { ECDSA_P384_SHA256, ECDSA_P384_SHA384, ED25519, + ED448, RSA_PKCS1_SHA256, RSA_PKCS1_SHA384, RSA_PKCS1_SHA512, @@ -31,7 +34,9 @@ pub static ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms { SignatureScheme::ECDSA_NISTP256_SHA256, &[ECDSA_P256_SHA256, ECDSA_P384_SHA256], ), + (SignatureScheme::ECDSA_NISTP521_SHA512, &[ECDSA_P521_SHA512]), (SignatureScheme::ED25519, &[ED25519]), + (SignatureScheme::ED448, &[ED448]), (SignatureScheme::RSA_PKCS1_SHA256, &[RSA_PKCS1_SHA256]), (SignatureScheme::RSA_PKCS1_SHA384, &[RSA_PKCS1_SHA384]), (SignatureScheme::RSA_PKCS1_SHA512, &[RSA_PKCS1_SHA512]), diff --git a/src/verify/ecdsa.rs b/src/verify/ecdsa.rs index 743a6c5..11de758 100644 --- a/src/verify/ecdsa.rs +++ b/src/verify/ecdsa.rs @@ -51,3 +51,4 @@ impl_generic_ecdsa_verifer! {ECDSA_P256_SHA256, alg_id::ECDSA_P256, alg_id::ECDS impl_generic_ecdsa_verifer! {ECDSA_P256_SHA384, alg_id::ECDSA_P256, alg_id::ECDSA_SHA384, p256::ecdsa::VerifyingKey, p256::ecdsa::DerSignature, sha2::Sha384} impl_generic_ecdsa_verifer! {ECDSA_P384_SHA256, alg_id::ECDSA_P384, alg_id::ECDSA_SHA256, p384::ecdsa::VerifyingKey, p384::ecdsa::DerSignature, sha2::Sha256} impl_generic_ecdsa_verifer! {ECDSA_P384_SHA384, alg_id::ECDSA_P384, alg_id::ECDSA_SHA384, p384::ecdsa::VerifyingKey, p384::ecdsa::DerSignature, sha2::Sha384} +impl_generic_ecdsa_verifer! {ECDSA_P521_SHA512, alg_id::ECDSA_P521, alg_id::ECDSA_SHA512, p521::ecdsa::VerifyingKey, p521::ecdsa::DerSignature, sha2::Sha512} diff --git a/src/verify/eddsa.rs b/src/verify/eddsa.rs index 61009e1..3ac4c12 100644 --- a/src/verify/eddsa.rs +++ b/src/verify/eddsa.rs @@ -30,3 +30,33 @@ impl SignatureVerificationAlgorithm for Ed25519Verify { } pub const ED25519: &dyn SignatureVerificationAlgorithm = &Ed25519Verify; + +#[derive(Debug)] +struct Ed448Verify; + +impl SignatureVerificationAlgorithm for Ed448Verify { + fn public_key_alg_id(&self) -> AlgorithmIdentifier { + alg_id::ED448 + } + + fn signature_alg_id(&self) -> AlgorithmIdentifier { + alg_id::ED448 + } + + fn verify_signature( + &self, + public_key: &[u8], + message: &[u8], + signature: &[u8], + ) -> Result<(), InvalidSignature> { + let public_key = public_key.try_into().map_err(|_| InvalidSignature)?; + let signature = + ed448_goldilocks::Signature::from_slice(signature).map_err(|_| InvalidSignature)?; + ed448_goldilocks::VerifyingKey::from_bytes(public_key) + .map_err(|_| InvalidSignature)? + .verify(message, &signature) + .map_err(|_| InvalidSignature) + } +} + +pub const ED448: &dyn SignatureVerificationAlgorithm = &Ed448Verify; diff --git a/validation/local_ping_pong_openssl/src/lib.rs b/validation/local_ping_pong_openssl/src/lib.rs index 14da95c..0f9689c 100644 --- a/validation/local_ping_pong_openssl/src/lib.rs +++ b/validation/local_ping_pong_openssl/src/lib.rs @@ -118,7 +118,6 @@ mod test { vs_openssl_as_client(group_list, OpenSslCipherSuites::default()); } #[test] - #[should_panic] // no support fn vs_openssl_as_client_group_p521() { let mut group_list = OpenSslGroupsList::all_false(); group_list.P521 = true; @@ -131,7 +130,6 @@ mod test { vs_openssl_as_client(group_list, OpenSslCipherSuites::default()); } #[test] - #[should_panic] // no support fn vs_openssl_as_client_group_x448() { let mut group_list = OpenSslGroupsList::all_false(); group_list.X448 = true;