diff --git a/russh-keys/Cargo.toml b/russh-keys/Cargo.toml index ae62e614..95a9cfe6 100644 --- a/russh-keys/Cargo.toml +++ b/russh-keys/Cargo.toml @@ -47,6 +47,7 @@ md5 = "0.7" num-bigint = "0.4" num-integer = "0.1" openssl = { version = "0.10", optional = true } +p256 = "0.13" pbkdf2 = "0.11" rand = "0.7" rand_core = { version = "0.6.4", features = ["std"] } diff --git a/russh-keys/src/agent/client.rs b/russh-keys/src/agent/client.rs index 23ac90e3..e25f17fa 100644 --- a/russh-keys/src/agent/client.rs +++ b/russh-keys/src/agent/client.rs @@ -9,7 +9,7 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use super::{msg, Constraint}; use crate::encoding::{Encoding, Reader}; use crate::key::{PublicKey, SignatureHash}; -use crate::{key, Error}; +use crate::{key, Error, PublicKeyBase64}; /// SSH agent client. pub struct AgentClient { @@ -275,6 +275,14 @@ impl AgentClient { b"ssh-ed25519" => keys.push(PublicKey::Ed25519( ed25519_dalek::VerifyingKey::try_from(r.read_string()?)?, )), + b"ecdsa-sha2-nistp256" => { + let curve = r.read_string()?; + if curve != b"nistp256" { + return Err(Error::P256KeyError(p256::elliptic_curve::Error)); + } + let key = r.read_string()?; + keys.push(PublicKey::P256(p256::PublicKey::from_sec1_bytes(key)?)); + } t => { info!("Unsupported key type: {:?}", std::str::from_utf8(t)) } @@ -534,6 +542,9 @@ fn key_blob(public: &key::PublicKey, buf: &mut CryptoVec) -> Result<(), Error> { #[allow(clippy::indexing_slicing)] // length is known BigEndian::write_u32(&mut buf[5..], (len1 - len0) as u32); } + PublicKey::P256(_) => { + buf.extend_ssh_string(&public.public_key_bytes()); + } } Ok(()) } diff --git a/russh-keys/src/key.rs b/russh-keys/src/key.rs index 1ed51294..97db830a 100644 --- a/russh-keys/src/key.rs +++ b/russh-keys/src/key.rs @@ -17,6 +17,7 @@ use std::convert::TryFrom; use ed25519_dalek::{Signer, Verifier}; #[cfg(feature = "openssl")] use openssl::pkey::{Private, Public}; +use p256::elliptic_curve::generic_array::typenum::Unsigned; use rand_core::OsRng; use russh_cryptovec::CryptoVec; use serde::{Deserialize, Serialize}; @@ -35,6 +36,8 @@ impl AsRef for Name { } } +/// The name of the ecdsa-sha2-nistp256 algorithm for SSH. +pub const ECDSA_SHA2_NISTP256: Name = Name("ecdsa-sha2-nistp256"); /// The name of the Ed25519 algorithm for SSH. pub const ED25519: Name = Name("ssh-ed25519"); /// The name of the ssh-sha2-512 algorithm for SSH. @@ -50,6 +53,7 @@ impl Name { /// Base name of the private key file for a key name. pub fn identity_file(&self) -> &'static str { match *self { + ECDSA_SHA2_NISTP256 => "id_ecdsa", ED25519 => "id_ed25519", RSA_SHA2_512 => "id_rsa", RSA_SHA2_256 => "id_rsa", @@ -117,6 +121,8 @@ pub enum PublicKey { key: OpenSSLPKey, hash: SignatureHash, }, + #[doc(hidden)] + P256(p256::PublicKey), } impl PartialEq for PublicKey { @@ -125,7 +131,7 @@ impl PartialEq for PublicKey { #[cfg(feature = "openssl")] (Self::RSA { key: a, .. }, Self::RSA { key: b, .. }) => a == b, (Self::Ed25519(a), Self::Ed25519(b)) => a == b, - #[cfg(feature = "openssl")] + (Self::P256(a), Self::P256(b)) => a == b, _ => false, } } @@ -205,6 +211,18 @@ impl PublicKey { unreachable!() } } + b"ecdsa-sha2-nistp256" => { + let mut p = pubkey.reader(0); + let key_algo = p.read_string()?; + let curve = p.read_string()?; + if key_algo != b"ecdsa-sha2-nistp256" || curve != b"nistp256" { + return Err(Error::CouldNotReadKey); + } + let sec1_bytes = p.read_string()?; + let key = p256::PublicKey::from_sec1_bytes(sec1_bytes) + .map_err(|_| Error::CouldNotReadKey)?; + Ok(PublicKey::P256(key)) + } _ => Err(Error::CouldNotReadKey), } } @@ -215,6 +233,7 @@ impl PublicKey { PublicKey::Ed25519(_) => ED25519.0, #[cfg(feature = "openssl")] PublicKey::RSA { ref hash, .. } => hash.name().0, + PublicKey::P256(_) => ECDSA_SHA2_NISTP256.0, } } @@ -239,6 +258,30 @@ impl PublicKey { }; verify().unwrap_or(false) } + PublicKey::P256(ref public) => { + const FIELD_LEN: usize = + ::FieldBytesSize::USIZE; + let mut reader = sig.reader(0); + let mut read_field = || -> Option { + let f = reader.read_mpint().ok()?; + let f = f.strip_prefix(&[0]).unwrap_or(f); + let mut result = [0; FIELD_LEN]; + if f.len() > FIELD_LEN { + return None; + } + #[allow(clippy::indexing_slicing)] // length is known + result[FIELD_LEN - f.len()..].copy_from_slice(f); + Some(result.into()) + }; + let Some(r) = read_field() else { return false }; + let Some(s) = read_field() else { return false }; + let Ok(signature) = p256::ecdsa::Signature::from_scalars(r, s) else { + return false; + }; + p256::ecdsa::VerifyingKey::from(public) + .verify(buffer, &signature) + .is_ok() + } } } @@ -509,5 +552,13 @@ pub fn parse_public_key( }); } } + if t == b"ecdsa-sha2-nistp256" { + if pos.read_string()? != b"nistp256" { + return Err(Error::CouldNotReadKey); + } + let sec1_bytes = pos.read_string()?; + let key = p256::PublicKey::from_sec1_bytes(sec1_bytes)?; + return Ok(PublicKey::P256(key)); + } Err(Error::CouldNotReadKey) } diff --git a/russh-keys/src/lib.rs b/russh-keys/src/lib.rs index ebe12a42..f23263a1 100644 --- a/russh-keys/src/lib.rs +++ b/russh-keys/src/lib.rs @@ -101,6 +101,9 @@ pub enum Error { /// The type of the key is unsupported #[error("Invalid Ed25519 key data")] Ed25519KeyError(#[from] ed25519_dalek::SignatureError), + /// The type of the key is unsupported + #[error("Invalid NIST-P256 key data")] + P256KeyError(#[from] p256::elliptic_curve::Error), /// The key is encrypted (should supply a password?) #[error("The key is encrypted")] KeyIsEncrypted, @@ -162,7 +165,7 @@ impl From for Error { const KEYTYPE_ED25519: &[u8] = b"ssh-ed25519"; const KEYTYPE_RSA: &[u8] = b"ssh-rsa"; -/// Load a public key from a file. Ed25519 and RSA keys are supported. +/// Load a public key from a file. Ed25519, EC-DSA and RSA keys are supported. /// /// ``` /// russh_keys::load_public_key("../files/id_ed25519.pub").unwrap(); @@ -233,6 +236,12 @@ impl PublicKeyBase64 for key::PublicKey { #[allow(clippy::unwrap_used)] // TODO check s.extend_ssh_mpint(&key.0.rsa().unwrap().n().to_vec()); } + key::PublicKey::P256(ref publickey) => { + use encoding::Encoding; + s.extend_ssh_string(b"ecdsa-sha2-nistp256"); + s.extend_ssh_string(b"nistp256"); + s.extend_ssh_string(&publickey.to_sec1_bytes()); + } } s } @@ -590,6 +599,14 @@ QR+u0AypRPmzHnOPAAAAEXJvb3RAMTQwOTExNTQ5NDBkAQ== assert!(check_known_hosts_path(host, port, &hostkey, &path).is_err()); } + #[test] + fn test_parse_p256_public_key() { + env_logger::try_init().unwrap_or(()); + let key = "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMxBTpMIGvo7CnordO7wP0QQRqpBwUjOLl4eMhfucfE1sjTYyK5wmTl1UqoSDS1PtRVTBdl+0+9pquFb46U7fwg="; + + parse_public_key_base64(key).unwrap(); + } + #[test] #[cfg(feature = "openssl")] fn test_srhb() { diff --git a/russh-keys/src/signature.rs b/russh-keys/src/signature.rs index 712139c2..eab19345 100644 --- a/russh-keys/src/signature.rs +++ b/russh-keys/src/signature.rs @@ -16,6 +16,8 @@ pub struct SignatureBytes(pub [u8; 64]); pub enum Signature { /// An Ed25519 signature Ed25519(SignatureBytes), + /// An EC-DSA NIST P-256 signature + P256(Vec), /// An RSA signature RSA { hash: SignatureHash, bytes: Vec }, } @@ -34,6 +36,15 @@ impl Signature { bytes_.extend_ssh_string(t); bytes_.extend_ssh_string(&bytes.0[..]); } + Signature::P256(ref bytes) => { + let t = b"ecdsa-sha2-nistp256"; + #[allow(clippy::unwrap_used)] // Vec<>.write_all can't fail + bytes_ + .write_u32::((t.len() + bytes.len() + 8) as u32) + .unwrap(); + bytes_.extend_ssh_string(t); + bytes_.extend_ssh_string(bytes); + } Signature::RSA { ref hash, ref bytes, @@ -48,7 +59,7 @@ impl Signature { .write_u32::((t.len() + bytes.len() + 8) as u32) .unwrap(); bytes_.extend_ssh_string(t); - bytes_.extend_ssh_string(&bytes[..]); + bytes_.extend_ssh_string(bytes); } } data_encoding::BASE64_NOPAD.encode(&bytes_[..]) @@ -80,6 +91,7 @@ impl Signature { hash: SignatureHash::SHA1, bytes: bytes.to_vec(), }), + b"ecdsa-sha2-nistp256" => Ok(Signature::P256(bytes.to_vec())), _ => Err(Error::UnknownSignatureType { sig_type: std::str::from_utf8(typ).unwrap_or("").to_string(), }), @@ -92,6 +104,7 @@ impl AsRef<[u8]> for Signature { match *self { Signature::Ed25519(ref signature) => &signature.0, Signature::RSA { ref bytes, .. } => &bytes[..], + Signature::P256(ref signature) => signature, } } } diff --git a/russh/src/key.rs b/russh/src/key.rs index 17a28f68..5930231b 100644 --- a/russh/src/key.rs +++ b/russh/src/key.rs @@ -15,6 +15,7 @@ use russh_cryptovec::CryptoVec; use russh_keys::encoding::*; use russh_keys::key::*; +use russh_keys::PublicKeyBase64; #[doc(hidden)] pub trait PubKey { @@ -29,6 +30,9 @@ impl PubKey for PublicKey { buffer.extend_ssh_string(ED25519.0.as_bytes()); buffer.extend_ssh_string(public.as_bytes()); } + PublicKey::P256(_) => { + buffer.extend_ssh_string(&self.public_key_bytes()); + } #[cfg(feature = "openssl")] PublicKey::RSA { ref key, .. } => { #[allow(clippy::unwrap_used)] // type known diff --git a/russh/src/negotiation.rs b/russh/src/negotiation.rs index 87004cc5..5b0cf0af 100644 --- a/russh/src/negotiation.rs +++ b/russh/src/negotiation.rs @@ -76,19 +76,16 @@ const HMAC_ORDER: &[mac::Name] = &[ ]; impl Preferred { - #[cfg(feature = "openssl")] pub const DEFAULT: Preferred = Preferred { kex: SAFE_KEX_ORDER, - key: &[key::ED25519, key::RSA_SHA2_256, key::RSA_SHA2_512], - cipher: CIPHER_ORDER, - mac: HMAC_ORDER, - compression: &["none", "zlib", "zlib@openssh.com"], - }; - - #[cfg(not(feature = "openssl"))] - pub const DEFAULT: Preferred = Preferred { - kex: SAFE_KEX_ORDER, - key: &[key::ED25519], + key: &[ + key::ED25519, + key::ECDSA_SHA2_NISTP256, + #[cfg(feature = "openssl")] + key::RSA_SHA2_256, + #[cfg(feature = "openssl")] + key::RSA_SHA2_512, + ], cipher: CIPHER_ORDER, mac: HMAC_ORDER, compression: &["none", "zlib", "zlib@openssh.com"], @@ -96,7 +93,7 @@ impl Preferred { pub const COMPRESSED: Preferred = Preferred { kex: SAFE_KEX_ORDER, - key: &[key::ED25519, key::RSA_SHA2_256, key::RSA_SHA2_512], + key: Preferred::DEFAULT.key, cipher: CIPHER_ORDER, mac: HMAC_ORDER, compression: &["zlib", "zlib@openssh.com", "none"], @@ -121,15 +118,15 @@ impl Named for () { } } -#[cfg(not(feature = "openssl"))] -use russh_keys::key::ED25519; #[cfg(feature = "openssl")] -use russh_keys::key::{ED25519, SSH_RSA}; +use russh_keys::key::SSH_RSA; +use russh_keys::key::{ECDSA_SHA2_NISTP256, ED25519}; impl Named for PublicKey { fn name(&self) -> &'static str { match self { PublicKey::Ed25519(_) => ED25519.0, + PublicKey::P256(_) => ECDSA_SHA2_NISTP256.0, #[cfg(feature = "openssl")] PublicKey::RSA { .. } => SSH_RSA.0, }