From 1a326ad416f3a376d44372ae887ceea3ffffb9cf Mon Sep 17 00:00:00 2001 From: George Hopkins Date: Sun, 5 Nov 2023 15:17:53 +0100 Subject: [PATCH 1/2] Support for NIST P-256 public keys --- russh-keys/Cargo.toml | 1 + russh-keys/src/agent/client.rs | 13 ++++++++- russh-keys/src/key.rs | 53 +++++++++++++++++++++++++++++++++- russh-keys/src/lib.rs | 19 +++++++++++- russh-keys/src/signature.rs | 15 +++++++++- russh/src/key.rs | 4 +++ russh/src/negotiation.rs | 27 ++++++++--------- 7 files changed, 113 insertions(+), 19 deletions(-) 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, } From 05e7f7989727d05c95cfed1291be06bb6d444fdd Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 5 Nov 2023 21:10:22 +0100 Subject: [PATCH 2/2] ecdsa-sha2-nistp256 support in the client WIP --- README.md | 3 +- russh-keys/Cargo.toml | 1 + russh-keys/src/agent/client.rs | 3 + russh-keys/src/deps.rs | 3 + russh-keys/src/format/openssh.rs | 21 +++++- russh-keys/src/format/pkcs8.rs | 2 + russh-keys/src/key.rs | 56 +++++++++++++++- russh-keys/src/lib.rs | 8 +++ russh-keys/src/signature.rs | 20 +++--- russh/examples/client_exec_interactive.rs | 1 - russh/examples/client_exec_simple.rs | 2 +- russh/src/key.rs | 78 +++++++++++++---------- russh/src/negotiation.rs | 1 + 13 files changed, 147 insertions(+), 52 deletions(-) create mode 100644 russh-keys/src/deps.rs diff --git a/README.md b/README.md index 8505878a..0fa1f054 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,12 @@ This is a fork of [Thrussh](https://nest.pijul.com/pijul/thrussh) by Pierre-Éti * `hmac-sha1-etm@openssh.com` ✨ * `hmac-sha2-256-etm@openssh.com` ✨ * `hmac-sha2-512-etm@openssh.com` ✨ -* Host keys: +* Key types: * `ssh-ed25519` * `rsa-sha2-256` * `rsa-sha2-512` * `ssh-rsa` ✨ + * `ecdsa-sha2-nistp256` ✨ * Dependency updates * OpenSSH keepalive request handling ✨ * OpenSSH agent forwarding channels ✨ diff --git a/russh-keys/Cargo.toml b/russh-keys/Cargo.toml index 95a9cfe6..b827e61a 100644 --- a/russh-keys/Cargo.toml +++ b/russh-keys/Cargo.toml @@ -38,6 +38,7 @@ block-padding = { version = "0.3", features = ["std"] } byteorder = "1.4" data-encoding = "2.3" dirs = "5.0" +ecdsa = { version = "0.16", features = ["serde"] } ed25519-dalek = { version= "2.0", features = ["rand_core"] } futures = "0.3" hmac = "0.12" diff --git a/russh-keys/src/agent/client.rs b/russh-keys/src/agent/client.rs index e25f17fa..f57e4c05 100644 --- a/russh-keys/src/agent/client.rs +++ b/russh-keys/src/agent/client.rs @@ -119,6 +119,9 @@ impl AgentClient { self.buf.extend(pair.verifying_key().as_bytes()); self.buf.extend_ssh_string(b""); } + key::KeyPair::EcdsaSha2NistP256(ref key) => { + todo!("TODO"); + } #[cfg(feature = "openssl")] #[allow(clippy::unwrap_used)] // key is known to be private key::KeyPair::RSA { ref key, .. } => { diff --git a/russh-keys/src/deps.rs b/russh-keys/src/deps.rs new file mode 100644 index 00000000..bb228f54 --- /dev/null +++ b/russh-keys/src/deps.rs @@ -0,0 +1,3 @@ +pub use ed25519_dalek; +pub use ecdsa; +pub use p256; diff --git a/russh-keys/src/format/openssh.rs b/russh-keys/src/format/openssh.rs index 44821fb8..3324c9db 100644 --- a/russh-keys/src/format/openssh.rs +++ b/russh-keys/src/format/openssh.rs @@ -8,7 +8,7 @@ use ctr::Ctr64BE; use openssl::bn::BigNum; use crate::encoding::Reader; -use crate::{key, Error, KEYTYPE_ED25519, KEYTYPE_RSA}; +use crate::{key, Error, KEYTYPE_ED25519, KEYTYPE_P256, KEYTYPE_RSA}; /// Decode a secret key given in the OpenSSH format, deciphering it if /// needed using the supplied password. @@ -79,6 +79,25 @@ pub fn decode_openssh(secret: &[u8], password: Option<&str>) -> Result println!("Ed25519"), + key::KeyPair::EcdsaSha2NistP256(_) => println!("P256"), #[cfg(feature = "openssl")] key::KeyPair::RSA { .. } => println!("RSA"), } @@ -316,6 +317,7 @@ pub fn encode_pkcs8(key: &key::KeyPair) -> Vec { yasna::construct_der(|writer| { writer.write_sequence(|writer| match *key { key::KeyPair::Ed25519(ref pair) => write_key_v1(writer, pair), + key::KeyPair::EcdsaSha2NistP256(_) => todo!("TODO"), #[cfg(feature = "openssl")] key::KeyPair::RSA { ref key, .. } => write_key_v0(writer, key), }) diff --git a/russh-keys/src/key.rs b/russh-keys/src/key.rs index 97db830a..572a8a31 100644 --- a/russh-keys/src/key.rs +++ b/russh-keys/src/key.rs @@ -14,6 +14,8 @@ // use std::convert::TryFrom; +use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; +use ecdsa::elliptic_curve::ff::PrimeField; use ed25519_dalek::{Signer, Verifier}; #[cfg(feature = "openssl")] use openssl::pkey::{Private, Public}; @@ -22,7 +24,7 @@ use rand_core::OsRng; use russh_cryptovec::CryptoVec; use serde::{Deserialize, Serialize}; -use crate::encoding::{Encoding, Reader}; +use crate::encoding::{mpint_len, Encoding, Reader}; pub use crate::signature::*; use crate::Error; @@ -215,7 +217,7 @@ impl PublicKey { 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" { + if key_algo != ECDSA_SHA2_NISTP256.0.as_bytes() || curve != b"nistp256" { return Err(Error::CouldNotReadKey); } let sec1_bytes = p.read_string()?; @@ -325,6 +327,7 @@ impl Verify for PublicKey { #[allow(clippy::large_enum_variant)] pub enum KeyPair { Ed25519(ed25519_dalek::SigningKey), + EcdsaSha2NistP256(p256::SecretKey), #[cfg(feature = "openssl")] RSA { key: openssl::rsa::Rsa, @@ -339,6 +342,8 @@ impl Clone for KeyPair { Self::Ed25519(kp) => { Self::Ed25519(ed25519_dalek::SigningKey::from_bytes(&kp.to_bytes())) } + #[allow(clippy::expect_used)] + Self::EcdsaSha2NistP256(k) => Self::EcdsaSha2NistP256(k.clone()), #[cfg(feature = "openssl")] Self::RSA { key, hash } => Self::RSA { key: key.clone(), @@ -356,6 +361,7 @@ impl std::fmt::Debug for KeyPair { "Ed25519 {{ public: {:?}, secret: (hidden) }}", key.verifying_key().as_bytes() ), + KeyPair::EcdsaSha2NistP256 { .. } => write!(f, "P256 {{ (hidden) }}"), #[cfg(feature = "openssl")] KeyPair::RSA { .. } => write!(f, "RSA {{ (hidden) }}"), } @@ -373,6 +379,7 @@ impl KeyPair { pub fn clone_public_key(&self) -> Result { Ok(match self { KeyPair::Ed25519(ref key) => PublicKey::Ed25519(key.verifying_key()), + KeyPair::EcdsaSha2NistP256(ref key) => PublicKey::P256(key.public_key()), #[cfg(feature = "openssl")] KeyPair::RSA { ref key, ref hash } => { use openssl::pkey::PKey; @@ -390,6 +397,7 @@ impl KeyPair { pub fn name(&self) -> &'static str { match *self { KeyPair::Ed25519(_) => ED25519.0, + KeyPair::EcdsaSha2NistP256(_) => ECDSA_SHA2_NISTP256.0, #[cfg(feature = "openssl")] KeyPair::RSA { ref hash, .. } => hash.name().0, } @@ -414,10 +422,12 @@ impl KeyPair { /// Sign a slice using this algorithm. pub fn sign_detached(&self, to_sign: &[u8]) -> Result { match self { - #[allow(clippy::unwrap_used)] KeyPair::Ed25519(ref secret) => Ok(Signature::Ed25519(SignatureBytes( secret.sign(to_sign).to_bytes(), ))), + KeyPair::EcdsaSha2NistP256(ref secret) => Ok(Signature::P256({ + p256_signature(secret, to_sign) + })), #[cfg(feature = "openssl")] KeyPair::RSA { ref key, ref hash } => Ok(Signature::RSA { bytes: rsa_signature(hash, key, to_sign)?, @@ -443,6 +453,10 @@ impl KeyPair { buffer.extend_ssh_string(ED25519.0.as_bytes()); buffer.extend_ssh_string(signature.to_bytes().as_slice()); } + KeyPair::EcdsaSha2NistP256(ref secret) => { + let signature = p256_signature(secret, to_sign.as_ref()); + encode_ecdsa_signature(buffer, &ECDSA_SHA2_NISTP256, &signature); + } #[cfg(feature = "openssl")] KeyPair::RSA { ref key, ref hash } => { // https://tools.ietf.org/html/draft-rsa-dsa-sha2-256-02#section-2.2 @@ -468,6 +482,10 @@ impl KeyPair { buffer.extend_ssh_string(ED25519.0.as_bytes()); buffer.extend_ssh_string(signature.to_bytes().as_slice()); } + KeyPair::EcdsaSha2NistP256(ref secret) => { + let signature = p256_signature(secret, buffer.as_ref()); + encode_ecdsa_signature(buffer, &ECDSA_SHA2_NISTP256, &signature); + } #[cfg(feature = "openssl")] KeyPair::RSA { ref key, ref hash } => { // https://tools.ietf.org/html/draft-rsa-dsa-sha2-256-02#section-2.2 @@ -486,6 +504,7 @@ impl KeyPair { pub fn with_signature_hash(&self, hash: SignatureHash) -> Option { match self { KeyPair::Ed25519(_) => None, + KeyPair::EcdsaSha2NistP256(_) => None, #[cfg(feature = "openssl")] KeyPair::RSA { key, .. } => Some(KeyPair::RSA { key: key.clone(), @@ -519,6 +538,37 @@ fn rsa_signature( Ok(signer.sign_to_vec()?) } +fn p256_signature( + secret: &p256::SecretKey, + to_sign: &[u8], +) -> p256::ecdsa::Signature { + let signer = p256::ecdsa::SigningKey::from(secret); + signer.sign(to_sign) +} + +pub(crate) fn encode_ecdsa_signature( + buffer: &mut E, + name: &Name, + signature: &p256::ecdsa::Signature, +) { + let r = signature.r().to_repr(); + let s = signature.s().to_repr(); + + let mut blob = vec![]; + blob.extend_ssh_mpint(&r); + blob.extend_ssh_mpint(&s); + + #[allow(clippy::unwrap_used)] // Vec<>.write_all can't fail + buffer + // .write_u32::((name.0.len() + mpint_len(&r) + mpint_len(&s) + 4) as u32) + .write_u32::((name.0.len() + blob.len() + 8) as u32) + .unwrap(); + buffer.extend_ssh_string(name.0.as_bytes()); + buffer.extend_ssh_string(&blob[..]); + // buffer.extend_ssh_mpint(&r); + // buffer.extend_ssh_mpint(&s); +} + /// Parse a public key from a byte slice. pub fn parse_public_key( p: &[u8], diff --git a/russh-keys/src/lib.rs b/russh-keys/src/lib.rs index f23263a1..853b0017 100644 --- a/russh-keys/src/lib.rs +++ b/russh-keys/src/lib.rs @@ -82,6 +82,7 @@ pub mod key; pub mod signature; mod format; +pub mod deps; pub use format::*; /// A module to write SSH agent. @@ -163,6 +164,7 @@ impl From for Error { } const KEYTYPE_ED25519: &[u8] = b"ssh-ed25519"; +const KEYTYPE_P256: &[u8] = b"ecdsa-sha2-nistp256"; const KEYTYPE_RSA: &[u8] = b"ssh-rsa"; /// Load a public key from a file. Ed25519, EC-DSA and RSA keys are supported. @@ -261,6 +263,12 @@ impl PublicKeyBase64 for key::KeyPair { s.write_u32::(public.len() as u32).unwrap(); s.extend_from_slice(public.as_slice()); } + key::KeyPair::EcdsaSha2NistP256(ref key) => { + let public = key.public_key().to_sec1_bytes(); + #[allow(clippy::unwrap_used)] // Vec<>.write can't fail + s.write_u32::(public.len() as u32).unwrap(); + s.extend_from_slice(&public); + }, #[cfg(feature = "openssl")] key::KeyPair::RSA { ref key, .. } => { use encoding::Encoding; diff --git a/russh-keys/src/signature.rs b/russh-keys/src/signature.rs index eab19345..ed8c7709 100644 --- a/russh-keys/src/signature.rs +++ b/russh-keys/src/signature.rs @@ -6,7 +6,7 @@ use serde::de::{SeqAccess, Visitor}; use serde::ser::SerializeTuple; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::key::SignatureHash; +use crate::key::{SignatureHash, ECDSA_SHA2_NISTP256, encode_ecdsa_signature}; use crate::Error; pub struct SignatureBytes(pub [u8; 64]); @@ -17,7 +17,7 @@ pub enum Signature { /// An Ed25519 signature Ed25519(SignatureBytes), /// An EC-DSA NIST P-256 signature - P256(Vec), + P256(p256::ecdsa::Signature), /// An RSA signature RSA { hash: SignatureHash, bytes: Vec }, } @@ -36,14 +36,8 @@ 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::P256(ref signature) => { + encode_ecdsa_signature(&mut bytes_, &ECDSA_SHA2_NISTP256, &signature); } Signature::RSA { ref hash, @@ -91,7 +85,9 @@ impl Signature { hash: SignatureHash::SHA1, bytes: bytes.to_vec(), }), - b"ecdsa-sha2-nistp256" => Ok(Signature::P256(bytes.to_vec())), + b"ecdsa-sha2-nistp256" => Ok(Signature::P256( + p256::ecdsa::Signature::from_slice(bytes)?, + )), _ => Err(Error::UnknownSignatureType { sig_type: std::str::from_utf8(typ).unwrap_or("").to_string(), }), @@ -104,7 +100,7 @@ impl AsRef<[u8]> for Signature { match *self { Signature::Ed25519(ref signature) => &signature.0, Signature::RSA { ref bytes, .. } => &bytes[..], - Signature::P256(ref signature) => signature, + Signature::P256(ref signature) => todo!(), } } } diff --git a/russh/examples/client_exec_interactive.rs b/russh/examples/client_exec_interactive.rs index 4127ae37..f647d101 100644 --- a/russh/examples/client_exec_interactive.rs +++ b/russh/examples/client_exec_interactive.rs @@ -21,7 +21,6 @@ use tokio::net::ToSocketAddrs; #[tokio::main] async fn main() -> Result<()> { env_logger::builder() - .filter_level(log::LevelFilter::Info) .init(); // CLI options are defined later in this file diff --git a/russh/examples/client_exec_simple.rs b/russh/examples/client_exec_simple.rs index c3ff0585..157f5b57 100644 --- a/russh/examples/client_exec_simple.rs +++ b/russh/examples/client_exec_simple.rs @@ -18,7 +18,6 @@ use tokio::net::ToSocketAddrs; #[tokio::main] async fn main() -> Result<()> { env_logger::builder() - .filter_level(log::LevelFilter::Info) .init(); // CLI options are defined later in this file @@ -81,6 +80,7 @@ impl Session { addrs: A, ) -> Result { let key_pair = load_secret_key(key_path, None)?; + println!("{:?}", key_pair.public_key_base64()); let config = client::Config { inactivity_timeout: Some(Duration::from_secs(5)), ..<_>::default() diff --git a/russh/src/key.rs b/russh/src/key.rs index 5930231b..5ef36206 100644 --- a/russh/src/key.rs +++ b/russh/src/key.rs @@ -1,3 +1,5 @@ +use openssl::pkey::HasPublic; +use openssl::rsa::Rsa; // Copyright 2016 Pierre-Étienne Meunier // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,37 +15,58 @@ // limitations under the License. // use russh_cryptovec::CryptoVec; +use russh_keys::deps::ecdsa::elliptic_curve; +use russh_keys::deps::ed25519_dalek; +use russh_keys::deps::p256::NistP256; use russh_keys::encoding::*; use russh_keys::key::*; -use russh_keys::PublicKeyBase64; #[doc(hidden)] pub trait PubKey { fn push_to(&self, buffer: &mut CryptoVec); } +impl PubKey for ed25519_dalek::VerifyingKey { + fn push_to(&self, buffer: &mut CryptoVec) { + buffer.push_u32_be((ED25519.0.len() + self.as_bytes().len() + 8) as u32); + buffer.extend_ssh_string(ED25519.0.as_bytes()); + buffer.extend_ssh_string(self.as_bytes()); + } +} + +impl PubKey for elliptic_curve::PublicKey { + fn push_to(&self, buffer: &mut CryptoVec) { + let public = self.to_sec1_bytes(); + buffer.push_u32_be((ECDSA_SHA2_NISTP256.0.len() + public.len() + 20) as u32); + buffer.extend_ssh_string(ECDSA_SHA2_NISTP256.0.as_bytes()); + buffer.extend_ssh_string(b"nistp256"); + buffer.extend_ssh_string(&public); + } +} + +#[cfg(feature = "openssl")] +impl PubKey for Rsa +where + T: HasPublic, +{ + fn push_to(&self, buffer: &mut CryptoVec) { + let e = self.e().to_vec(); + let n = self.n().to_vec(); + buffer.push_u32_be((4 + SSH_RSA.0.len() + mpint_len(&n) + mpint_len(&e)) as u32); + buffer.extend_ssh_string(SSH_RSA.0.as_bytes()); + buffer.extend_ssh_mpint(&e); + buffer.extend_ssh_mpint(&n); + } +} + impl PubKey for PublicKey { fn push_to(&self, buffer: &mut CryptoVec) { match self { - PublicKey::Ed25519(ref public) => { - buffer.push_u32_be((ED25519.0.len() + public.as_bytes().len() + 8) as u32); - 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()); - } + PublicKey::Ed25519(ref key) => key.push_to(buffer), + PublicKey::P256(ref key) => key.push_to(buffer), #[cfg(feature = "openssl")] - PublicKey::RSA { ref key, .. } => { - #[allow(clippy::unwrap_used)] // type known - let rsa = key.0.rsa().unwrap(); - let e = rsa.e().to_vec(); - let n = rsa.n().to_vec(); - buffer.push_u32_be((4 + SSH_RSA.0.len() + mpint_len(&n) + mpint_len(&e)) as u32); - buffer.extend_ssh_string(SSH_RSA.0.as_bytes()); - buffer.extend_ssh_mpint(&e); - buffer.extend_ssh_mpint(&n); - } + #[allow(clippy::unwrap_used)] // type known + PublicKey::RSA { ref key, .. } => key.0.rsa().unwrap().push_to(buffer), } } } @@ -51,21 +74,10 @@ impl PubKey for PublicKey { impl PubKey for KeyPair { fn push_to(&self, buffer: &mut CryptoVec) { match self { - KeyPair::Ed25519(ref key) => { - let public = key.verifying_key().to_bytes(); - buffer.push_u32_be((ED25519.0.len() + public.len() + 8) as u32); - buffer.extend_ssh_string(ED25519.0.as_bytes()); - buffer.extend_ssh_string(public.as_slice()); - } + KeyPair::Ed25519(ref key) => key.verifying_key().push_to(buffer), + KeyPair::EcdsaSha2NistP256(ref key) => key.public_key().push_to(buffer), #[cfg(feature = "openssl")] - KeyPair::RSA { ref key, .. } => { - let e = key.e().to_vec(); - let n = key.n().to_vec(); - buffer.push_u32_be((4 + SSH_RSA.0.len() + mpint_len(&n) + mpint_len(&e)) as u32); - buffer.extend_ssh_string(SSH_RSA.0.as_bytes()); - buffer.extend_ssh_mpint(&e); - buffer.extend_ssh_mpint(&n); - } + KeyPair::RSA { ref key, .. } => key.push_to(buffer), } } } diff --git a/russh/src/negotiation.rs b/russh/src/negotiation.rs index 5b0cf0af..105f6159 100644 --- a/russh/src/negotiation.rs +++ b/russh/src/negotiation.rs @@ -137,6 +137,7 @@ impl Named for KeyPair { fn name(&self) -> &'static str { match self { KeyPair::Ed25519 { .. } => ED25519.0, + KeyPair::EcdsaSha2NistP256 { .. } => ECDSA_SHA2_NISTP256.0, #[cfg(feature = "openssl")] KeyPair::RSA { ref hash, .. } => hash.name().0, }