diff --git a/.gitignore b/.gitignore index c2d07934..abb7748a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,3 @@ target Cargo.lock .cargo-ok -ca-test* diff --git a/Cargo.toml b/Cargo.toml index 83190951..9dc05561 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,5 @@ [workspace] -members = [ - "russh-keys", - "russh", - "russh-config", - "cryptovec", - "pageant", - "russh-util", -] +members = ["russh-keys", "russh", "russh-config", "cryptovec", "pageant", "russh-util"] resolver = "2" [patch.crates-io] @@ -23,19 +16,12 @@ digest = "0.10" futures = "0.3" hmac = "0.12" log = "0.4" +openssl = { version = "0.10" } rand = "0.8" sha1 = { version = "0.10", features = ["oid"] } sha2 = { version = "0.10", features = ["oid"] } -signature = "2.2" ssh-encoding = "0.2" -ssh-key = { version = "0.6", features = [ - "ed25519", - "rsa", - "p256", - "p384", - "p521", - "encryption", -] } +ssh-key = { version = "0.6", features = ["ed25519", "rsa", "encryption"] } thiserror = "1.0" tokio = { version = "1.17.0" } tokio-stream = { version = "0.1", features = ["net", "sync"] } diff --git a/README.md b/README.md index c0a91038..d0c4cab0 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,12 @@ This is a fork of [Thrussh](https://nest.pijul.com/pijul/thrussh) by Pierre-Éti * `publickey` * `keyboard-interactive` * `none` - * OpenSSH certificates ✨ + * OpenSSH certificates (client only ✨) * Dependency updates * OpenSSH keepalive request handling ✨ * OpenSSH agent forwarding channels ✨ * OpenSSH `server-sig-algs` extension ✨ +* `openssl` dependency is optional ✨ ## Safety diff --git a/russh-keys/Cargo.toml b/russh-keys/Cargo.toml index 5b832494..10206cb0 100644 --- a/russh-keys/Cargo.toml +++ b/russh-keys/Cargo.toml @@ -31,6 +31,7 @@ inout = { version = "0.1", features = ["std"] } log = { workspace = true } md5 = "0.7" num-integer = "0.1" +openssl = { workspace = true, optional = true } p256 = "0.13" p384 = "0.13" p521 = "0.13" @@ -47,7 +48,6 @@ sec1 = { version = "0.7", features = ["pkcs8"] } serde = { version = "1.0", features = ["derive"] } sha1 = { workspace = true } sha2 = { workspace = true } -signature = { workspace = true } spki = "0.7" ssh-encoding = { workspace = true } ssh-key = { workspace = true } @@ -62,6 +62,7 @@ tokio = { workspace = true, features = [ ] } [features] +vendored-openssl = ["openssl", "openssl/vendored"] legacy-ed25519-pkcs8-parser = ["yasna"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -82,3 +83,6 @@ pageant = { version = "0.0.1-beta.3", path = "../pageant" } env_logger = "0.11" tempdir = "0.3" tokio = { workspace = true, features = ["test-util", "macros", "process"] } + +[package.metadata.docs.rs] +features = ["openssl"] diff --git a/russh-keys/src/agent/client.rs b/russh-keys/src/agent/client.rs index c707a356..82d78980 100644 --- a/russh-keys/src/agent/client.rs +++ b/russh-keys/src/agent/client.rs @@ -1,16 +1,15 @@ -use core::str; +use std::convert::TryFrom; use byteorder::{BigEndian, ByteOrder}; use log::debug; use russh_cryptovec::CryptoVec; -use ssh_key::{Algorithm, HashAlg, PrivateKey, PublicKey, Signature}; use tokio; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use super::{msg, Constraint}; use crate::encoding::{Encoding, Reader}; -use crate::helpers::EncodedExt; -use crate::{key, Error}; +use crate::key::{PublicKey, SignatureHash}; +use crate::{key, protocol, Error, PublicKeyBase64}; pub trait AgentStream: AsyncRead + AsyncWrite {} @@ -143,7 +142,7 @@ impl AgentClient { /// constraints to apply when using the key to sign. pub async fn add_identity( &mut self, - key: &PrivateKey, + key: &key::KeyPair, constraints: &[Constraint], ) -> Result<(), Error> { // See IETF draft-miller-ssh-agent-13, section 3.2 for format. @@ -155,10 +154,30 @@ impl AgentClient { } else { self.buf.push(msg::ADD_ID_CONSTRAINED) } - - self.buf.extend(key.key_data().encoded()?.as_slice()); - self.buf.extend_ssh_string(&[]); // comment field - + match *key { + key::KeyPair::Ed25519(ref pair) => { + self.buf.extend_ssh_string(b"ssh-ed25519"); + self.buf.extend_ssh_string(pair.verifying_key().as_bytes()); + self.buf.push_u32_be(64); + self.buf.extend(pair.to_bytes().as_slice()); + self.buf.extend(pair.verifying_key().as_bytes()); + self.buf.extend_ssh_string(b""); + } + #[allow(clippy::unwrap_used)] // key is known to be private + key::KeyPair::RSA { ref key, .. } => { + self.buf.extend_ssh_string(b"ssh-rsa"); + self.buf + .extend_ssh(&protocol::RsaPrivateKey::try_from(key)?); + } + key::KeyPair::EC { ref key } => { + self.buf.extend_ssh_string(key.algorithm().as_bytes()); + self.buf.extend_ssh_string(key.ident().as_bytes()); + self.buf + .extend_ssh_string(&key.to_public_key().to_sec1_bytes()); + self.buf.extend_ssh_mpint(&key.to_secret_bytes()); + self.buf.extend_ssh_string(b""); // comment + } + } if !constraints.is_empty() { for cons in constraints { match *cons { @@ -273,7 +292,10 @@ impl AgentClient { for _ in 0..n { let key_blob = r.read_string()?; let _comment = r.read_string()?; - keys.push(key::parse_public_key(key_blob)?); + keys.push(key::parse_public_key( + key_blob, + Some(SignatureHash::SHA2_512), + )?); } } @@ -281,46 +303,55 @@ impl AgentClient { } /// Ask the agent to sign the supplied piece of data. - pub async fn sign_request( - &mut self, - public: &PublicKey, + pub fn sign_request( + mut self, + public: &key::PublicKey, mut data: CryptoVec, - ) -> Result { + ) -> impl futures::Future)> { debug!("sign_request: {:?}", data); - let hash = self.prepare_sign_request(public, &data)?; + let hash = self.prepare_sign_request(public, &data); - self.read_response().await?; + async move { + if let Err(e) = hash { + return (self, Err(e)); + } - if self.buf.first() == Some(&msg::SIGN_RESPONSE) { - self.write_signature(hash, &mut data)?; - Ok(data) - } else if self.buf.first() == Some(&msg::FAILURE) { - Err(Error::AgentFailure) - } else { - debug!("self.buf = {:?}", &self.buf[..]); - Ok(data) + let resp = self.read_response().await; + debug!("resp = {:?}", &self.buf[..]); + if let Err(e) = resp { + return (self, Err(e)); + } + + #[allow(clippy::indexing_slicing, clippy::unwrap_used)] + // length is checked, hash already checked + if !self.buf.is_empty() && self.buf[0] == msg::SIGN_RESPONSE { + let resp = self.write_signature(hash.unwrap(), &mut data); + if let Err(e) = resp { + return (self, Err(e)); + } + (self, Ok(data)) + } else if self.buf.first() == Some(&msg::FAILURE) { + (self, Err(Error::AgentFailure)) + } else { + debug!("self.buf = {:?}", &self.buf[..]); + (self, Ok(data)) + } } } - fn prepare_sign_request( - &mut self, - public: &ssh_key::PublicKey, - data: &[u8], - ) -> Result { + fn prepare_sign_request(&mut self, public: &key::PublicKey, data: &[u8]) -> Result { self.buf.clear(); self.buf.resize(4); self.buf.push(msg::SIGN_REQUEST); key_blob(public, &mut self.buf)?; self.buf.extend_ssh_string(data); debug!("public = {:?}", public); - let hash = match public.algorithm() { - Algorithm::Rsa { - hash: Some(HashAlg::Sha256), - } => 2, - Algorithm::Rsa { - hash: Some(HashAlg::Sha512), - } => 4, - Algorithm::Rsa { hash: None } => 0, + let hash = match public { + PublicKey::RSA { hash, .. } => match hash { + SignatureHash::SHA2_256 => 2, + SignatureHash::SHA2_512 => 4, + SignatureHash::SHA1 => 0, + }, _ => 0, }; self.buf.push_u32_be(hash); @@ -345,7 +376,7 @@ impl AgentClient { /// Ask the agent to sign the supplied piece of data. pub fn sign_request_base64( mut self, - public: &ssh_key::PublicKey, + public: &key::PublicKey, data: &[u8], ) -> impl futures::Future)> { debug!("sign_request: {:?}", data); @@ -371,32 +402,67 @@ impl AgentClient { } /// Ask the agent to sign the supplied piece of data, and return a `Signature`. - pub async fn sign_request_signature( - &mut self, - public: &ssh_key::PublicKey, + pub fn sign_request_signature( + mut self, + public: &key::PublicKey, data: &[u8], - ) -> Result { + ) -> impl futures::Future)> { debug!("sign_request: {:?}", data); - self.prepare_sign_request(public, data)?; - self.read_response().await?; + let r = self.prepare_sign_request(public, data); - #[allow(clippy::indexing_slicing)] // length is checked - if !self.buf.is_empty() && self.buf[0] == msg::SIGN_RESPONSE { - let mut r = self.buf.reader(1); - let mut resp = r.read_string()?.reader(0); - let typ = String::from_utf8(resp.read_string()?.into())?; - let sig = resp.read_string()?; - let algo = Algorithm::new(&typ)?; - let sig = Signature::new(algo, sig.to_vec())?; - Ok(sig) - } else { - Err(Error::AgentProtocolError) + async move { + if let Err(e) = r { + return (self, Err(e)); + } + + if let Err(e) = self.read_response().await { + return (self, Err(e)); + } + + #[allow(clippy::indexing_slicing)] // length is checked + if !self.buf.is_empty() && self.buf[0] == msg::SIGN_RESPONSE { + let as_sig = |buf: &CryptoVec| -> Result { + let mut r = buf.reader(1); + let mut resp = r.read_string()?.reader(0); + let typ = resp.read_string()?; + let sig = resp.read_string()?; + use crate::signature::Signature; + match typ { + b"ssh-rsa" => Ok(Signature::RSA { + bytes: sig.to_vec(), + hash: SignatureHash::SHA1, + }), + b"rsa-sha2-256" => Ok(Signature::RSA { + bytes: sig.to_vec(), + hash: SignatureHash::SHA2_256, + }), + b"rsa-sha2-512" => Ok(Signature::RSA { + bytes: sig.to_vec(), + hash: SignatureHash::SHA2_512, + }), + b"ssh-ed25519" => { + let mut sig_bytes = [0; 64]; + sig_bytes.clone_from_slice(sig); + Ok(Signature::Ed25519(crate::signature::SignatureBytes( + sig_bytes, + ))) + } + _ => Err(Error::UnknownSignatureType { + sig_type: std::str::from_utf8(typ).unwrap_or("").to_string(), + }), + } + }; + let sig = as_sig(&self.buf); + (self, sig) + } else { + (self, Err(Error::AgentProtocolError)) + } } } /// Ask the agent to remove a key from its memory. - pub async fn remove_identity(&mut self, public: &ssh_key::PublicKey) -> Result<(), Error> { + pub async fn remove_identity(&mut self, public: &key::PublicKey) -> Result<(), Error> { self.buf.clear(); self.buf.resize(4); self.buf.push(msg::REMOVE_IDENTITY); @@ -461,7 +527,29 @@ impl AgentClient { } } -fn key_blob(public: &ssh_key::PublicKey, buf: &mut CryptoVec) -> Result<(), Error> { - buf.extend_ssh_string(public.key_data().encoded()?.as_slice()); +fn key_blob(public: &key::PublicKey, buf: &mut CryptoVec) -> Result<(), Error> { + match *public { + PublicKey::RSA { ref key, .. } => { + buf.extend(&[0, 0, 0, 0]); + let len0 = buf.len(); + buf.extend_ssh_string(b"ssh-rsa"); + buf.extend_ssh(&protocol::RsaPublicKey::from(key)); + let len1 = buf.len(); + #[allow(clippy::indexing_slicing)] // length is known + BigEndian::write_u32(&mut buf[5..], (len1 - len0) as u32); + } + PublicKey::Ed25519(ref p) => { + buf.extend(&[0, 0, 0, 0]); + let len0 = buf.len(); + buf.extend_ssh_string(b"ssh-ed25519"); + buf.extend_ssh_string(p.as_bytes()); + let len1 = buf.len(); + #[allow(clippy::indexing_slicing)] // length is known + BigEndian::write_u32(&mut buf[5..], (len1 - len0) as u32); + } + PublicKey::EC { .. } => { + buf.extend_ssh_string(&public.public_key_bytes()); + } + } Ok(()) } diff --git a/russh-keys/src/agent/server.rs b/russh-keys/src/agent/server.rs index 0dc3caa3..e52306a5 100644 --- a/russh-keys/src/agent/server.rs +++ b/russh-keys/src/agent/server.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::convert::TryFrom; use std::marker::Sync; use std::sync::{Arc, RwLock}; use std::time::{Duration, SystemTime}; @@ -8,19 +9,17 @@ use byteorder::{BigEndian, ByteOrder}; use futures::future::Future; use futures::stream::{Stream, StreamExt}; use russh_cryptovec::CryptoVec; -use ssh_key::PrivateKey; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::time::sleep; use {std, tokio}; use super::{msg, Constraint}; use crate::encoding::{Encoding, Position, Reader}; -use crate::helpers::EncodedExt; -use crate::{add_signature, Error}; +use crate::{key, Error}; #[derive(Clone)] #[allow(clippy::type_complexity)] -struct KeyStore(Arc, (Arc, SystemTime, Vec)>>>); +struct KeyStore(Arc, (Arc, SystemTime, Vec)>>>); #[derive(Clone)] struct Lock(Arc>); @@ -46,7 +45,7 @@ pub enum MessageType { pub trait Agent: Clone + Send + 'static { fn confirm( self, - _pk: Arc, + _pk: Arc, ) -> Box + Unpin + Send> { Box::new(futures::future::ready((self, true))) } @@ -82,7 +81,10 @@ where } impl Agent for () { - fn confirm(self, _: Arc) -> Box + Unpin + Send> { + fn confirm( + self, + _: Arc, + ) -> Box + Unpin + Send> { Box::new(futures::future::ready((self, true))) } } @@ -249,15 +251,19 @@ impl Result { let (blob, key_pair) = { - use ssh_encoding::Decode; + use ssh_encoding::{Decode, Encode}; let private_key = ssh_key::private::PrivateKey::new( ssh_key::private::KeypairData::decode(&mut r)?, "", )?; let _comment = r.read_string()?; + let key_pair = key::KeyPair::try_from(&private_key)?; - (private_key.public_key().key_data().encoded()?, private_key) + let mut blob = Vec::new(); + private_key.public_key().key_data().encode(&mut blob)?; + + (blob, key_pair) }; writebuf.push(msg::SUCCESS); let mut w = self.keys.0.write().or(Err(Error::AgentFailure))?; @@ -326,9 +332,7 @@ impl, + pkey: PKey, +} + +impl RsaPublic { + pub fn verify_detached(&self, hash: &SignatureHash, msg: &[u8], sig: &[u8]) -> bool { + openssl::sign::Verifier::new(message_digest_for(hash), &self.pkey) + .and_then(|mut v| v.verify_oneshot(sig, msg)) + .unwrap_or(false) + } +} + +impl TryFrom<&protocol::RsaPublicKey<'_>> for RsaPublic { + type Error = Error; + + fn try_from(pk: &protocol::RsaPublicKey<'_>) -> Result { + let key = Rsa::from_public_components( + BigNum::from_slice(&pk.modulus)?, + BigNum::from_slice(&pk.public_exponent)?, + )?; + Ok(Self { + pkey: PKey::from_rsa(key.clone())?, + key, + }) + } +} + +impl<'a> From<&RsaPublic> for protocol::RsaPublicKey<'a> { + fn from(key: &RsaPublic) -> Self { + Self { + modulus: key.key.n().to_vec().into(), + public_exponent: key.key.e().to_vec().into(), + } + } +} + +impl PartialEq for RsaPublic { + fn eq(&self, b: &RsaPublic) -> bool { + self.pkey.public_eq(&b.pkey) + } +} + +impl Eq for RsaPublic {} + +impl std::fmt::Debug for RsaPublic { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "RsaPublic {{ (hidden) }}") + } +} + +#[derive(Clone)] +pub struct RsaPrivate { + key: Rsa, + pkey: PKey, +} + +impl RsaPrivate { + pub fn new( + sk: &protocol::RsaPrivateKey<'_>, + extra: Option<&RsaCrtExtra<'_>>, + ) -> Result { + let (d, p, q) = ( + BigNum::from_slice(&sk.private_exponent)?, + BigNum::from_slice(&sk.prime1)?, + BigNum::from_slice(&sk.prime2)?, + ); + let (dp, dq) = if let Some(extra) = extra { + ( + BigNum::from_slice(&extra.dp)?, + BigNum::from_slice(&extra.dq)?, + ) + } else { + calc_dp_dq(d.as_ref(), p.as_ref(), q.as_ref())? + }; + let key = Rsa::from_private_components( + BigNum::from_slice(&sk.public_key.modulus)?, + BigNum::from_slice(&sk.public_key.public_exponent)?, + d, + p, + q, + dp, + dq, + BigNum::from_slice(&sk.coefficient)?, + )?; + key.check_key()?; + Ok(Self { + pkey: PKey::from_rsa(key.clone())?, + key, + }) + } + + pub fn new_from_der(der: &[u8]) -> Result { + let key = Rsa::private_key_from_der(der)?; + key.check_key()?; + Ok(Self { + pkey: PKey::from_rsa(key.clone())?, + key, + }) + } + + pub fn generate(bits: usize) -> Result { + let key = Rsa::generate(bits as u32)?; + Ok(Self { + pkey: PKey::from_rsa(key.clone())?, + key, + }) + } + + pub fn sign(&self, hash: &SignatureHash, msg: &[u8]) -> Result, Error> { + Ok( + openssl::sign::Signer::new(message_digest_for(hash), &self.pkey)? + .sign_oneshot_to_vec(msg)?, + ) + } +} + +impl<'a> TryFrom<&RsaPrivate> for protocol::RsaPrivateKey<'a> { + type Error = Error; + + fn try_from(key: &RsaPrivate) -> Result, Self::Error> { + let key = &key.key; + // We always set these. + if let (Some(p), Some(q), Some(iqmp)) = (key.p(), key.q(), key.iqmp()) { + Ok(protocol::RsaPrivateKey { + public_key: protocol::RsaPublicKey { + modulus: key.n().to_vec().into(), + public_exponent: key.e().to_vec().into(), + }, + private_exponent: key.d().to_vec().into(), + prime1: p.to_vec().into(), + prime2: q.to_vec().into(), + coefficient: iqmp.to_vec().into(), + comment: b"".as_slice().into(), + }) + } else { + Err(Error::KeyIsCorrupt) + } + } +} + +impl<'a> TryFrom<&RsaPrivate> for RsaCrtExtra<'a> { + type Error = Error; + + fn try_from(key: &RsaPrivate) -> Result, Self::Error> { + let key = &key.key; + // We always set these. + if let (Some(dp), Some(dq)) = (key.dmp1(), key.dmq1()) { + Ok(RsaCrtExtra { + dp: dp.to_vec().into(), + dq: dq.to_vec().into(), + }) + } else { + Err(Error::KeyIsCorrupt) + } + } +} + +impl<'a> From<&RsaPrivate> for protocol::RsaPublicKey<'a> { + fn from(key: &RsaPrivate) -> Self { + Self { + modulus: key.key.n().to_vec().into(), + public_exponent: key.key.e().to_vec().into(), + } + } +} + +impl TryFrom<&RsaPrivate> for RsaPublic { + type Error = Error; + + fn try_from(key: &RsaPrivate) -> Result { + let key = Rsa::from_public_components(key.key.n().to_owned()?, key.key.e().to_owned()?)?; + Ok(Self { + pkey: PKey::from_rsa(key.clone())?, + key, + }) + } +} + +fn message_digest_for(hash: &SignatureHash) -> MessageDigest { + match hash { + SignatureHash::SHA2_256 => MessageDigest::sha256(), + SignatureHash::SHA2_512 => MessageDigest::sha512(), + SignatureHash::SHA1 => MessageDigest::sha1(), + } +} + +fn calc_dp_dq(d: &BigNumRef, p: &BigNumRef, q: &BigNumRef) -> Result<(BigNum, BigNum), Error> { + let one = BigNum::from_u32(1)?; + let p1 = p - one.as_ref(); + let q1 = q - one.as_ref(); + let mut context = BigNumContext::new()?; + let mut dp = BigNum::new()?; + let mut dq = BigNum::new()?; + dp.checked_rem(d, &p1, &mut context)?; + dq.checked_rem(d, &q1, &mut context)?; + Ok((dp, dq)) +} diff --git a/russh-keys/src/backend_rust.rs b/russh-keys/src/backend_rust.rs new file mode 100644 index 00000000..9b568887 --- /dev/null +++ b/russh-keys/src/backend_rust.rs @@ -0,0 +1,184 @@ +use std::convert::TryFrom; + +use rsa::traits::{PrivateKeyParts, PublicKeyParts}; +use rsa::BigUint; + +use crate::key::{RsaCrtExtra, SignatureHash}; +use crate::{protocol, Error}; + +#[derive(Clone, PartialEq, Eq)] +pub struct RsaPublic { + key: rsa::RsaPublicKey, +} + +impl RsaPublic { + pub fn verify_detached(&self, hash: &SignatureHash, msg: &[u8], sig: &[u8]) -> bool { + self.key + .verify(signature_scheme_for_hash(hash), &hash_msg(hash, msg), sig) + .is_ok() + } +} + +impl TryFrom<&protocol::RsaPublicKey<'_>> for RsaPublic { + type Error = Error; + + fn try_from(pk: &protocol::RsaPublicKey<'_>) -> Result { + Ok(Self { + key: rsa::RsaPublicKey::new( + BigUint::from_bytes_be(&pk.modulus), + BigUint::from_bytes_be(&pk.public_exponent), + )?, + }) + } +} + +impl<'a> From<&RsaPublic> for protocol::RsaPublicKey<'a> { + fn from(key: &RsaPublic) -> Self { + Self { + modulus: key.key.n().to_bytes_be().into(), + public_exponent: key.key.e().to_bytes_be().into(), + } + } +} + +impl std::fmt::Debug for RsaPublic { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "RsaPublic {{ (hidden) }}") + } +} + +#[derive(Clone)] +pub struct RsaPrivate { + key: rsa::RsaPrivateKey, +} + +impl RsaPrivate { + pub fn new( + sk: &protocol::RsaPrivateKey<'_>, + extra: Option<&RsaCrtExtra<'_>>, + ) -> Result { + let mut key = rsa::RsaPrivateKey::from_components( + BigUint::from_bytes_be(&sk.public_key.modulus), + BigUint::from_bytes_be(&sk.public_key.public_exponent), + BigUint::from_bytes_be(&sk.private_exponent), + vec![ + BigUint::from_bytes_be(&sk.prime1), + BigUint::from_bytes_be(&sk.prime2), + ], + )?; + key.validate()?; + key.precompute()?; + + if Some(BigUint::from_bytes_be(&sk.coefficient)) != key.crt_coefficient() { + return Err(Error::KeyIsCorrupt); + } + if let Some(extra) = extra { + if ( + Some(&BigUint::from_bytes_be(&extra.dp)), + Some(&BigUint::from_bytes_be(&extra.dq)), + ) != (key.dp(), key.dq()) + { + return Err(Error::KeyIsCorrupt); + } + } + + Ok(Self { key }) + } + + pub fn new_from_der(der: &[u8]) -> Result { + use pkcs1::DecodeRsaPrivateKey; + Ok(Self { + key: rsa::RsaPrivateKey::from_pkcs1_der(der)?, + }) + } + + pub fn generate(bits: usize) -> Result { + Ok(Self { + key: rsa::RsaPrivateKey::new(&mut crate::key::safe_rng(), bits)?, + }) + } + + pub fn sign(&self, hash: &SignatureHash, msg: &[u8]) -> Result, Error> { + Ok(self + .key + .sign(signature_scheme_for_hash(hash), &hash_msg(hash, msg))?) + } +} + +impl<'a> TryFrom<&RsaPrivate> for protocol::RsaPrivateKey<'a> { + type Error = Error; + + fn try_from(key: &RsaPrivate) -> Result, Self::Error> { + let key = &key.key; + // We always precompute these. + if let ([p, q], Some(iqmp)) = (key.primes(), key.crt_coefficient()) { + Ok(protocol::RsaPrivateKey { + public_key: protocol::RsaPublicKey { + modulus: key.n().to_bytes_be().into(), + public_exponent: key.e().to_bytes_be().into(), + }, + private_exponent: key.d().to_bytes_be().into(), + prime1: p.to_bytes_be().into(), + prime2: q.to_bytes_be().into(), + coefficient: iqmp.to_bytes_be().into(), + comment: b"".as_slice().into(), + }) + } else { + Err(Error::KeyIsCorrupt) + } + } +} + +impl<'a> TryFrom<&RsaPrivate> for RsaCrtExtra<'a> { + type Error = Error; + + fn try_from(key: &RsaPrivate) -> Result, Self::Error> { + let key = &key.key; + // We always precompute these. + if let (Some(dp), Some(dq)) = (key.dp(), key.dq()) { + Ok(RsaCrtExtra { + dp: dp.to_bytes_be().into(), + dq: dq.to_bytes_be().into(), + }) + } else { + Err(Error::KeyIsCorrupt) + } + } +} + +impl<'a> From<&RsaPrivate> for protocol::RsaPublicKey<'a> { + fn from(key: &RsaPrivate) -> Self { + Self { + modulus: key.key.n().to_bytes_be().into(), + public_exponent: key.key.e().to_bytes_be().into(), + } + } +} + +impl TryFrom<&RsaPrivate> for RsaPublic { + type Error = Error; + + fn try_from(key: &RsaPrivate) -> Result { + Ok(Self { + key: key.key.to_public_key(), + }) + } +} + +fn signature_scheme_for_hash(hash: &SignatureHash) -> rsa::pkcs1v15::Pkcs1v15Sign { + use rsa::pkcs1v15::Pkcs1v15Sign; + match *hash { + SignatureHash::SHA2_256 => Pkcs1v15Sign::new::(), + SignatureHash::SHA2_512 => Pkcs1v15Sign::new::(), + SignatureHash::SHA1 => Pkcs1v15Sign::new::(), + } +} + +fn hash_msg(hash: &SignatureHash, msg: &[u8]) -> Vec { + use digest::Digest; + match *hash { + SignatureHash::SHA2_256 => sha2::Sha256::digest(msg).to_vec(), + SignatureHash::SHA2_512 => sha2::Sha512::digest(msg).to_vec(), + SignatureHash::SHA1 => sha1::Sha1::digest(msg).to_vec(), + } +} diff --git a/russh-keys/src/ec.rs b/russh-keys/src/ec.rs new file mode 100644 index 00000000..689ad15a --- /dev/null +++ b/russh-keys/src/ec.rs @@ -0,0 +1,263 @@ +use elliptic_curve::{Curve, CurveArithmetic, FieldBytes, FieldBytesSize}; + +use crate::key::safe_rng; +use crate::Error; + +// p521::{SigningKey, VerifyingKey} are wrapped versions and do not provide PartialEq and Eq, hence +// we make our own type alias here. +mod local_p521 { + use rand_core::CryptoRngCore; + use sha2::{Digest, Sha512}; + + pub type NistP521 = p521::NistP521; + pub type VerifyingKey = ecdsa::VerifyingKey; + pub type SigningKey = ecdsa::SigningKey; + pub type Signature = ecdsa::Signature; + pub type Result = ecdsa::Result; + + // Implement signing because p521::NistP521 does not implement DigestPrimitive trait. + pub fn try_sign_with_rng( + key: &SigningKey, + rng: &mut impl CryptoRngCore, + msg: &[u8], + ) -> Result { + use ecdsa::hazmat::{bits2field, sign_prehashed}; + use elliptic_curve::Field; + let prehash = Sha512::digest(msg); + let z = bits2field::(&prehash)?; + let k = p521::Scalar::random(rng); + sign_prehashed(key.as_nonzero_scalar().as_ref(), k, &z).map(|sig| sig.0) + } + + // Implement verifying because ecdsa::VerifyingKey does not satisfy the trait + // bound requirements of the DigestVerifier's implementation in ecdsa crate. + pub fn verify(key: &VerifyingKey, msg: &[u8], signature: &Signature) -> Result<()> { + use ecdsa::signature::hazmat::PrehashVerifier; + key.verify_prehash(&Sha512::digest(msg), signature) + } +} + +const CURVE_NISTP256: &str = "nistp256"; +const CURVE_NISTP384: &str = "nistp384"; +const CURVE_NISTP521: &str = "nistp521"; + +/// An ECC public key. +#[derive(Clone, Eq, PartialEq)] +pub enum PublicKey { + P256(p256::ecdsa::VerifyingKey), + P384(p384::ecdsa::VerifyingKey), + P521(local_p521::VerifyingKey), +} + +impl PublicKey { + /// Returns the elliptic curve domain parameter identifiers defined in RFC 5656 section 6.1. + pub fn ident(&self) -> &'static str { + match self { + Self::P256(_) => CURVE_NISTP256, + Self::P384(_) => CURVE_NISTP384, + Self::P521(_) => CURVE_NISTP521, + } + } + + /// Returns the ECC public key algorithm name defined in RFC 5656 section 6.2, in the form of + /// `"ecdsa-sha2-[identifier]"`. + pub fn algorithm(&self) -> &'static str { + match self { + Self::P256(_) => crate::ECDSA_SHA2_NISTP256, + Self::P384(_) => crate::ECDSA_SHA2_NISTP384, + Self::P521(_) => crate::ECDSA_SHA2_NISTP521, + } + } + + /// Creates a `PrivateKey` from algorithm name and SEC1-encoded point on curve. + pub fn from_sec1_bytes(algorithm: &[u8], bytes: &[u8]) -> Result { + match algorithm { + crate::KEYTYPE_ECDSA_SHA2_NISTP256 => Ok(Self::P256( + p256::ecdsa::VerifyingKey::from_sec1_bytes(bytes)?, + )), + crate::KEYTYPE_ECDSA_SHA2_NISTP384 => Ok(Self::P384( + p384::ecdsa::VerifyingKey::from_sec1_bytes(bytes)?, + )), + crate::KEYTYPE_ECDSA_SHA2_NISTP521 => Ok(Self::P521( + local_p521::VerifyingKey::from_sec1_bytes(bytes)?, + )), + _ => Err(Error::UnsupportedKeyType { + key_type_string: String::from_utf8(algorithm.to_vec()) + .unwrap_or_else(|_| format!("{algorithm:?}")), + key_type_raw: algorithm.to_vec(), + }), + } + } + + /// Returns the SEC1-encoded public curve point. + pub fn to_sec1_bytes(&self) -> Vec { + match self { + Self::P256(key) => key.to_encoded_point(false).as_bytes().to_vec(), + Self::P384(key) => key.to_encoded_point(false).as_bytes().to_vec(), + Self::P521(key) => key.to_encoded_point(false).as_bytes().to_vec(), + } + } + + /// Verifies message against signature `(r, s)` using the associated digest algorithm. + pub fn verify(&self, msg: &[u8], r: &[u8], s: &[u8]) -> Result<(), Error> { + use ecdsa::signature::Verifier; + match self { + Self::P256(key) => { + key.verify(msg, &signature_from_scalar_bytes::(r, s)?) + } + Self::P384(key) => { + key.verify(msg, &signature_from_scalar_bytes::(r, s)?) + } + Self::P521(key) => local_p521::verify( + key, + msg, + &signature_from_scalar_bytes::(r, s)?, + ), + } + .map_err(Error::from) + } +} + +impl std::fmt::Debug for PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + Self::P256(_) => write!(f, "P256"), + Self::P384(_) => write!(f, "P384"), + Self::P521(_) => write!(f, "P521"), + } + } +} + +/// An ECC private key. +#[derive(Clone, Eq, PartialEq)] +pub enum PrivateKey { + P256(p256::ecdsa::SigningKey), + P384(p384::ecdsa::SigningKey), + P521(local_p521::SigningKey), +} + +impl PrivateKey { + /// Creates a `PrivateKey` with algorithm name and scalar. + pub fn new_from_secret_scalar(algorithm: &[u8], scalar: &[u8]) -> Result { + match algorithm { + crate::KEYTYPE_ECDSA_SHA2_NISTP256 => { + Ok(Self::P256(p256::ecdsa::SigningKey::from_slice(scalar)?)) + } + crate::KEYTYPE_ECDSA_SHA2_NISTP384 => { + Ok(Self::P384(p384::ecdsa::SigningKey::from_slice(scalar)?)) + } + crate::KEYTYPE_ECDSA_SHA2_NISTP521 => { + Ok(Self::P521(local_p521::SigningKey::from_slice(scalar)?)) + } + _ => Err(Error::UnsupportedKeyType { + key_type_string: String::from_utf8(algorithm.to_vec()) + .unwrap_or_else(|_| format!("{algorithm:?}")), + key_type_raw: algorithm.to_vec(), + }), + } + } + + /// Returns the elliptic curve domain parameter identifiers defined in RFC 5656 section 6.1. + pub fn ident(&self) -> &'static str { + match self { + Self::P256(_) => CURVE_NISTP256, + Self::P384(_) => CURVE_NISTP384, + Self::P521(_) => CURVE_NISTP521, + } + } + + /// Returns the ECC public key algorithm name defined in RFC 5656 section 6.2, in the form of + /// `"ecdsa-sha2-[identifier]"`. + pub fn algorithm(&self) -> &'static str { + match self { + Self::P256(_) => crate::ECDSA_SHA2_NISTP256, + Self::P384(_) => crate::ECDSA_SHA2_NISTP384, + Self::P521(_) => crate::ECDSA_SHA2_NISTP521, + } + } + + /// Returns the public key. + pub fn to_public_key(&self) -> PublicKey { + match self { + Self::P256(key) => PublicKey::P256(*key.verifying_key()), + Self::P384(key) => PublicKey::P384(*key.verifying_key()), + Self::P521(key) => PublicKey::P521(*key.verifying_key()), + } + } + + /// Returns the secret scalar in bytes. + pub fn to_secret_bytes(&self) -> Vec { + match self { + Self::P256(key) => key.to_bytes().to_vec(), + Self::P384(key) => key.to_bytes().to_vec(), + Self::P521(key) => key.to_bytes().to_vec(), + } + } + + /// Sign the message with associated digest algorithm. + pub fn try_sign(&self, msg: &[u8]) -> Result<(Vec, Vec), Error> { + use ecdsa::signature::RandomizedSigner; + Ok(match self { + Self::P256(key) => { + signature_to_scalar_bytes(key.try_sign_with_rng(&mut safe_rng(), msg)?) + } + Self::P384(key) => { + signature_to_scalar_bytes(key.try_sign_with_rng(&mut safe_rng(), msg)?) + } + Self::P521(key) => { + signature_to_scalar_bytes(local_p521::try_sign_with_rng(key, &mut safe_rng(), msg)?) + } + }) + } +} + +impl std::fmt::Debug for PrivateKey { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + Self::P256(_) => write!(f, "P256 {{ (hidden) }}"), + Self::P384(_) => write!(f, "P384 {{ (hidden) }}"), + Self::P521(_) => write!(f, "P521 {{ (hidden) }}"), + } + } +} + +fn try_field_bytes_from_mpint(b: &[u8]) -> Option> +where + C: Curve + CurveArithmetic, +{ + use typenum::Unsigned; + let size = FieldBytesSize::::to_usize(); + assert!(size > 0); + #[allow(clippy::indexing_slicing)] // Length checked + if b.len() == size + 1 && b[0] == 0 { + Some(FieldBytes::::clone_from_slice(&b[1..])) + } else if b.len() == size { + Some(FieldBytes::::clone_from_slice(b)) + } else if b.len() < size { + let mut fb: FieldBytes = Default::default(); + fb.as_mut_slice()[size - b.len()..].clone_from_slice(b); + Some(fb) + } else { + None + } +} + +fn signature_from_scalar_bytes(r: &[u8], s: &[u8]) -> Result, Error> +where + C: Curve + CurveArithmetic + elliptic_curve::PrimeCurve, + ecdsa::SignatureSize: elliptic_curve::generic_array::ArrayLength, +{ + Ok(ecdsa::Signature::::from_scalars( + try_field_bytes_from_mpint::(r).ok_or(Error::InvalidSignature)?, + try_field_bytes_from_mpint::(s).ok_or(Error::InvalidSignature)?, + )?) +} + +fn signature_to_scalar_bytes(sig: ecdsa::Signature) -> (Vec, Vec) +where + C: Curve + CurveArithmetic + elliptic_curve::PrimeCurve, + ecdsa::SignatureSize: elliptic_curve::generic_array::ArrayLength, +{ + let (r, s) = sig.split_bytes(); + (r.to_vec(), s.to_vec()) +} diff --git a/russh-keys/src/encoding.rs b/russh-keys/src/encoding.rs index 577539b1..005196d3 100644 --- a/russh-keys/src/encoding.rs +++ b/russh-keys/src/encoding.rs @@ -261,12 +261,6 @@ impl<'a> Position<'a> { Err(Error::IndexOutOfBounds) } } - /// Read a `u64` from this reader by combining two `u32` values. - pub fn read_u64(&mut self) -> Result { - let high = self.read_u32()? as u64; - let low = self.read_u32()? as u64; - Ok((high << 32) | low) - } /// Read one byte from this reader. pub fn read_byte(&mut self) -> Result { if self.position < self.s.len() { diff --git a/russh-keys/src/format/mod.rs b/russh-keys/src/format/mod.rs index cc56f794..b120723c 100644 --- a/russh-keys/src/format/mod.rs +++ b/russh-keys/src/format/mod.rs @@ -1,13 +1,9 @@ -use std::convert::TryInto; use std::io::Write; use data_encoding::{BASE64_MIME, HEXLOWER_PERMISSIVE}; -use pkcs1::DecodeRsaPrivateKey; -use ssh_key::private::RsaKeypair; -use ssh_key::PrivateKey; use super::is_base64_char; -use crate::Error; +use crate::{key, Error}; pub mod openssh; @@ -43,7 +39,7 @@ enum Format { /// Decode a secret key, possibly deciphering it with the supplied /// password. -pub fn decode_secret_key(secret: &str, password: Option<&str>) -> Result { +pub fn decode_secret_key(secret: &str, password: Option<&str>) -> Result { let mut format = None; let secret = { let mut started = false; @@ -86,7 +82,7 @@ pub fn decode_secret_key(secret: &str, password: Option<&str>) -> Result decode_openssh(&secret, password), - Some(Format::Rsa) => Ok(decode_rsa_pkcs1_der(&secret)?.into()), + Some(Format::Rsa) => decode_rsa(&secret), Some(Format::Pkcs5Encrypted(enc)) => decode_pkcs5(&secret, password, enc), Some(Format::Pkcs8Encrypted) | Some(Format::Pkcs8) => { let result = self::pkcs8::decode_pkcs8(&secret, password.map(|x| x.as_bytes())); @@ -106,7 +102,7 @@ pub fn decode_secret_key(secret: &str, password: Option<&str>) -> Result(key: &PrivateKey, mut w: W) -> Result<(), Error> { +pub fn encode_pkcs8_pem(key: &key::KeyPair, mut w: W) -> Result<(), Error> { let x = self::pkcs8::encode_pkcs8(key)?; w.write_all(b"-----BEGIN PRIVATE KEY-----\n")?; w.write_all(BASE64_MIME.encode(&x).as_bytes())?; @@ -115,7 +111,7 @@ pub fn encode_pkcs8_pem(key: &PrivateKey, mut w: W) -> Result<(), Erro } pub fn encode_pkcs8_pem_encrypted( - key: &PrivateKey, + key: &key::KeyPair, pass: &[u8], rounds: u32, mut w: W, @@ -127,6 +123,9 @@ pub fn encode_pkcs8_pem_encrypted( Ok(()) } -fn decode_rsa_pkcs1_der(secret: &[u8]) -> Result { - Ok(rsa::RsaPrivateKey::from_pkcs1_der(secret)?.try_into()?) +fn decode_rsa(secret: &[u8]) -> Result { + Ok(key::KeyPair::RSA { + key: crate::backend::RsaPrivate::new_from_der(secret)?, + hash: key::SignatureHash::SHA2_256, + }) } diff --git a/russh-keys/src/format/openssh.rs b/russh-keys/src/format/openssh.rs index 152ac347..d0f2fdc6 100644 --- a/russh-keys/src/format/openssh.rs +++ b/russh-keys/src/format/openssh.rs @@ -1,17 +1,121 @@ -use ssh_key::PrivateKey; +use std::convert::TryFrom; -use crate::Error; +use ssh_key::private::{ + EcdsaKeypair, Ed25519Keypair, KeypairData, PrivateKey, RsaKeypair, RsaPrivateKey, +}; +use ssh_key::public::{Ed25519PublicKey, KeyData, RsaPublicKey}; +use ssh_key::{Algorithm, HashAlg}; + +use crate::key::{KeyPair, PublicKey, SignatureHash}; +use crate::{ec, protocol, Error}; /// Decode a secret key given in the OpenSSH format, deciphering it if /// needed using the supplied password. -pub fn decode_openssh(secret: &[u8], password: Option<&str>) -> Result { +pub fn decode_openssh(secret: &[u8], password: Option<&str>) -> Result { let pk = PrivateKey::from_bytes(secret)?; - if pk.is_encrypted() { - if let Some(password) = password { - return Ok(pk.decrypt(password)?); - } else { - return Err(Error::KeyIsEncrypted); + KeyPair::try_from(&match password { + Some(password) => pk.decrypt(password)?, + None => pk, + }) +} + +impl TryFrom<&PrivateKey> for KeyPair { + type Error = Error; + + fn try_from(pk: &PrivateKey) -> Result { + match pk.key_data() { + KeypairData::Ed25519(Ed25519Keypair { public, private }) => { + let key = ed25519_dalek::SigningKey::from(private.as_ref()); + let public_key = ed25519_dalek::VerifyingKey::from_bytes(public.as_ref())?; + if public_key != key.verifying_key() { + return Err(Error::KeyIsCorrupt); + } + Ok(KeyPair::Ed25519(key)) + } + KeypairData::Rsa(keypair) => { + KeyPair::new_rsa_with_hash(&keypair.into(), None, SignatureHash::SHA2_512) + } + KeypairData::Ecdsa(keypair) => { + let key_type = match keypair { + EcdsaKeypair::NistP256 { .. } => crate::KEYTYPE_ECDSA_SHA2_NISTP256, + EcdsaKeypair::NistP384 { .. } => crate::KEYTYPE_ECDSA_SHA2_NISTP384, + EcdsaKeypair::NistP521 { .. } => crate::KEYTYPE_ECDSA_SHA2_NISTP521, + }; + let key = + ec::PrivateKey::new_from_secret_scalar(key_type, keypair.private_key_bytes())?; + let public_key = + ec::PublicKey::from_sec1_bytes(key_type, keypair.public_key_bytes())?; + if public_key != key.to_public_key() { + return Err(Error::KeyIsCorrupt); + } + Ok(KeyPair::EC { key }) + } + KeypairData::Encrypted(_) => Err(Error::KeyIsEncrypted), + _ => Err(Error::UnsupportedKeyType { + key_type_string: pk.algorithm().as_str().into(), + key_type_raw: pk.algorithm().as_str().as_bytes().into(), + }), + } + } +} + +impl<'a> From<&'a RsaKeypair> for protocol::RsaPrivateKey<'a> { + fn from(key: &'a RsaKeypair) -> Self { + let RsaPublicKey { e, n } = &key.public; + let RsaPrivateKey { d, iqmp, p, q } = &key.private; + Self { + public_key: protocol::RsaPublicKey { + public_exponent: e.as_bytes().into(), + modulus: n.as_bytes().into(), + }, + private_exponent: d.as_bytes().into(), + prime1: p.as_bytes().into(), + prime2: q.as_bytes().into(), + coefficient: iqmp.as_bytes().into(), + comment: b"".as_slice().into(), + } + } +} + +impl TryFrom<&KeyData> for PublicKey { + type Error = Error; + + fn try_from(key_data: &KeyData) -> Result { + match key_data { + KeyData::Ed25519(Ed25519PublicKey(public)) => Ok(PublicKey::Ed25519( + ed25519_dalek::VerifyingKey::from_bytes(public)?, + )), + KeyData::Rsa(ref public) => PublicKey::new_rsa_with_hash( + &public.into(), + match key_data.algorithm() { + Algorithm::Rsa { hash } => match hash { + Some(HashAlg::Sha256) => SignatureHash::SHA2_256, + Some(HashAlg::Sha512) => SignatureHash::SHA2_512, + _ => SignatureHash::SHA1, + }, + _ => return Err(Error::KeyIsCorrupt), + }, + ), + KeyData::Ecdsa(public) => Ok(PublicKey::EC { + key: ec::PublicKey::from_sec1_bytes( + key_data.algorithm().as_str().as_bytes(), + public.as_sec1_bytes(), + )?, + }), + _ => Err(Error::UnsupportedKeyType { + key_type_string: key_data.algorithm().as_str().into(), + key_type_raw: key_data.algorithm().as_str().as_bytes().into(), + }), + } + } +} + +impl<'a> From<&'a RsaPublicKey> for protocol::RsaPublicKey<'a> { + fn from(key: &'a RsaPublicKey) -> Self { + let RsaPublicKey { e, n } = key; + Self { + public_exponent: e.as_bytes().into(), + modulus: n.as_bytes().into(), } } - Ok(pk) } diff --git a/russh-keys/src/format/pkcs5.rs b/russh-keys/src/format/pkcs5.rs index f4851226..b1b4c266 100644 --- a/russh-keys/src/format/pkcs5.rs +++ b/russh-keys/src/format/pkcs5.rs @@ -1,8 +1,7 @@ use aes::*; -use ssh_key::PrivateKey; use super::Encryption; -use crate::Error; +use crate::{key, Error}; /// Decode a secret key in the PKCS#5 format, possibly deciphering it /// using the supplied password. @@ -10,7 +9,7 @@ pub fn decode_pkcs5( secret: &[u8], password: Option<&str>, enc: Encryption, -) -> Result { +) -> Result { use aes::cipher::{BlockDecryptMut, KeyIvInit}; use block_padding::Pkcs7; @@ -29,7 +28,7 @@ pub fn decode_pkcs5( } Encryption::Aes256Cbc(_) => unimplemented!(), }; - super::decode_rsa_pkcs1_der(&sec).map(Into::into) + super::decode_rsa(&sec) } else { Err(Error::KeyIsEncrypted) } diff --git a/russh-keys/src/format/pkcs8.rs b/russh-keys/src/format/pkcs8.rs index e1b83b85..e39d67b8 100644 --- a/russh-keys/src/format/pkcs8.rs +++ b/russh-keys/src/format/pkcs8.rs @@ -1,19 +1,12 @@ use std::convert::{TryFrom, TryInto}; -use p256::NistP256; -use p384::NistP384; -use p521::NistP521; -use pkcs8::{AssociatedOid, EncodePrivateKey, PrivateKeyInfo, SecretDocument}; -use ssh_key::private::{EcdsaKeypair, Ed25519Keypair, Ed25519PrivateKey, KeypairData}; -use ssh_key::PrivateKey; +use pkcs8::{EncodePrivateKey, PrivateKeyInfo, SecretDocument}; -use crate::Error; +use crate::key::SignatureHash; +use crate::{ec, key, protocol, Error}; /// Decode a PKCS#8-encoded private key. -pub fn decode_pkcs8( - ciphertext: &[u8], - password: Option<&[u8]>, -) -> Result { +pub fn decode_pkcs8(ciphertext: &[u8], password: Option<&[u8]>) -> Result { let doc = SecretDocument::try_from(ciphertext)?; let doc = if let Some(password) = password { doc.decode_msg::()? @@ -21,66 +14,133 @@ pub fn decode_pkcs8( } else { doc }; - Ok(pkcs8_pki_into_keypair_data(doc.decode_msg::()?)?.try_into()?) + key::KeyPair::try_from(doc.decode_msg::()?) } -fn pkcs8_pki_into_keypair_data(pki: PrivateKeyInfo<'_>) -> Result { - match pki.algorithm.oid { - ed25519_dalek::pkcs8::ALGORITHM_OID => { - let kpb = ed25519_dalek::pkcs8::KeypairBytes::try_from(pki)?; - let pk = Ed25519PrivateKey::from_bytes(&kpb.secret_key); - Ok(KeypairData::Ed25519(Ed25519Keypair { - public: pk.clone().into(), - private: pk, - })) +impl<'a> TryFrom> for key::KeyPair { + type Error = Error; + + fn try_from(pki: PrivateKeyInfo<'a>) -> Result { + match pki.algorithm.oid { + ed25519_dalek::pkcs8::ALGORITHM_OID => Ok(key::KeyPair::Ed25519( + ed25519_dalek::pkcs8::KeypairBytes::try_from(pki)? + .secret_key + .into(), + )), + pkcs1::ALGORITHM_OID => { + let sk = &pkcs1::RsaPrivateKey::try_from(pki.private_key)?; + key::KeyPair::new_rsa_with_hash( + &sk.into(), + Some(&sk.into()), + SignatureHash::SHA2_256, + ) + } + sec1::ALGORITHM_OID => Ok(key::KeyPair::EC { + key: pki.try_into()?, + }), + oid => Err(Error::UnknownAlgorithm(oid)), + } + } +} + +impl<'a> From<&pkcs1::RsaPrivateKey<'a>> for protocol::RsaPrivateKey<'a> { + fn from(sk: &pkcs1::RsaPrivateKey<'a>) -> Self { + Self { + public_key: protocol::RsaPublicKey { + public_exponent: sk.public_exponent.as_bytes().into(), + modulus: sk.modulus.as_bytes().into(), + }, + private_exponent: sk.private_exponent.as_bytes().into(), + prime1: sk.prime1.as_bytes().into(), + prime2: sk.prime2.as_bytes().into(), + coefficient: sk.coefficient.as_bytes().into(), + comment: b"".as_slice().into(), + } + } +} + +impl<'a> From<&pkcs1::RsaPrivateKey<'a>> for key::RsaCrtExtra<'a> { + fn from(sk: &pkcs1::RsaPrivateKey<'a>) -> Self { + Self { + dp: sk.exponent1.as_bytes().into(), + dq: sk.exponent2.as_bytes().into(), } - pkcs1::ALGORITHM_OID => { - let sk = &pkcs1::RsaPrivateKey::try_from(pki.private_key)?; - let pk = rsa::RsaPrivateKey::from_components( - rsa::BigUint::from_bytes_be(sk.modulus.as_bytes()), - rsa::BigUint::from_bytes_be(sk.public_exponent.as_bytes()), - rsa::BigUint::from_bytes_be(sk.private_exponent.as_bytes()), - vec![ - rsa::BigUint::from_bytes_be(sk.prime1.as_bytes()), - rsa::BigUint::from_bytes_be(sk.prime2.as_bytes()), - ], - )?; - Ok(KeypairData::Rsa(pk.try_into()?)) + } +} + +// Note: It's infeasible to implement `EncodePrivateKey` because that is bound to `pkcs8::Result`. +impl TryFrom<&key::RsaPrivate> for SecretDocument { + type Error = Error; + + fn try_from(key: &key::RsaPrivate) -> Result { + use der::Encode; + use pkcs1::UintRef; + + let sk = protocol::RsaPrivateKey::try_from(key)?; + let extra = key::RsaCrtExtra::try_from(key)?; + + let rsa_private_key = pkcs1::RsaPrivateKey { + modulus: UintRef::new(&sk.public_key.modulus)?, + public_exponent: UintRef::new(&sk.public_key.public_exponent)?, + private_exponent: UintRef::new(&sk.private_exponent)?, + prime1: UintRef::new(&sk.prime1)?, + prime2: UintRef::new(&sk.prime2)?, + exponent1: UintRef::new(&extra.dp)?, + exponent2: UintRef::new(&extra.dq)?, + coefficient: UintRef::new(&sk.coefficient)?, + other_prime_infos: None, + }; + let pki = PrivateKeyInfo { + algorithm: spki::AlgorithmIdentifier { + oid: pkcs1::ALGORITHM_OID, + parameters: Some(der::asn1::Null.into()), + }, + private_key: &rsa_private_key.to_der()?, + public_key: None, + }; + Ok(Self::try_from(pki)?) + } +} + +impl TryFrom> for ec::PrivateKey { + type Error = Error; + + fn try_from(pki: PrivateKeyInfo<'_>) -> Result { + use pkcs8::AssociatedOid; + match pki.algorithm.parameters_oid()? { + p256::NistP256::OID => Ok(ec::PrivateKey::P256(pki.try_into()?)), + p384::NistP384::OID => Ok(ec::PrivateKey::P384(pki.try_into()?)), + p521::NistP521::OID => Ok(ec::PrivateKey::P521(pki.try_into()?)), + oid => Err(Error::UnknownAlgorithm(oid)), } - sec1::ALGORITHM_OID => { - let sk = &sec1::EcPrivateKey::try_from(pki.private_key)?; - dbg!(sk.private_key); - dbg!(sk.public_key); - dbg!(sk.parameters); - dbg!(sk.parameters.and_then(|x| x.named_curve())); - Ok(KeypairData::Ecdsa( - match pki.algorithm.parameters_oid()? { - NistP256::OID => { - let sk = p256::SecretKey::try_from(pki)?; - EcdsaKeypair::NistP256 { - public: sk.public_key().into(), - private: sk.into(), - } - } - NistP384::OID => { - let sk = p384::SecretKey::try_from(pki)?; - EcdsaKeypair::NistP384 { - public: sk.public_key().into(), - private: sk.into(), - } - } - NistP521::OID => { - let sk = p521::SecretKey::try_from(pki)?; - EcdsaKeypair::NistP521 { - public: sk.public_key().into(), - private: sk.into(), - } - } - oid => return Err(Error::UnknownAlgorithm(oid)), - }, - )) + } +} + +impl EncodePrivateKey for ec::PrivateKey { + fn to_pkcs8_der(&self) -> pkcs8::Result { + match self { + ec::PrivateKey::P256(key) => key.to_pkcs8_der(), + ec::PrivateKey::P384(key) => key.to_pkcs8_der(), + ec::PrivateKey::P521(key) => key.to_pkcs8_der(), } - oid => Err(Error::UnknownAlgorithm(oid)), + } +} + +#[test] +fn test_read_write_pkcs8() { + let secret = ed25519_dalek::SigningKey::generate(&mut key::safe_rng()); + assert_eq!( + secret.verifying_key().as_bytes(), + ed25519_dalek::VerifyingKey::from(&secret).as_bytes() + ); + let key = key::KeyPair::Ed25519(secret); + let password = b"blabla"; + let ciphertext = encode_pkcs8_encrypted(password, 100, &key).unwrap(); + let key = decode_pkcs8(&ciphertext, Some(password)).unwrap(); + match key { + key::KeyPair::Ed25519 { .. } => println!("Ed25519"), + key::KeyPair::EC { .. } => println!("EC"), + key::KeyPair::RSA { .. } => println!("RSA"), } } @@ -88,7 +148,7 @@ fn pkcs8_pki_into_keypair_data(pki: PrivateKeyInfo<'_>) -> Result Result, Error> { let pvi_bytes = encode_pkcs8(key)?; let pvi = PrivateKeyInfo::try_from(pvi_bytes.as_slice())?; @@ -109,47 +169,11 @@ pub fn encode_pkcs8_encrypted( } /// Encode into a PKCS#8-encoded private key. -pub fn encode_pkcs8(key: &ssh_key::PrivateKey) -> Result, Error> { - let v = match key.key_data() { - ssh_key::private::KeypairData::Ed25519(ref pair) => { - let sk: ed25519_dalek::SigningKey = pair.try_into()?; - sk.to_pkcs8_der()? - } - ssh_key::private::KeypairData::Rsa(ref pair) => { - // TODO: Implementation in ssh-key 0.6.7 is broken (fixed in 0.7.0-pre) - let sk = rsa::RsaPrivateKey::from_components( - rsa::BigUint::try_from(&pair.public.n)?, - rsa::BigUint::try_from(&pair.public.e)?, - rsa::BigUint::try_from(&pair.private.d)?, - vec![ - rsa::BigUint::try_from(&pair.private.p)?, - rsa::BigUint::try_from(&pair.private.q)?, - ], - )?; - sk.to_pkcs8_der()? - } - ssh_key::private::KeypairData::Ecdsa(ref pair) => match pair { - EcdsaKeypair::NistP256 { private, .. } => { - let sk = p256::SecretKey::from_bytes(private.as_slice().into())?; - sk.to_pkcs8_der()? - } - EcdsaKeypair::NistP384 { private, .. } => { - let sk = p384::SecretKey::from_bytes(private.as_slice().into())?; - sk.to_pkcs8_der()? - } - EcdsaKeypair::NistP521 { private, .. } => { - let sk = p521::SecretKey::from_bytes(private.as_slice().into())?; - sk.to_pkcs8_der()? - } - }, - _ => { - let algo = key.algorithm(); - let kt = algo.as_str(); - return Err(Error::UnsupportedKeyType { - key_type_string: kt.into(), - key_type_raw: kt.as_bytes().into(), - }); - } +pub fn encode_pkcs8(key: &key::KeyPair) -> Result, Error> { + let v = match *key { + key::KeyPair::Ed25519(ref pair) => pair.to_pkcs8_der()?, + key::KeyPair::RSA { ref key, .. } => SecretDocument::try_from(key)?, + key::KeyPair::EC { ref key, .. } => key.to_pkcs8_der()?, } .as_bytes() .to_vec(); diff --git a/russh-keys/src/format/pkcs8_legacy.rs b/russh-keys/src/format/pkcs8_legacy.rs index 064c2dc2..ad9dc7f0 100644 --- a/russh-keys/src/format/pkcs8_legacy.rs +++ b/russh-keys/src/format/pkcs8_legacy.rs @@ -4,12 +4,10 @@ use std::convert::TryFrom; use aes::cipher::{BlockDecryptMut, KeyIvInit}; use aes::*; use block_padding::Pkcs7; -use ssh_key::private::{Ed25519Keypair, Ed25519PrivateKey, KeypairData}; -use ssh_key::PrivateKey; use yasna::BERReaderSeq; use super::Encryption; -use crate::Error; +use crate::{key, Error}; const PBES2: &[u64] = &[1, 2, 840, 113549, 1, 5, 13]; const ED25519: &[u64] = &[1, 3, 101, 112]; @@ -17,7 +15,7 @@ const PBKDF2: &[u64] = &[1, 2, 840, 113549, 1, 5, 12]; const AES256CBC: &[u64] = &[2, 16, 840, 1, 101, 3, 4, 1, 42]; const HMAC_SHA256: &[u64] = &[1, 2, 840, 113549, 2, 9]; -pub fn decode_pkcs8(ciphertext: &[u8], password: Option<&[u8]>) -> Result { +pub fn decode_pkcs8(ciphertext: &[u8], password: Option<&[u8]>) -> Result { let secret = if let Some(pass) = password { Cow::Owned(yasna::parse_der(ciphertext, |reader| { reader.read_sequence(|reader| { @@ -52,7 +50,7 @@ pub fn decode_pkcs8(ciphertext: &[u8], password: Option<&[u8]>) -> Result Result { +fn read_key_v1(reader: &mut BERReaderSeq) -> Result { let oid = reader .next() .read_sequence(|reader| reader.next().read_oid())?; @@ -69,15 +67,7 @@ fn read_key_v1(reader: &mut BERReaderSeq) -> Result { reader .next() .read_tagged(yasna::Tag::context(1), |reader| reader.read_bitvec())?; - - let pk = Ed25519PrivateKey::from(&secret); - Ok(PrivateKey::new( - KeypairData::Ed25519(Ed25519Keypair { - public: pk.clone().into(), - private: pk, - }), - "", - )?) + Ok(key::KeyPair::Ed25519(secret)) } else { Err(Error::CouldNotReadKey) } diff --git a/russh-keys/src/helpers.rs b/russh-keys/src/helpers.rs deleted file mode 100644 index 3f4befba..00000000 --- a/russh-keys/src/helpers.rs +++ /dev/null @@ -1,13 +0,0 @@ -use ssh_encoding::Encode; - -pub(crate) trait EncodedExt { - fn encoded(&self) -> ssh_key::Result>; -} - -impl EncodedExt for E { - fn encoded(&self) -> ssh_key::Result> { - let mut buf = Vec::new(); - self.encode(&mut buf)?; - Ok(buf) - } -} diff --git a/russh-keys/src/key.rs b/russh-keys/src/key.rs index e584f399..1214841c 100644 --- a/russh-keys/src/key.rs +++ b/russh-keys/src/key.rs @@ -12,21 +12,78 @@ // See the License for the specific language governing permissions and // limitations under the License. // -use ssh_encoding::Decode; -use ssh_key::public::KeyData; -use ssh_key::{Algorithm, EcdsaCurve, HashAlg, PublicKey}; +use std::borrow::Cow; +use std::convert::{TryFrom, TryInto}; -use crate::encoding::Reader; -use crate::Error; +pub use backend::{RsaPrivate, RsaPublic}; +use ed25519_dalek::{Signer, Verifier}; +use rand_core::OsRng; +use russh_cryptovec::CryptoVec; +use serde::{Deserialize, Serialize}; -pub trait PublicKeyExt { - fn decode(bytes: &[u8]) -> Result; +use crate::encoding::{Encoding, Reader}; +pub use crate::signature::*; +use crate::{backend, ec, protocol, Error}; + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +/// Name of a public key algorithm. +pub struct Name(pub &'static str); + +impl AsRef for Name { + fn as_ref(&self) -> &str { + self.0 + } +} + +/// The name of the ecdsa-sha2-nistp256 algorithm for SSH. +pub const ECDSA_SHA2_NISTP256: Name = Name("ecdsa-sha2-nistp256"); +/// The name of the ecdsa-sha2-nistp384 algorithm for SSH. +pub const ECDSA_SHA2_NISTP384: Name = Name("ecdsa-sha2-nistp384"); +/// The name of the ecdsa-sha2-nistp521 algorithm for SSH. +pub const ECDSA_SHA2_NISTP521: Name = Name("ecdsa-sha2-nistp521"); +/// 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. +pub const RSA_SHA2_512: Name = Name("rsa-sha2-512"); +/// The name of the ssh-sha2-256 algorithm for SSH. +pub const RSA_SHA2_256: Name = Name("rsa-sha2-256"); + +pub const NONE: Name = Name("none"); + +pub const SSH_RSA: Name = Name("ssh-rsa"); + +pub static ALL_KEY_TYPES: &[&Name] = &[ + &NONE, + &ED25519, + &SSH_RSA, + &RSA_SHA2_256, + &RSA_SHA2_512, + &ECDSA_SHA2_NISTP256, + &ECDSA_SHA2_NISTP384, + &ECDSA_SHA2_NISTP521, +]; + +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 | ECDSA_SHA2_NISTP384 | ECDSA_SHA2_NISTP521 => "id_ecdsa", + ED25519 => "id_ed25519", + RSA_SHA2_512 => "id_rsa", + RSA_SHA2_256 => "id_rsa", + _ => unreachable!(), + } + } } -impl PublicKeyExt for PublicKey { - fn decode(bytes: &[u8]) -> Result { - let key = KeyData::decode(&mut bytes.reader(0))?; - Ok(PublicKey::new(key, "")) +impl TryFrom<&str> for Name { + type Error = (); + fn try_from(s: &str) -> Result { + ALL_KEY_TYPES + .iter() + .find(|x| x.0 == s) + .map(|x| **x) + .ok_or(()) } } @@ -36,10 +93,374 @@ pub trait Verify { fn verify_server_auth(&self, buffer: &[u8], sig: &[u8]) -> bool; } +/// The hash function used for signing with RSA keys. +#[derive(Eq, PartialEq, Clone, Copy, Debug, Hash, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub enum SignatureHash { + /// SHA2, 256 bits. + SHA2_256, + /// SHA2, 512 bits. + SHA2_512, + /// SHA1 + SHA1, +} + +impl SignatureHash { + pub fn name(&self) -> Name { + match *self { + SignatureHash::SHA2_256 => RSA_SHA2_256, + SignatureHash::SHA2_512 => RSA_SHA2_512, + SignatureHash::SHA1 => SSH_RSA, + } + } + + pub fn from_rsa_hostkey_algo(algo: &[u8]) -> Option { + match algo { + b"rsa-sha2-256" => Some(Self::SHA2_256), + b"rsa-sha2-512" => Some(Self::SHA2_512), + b"ssh-rsa" => Some(Self::SHA1), + _ => None, + } + } +} + +/// Public key +#[derive(Eq, Debug, Clone)] +pub enum PublicKey { + #[doc(hidden)] + Ed25519(ed25519_dalek::VerifyingKey), + #[doc(hidden)] + RSA { + key: backend::RsaPublic, + hash: SignatureHash, + }, + #[doc(hidden)] + EC { key: ec::PublicKey }, +} + +impl PartialEq for PublicKey { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::RSA { key: a, .. }, Self::RSA { key: b, .. }) => a == b, + (Self::Ed25519(a), Self::Ed25519(b)) => a == b, + (Self::EC { key: a }, Self::EC { key: b }) => a == b, + _ => false, + } + } +} + +impl PublicKey { + /// Parse a public key in SSH format. + pub fn parse(algo: &[u8], pubkey: &[u8]) -> Result { + use ssh_encoding::Decode; + let key_data = &ssh_key::public::KeyData::decode(&mut pubkey.reader(0))?; + let key_algo = key_data.algorithm(); + let key_algo = key_algo.as_str().as_bytes(); + if key_algo == b"ssh-rsa" { + if algo != SSH_RSA.as_ref().as_bytes() + && algo != RSA_SHA2_256.as_ref().as_bytes() + && algo != RSA_SHA2_512.as_ref().as_bytes() + { + return Err(Error::KeyIsCorrupt); + } + } else if key_algo != algo { + return Err(Error::KeyIsCorrupt); + } + Self::try_from(key_data) + } + + pub fn new_rsa_with_hash( + pk: &protocol::RsaPublicKey<'_>, + hash: SignatureHash, + ) -> Result { + Ok(PublicKey::RSA { + key: RsaPublic::try_from(pk)?, + hash, + }) + } + + /// Algorithm name for that key. + pub fn name(&self) -> &'static str { + match *self { + PublicKey::Ed25519(_) => ED25519.0, + PublicKey::RSA { ref hash, .. } => hash.name().0, + PublicKey::EC { ref key } => key.algorithm(), + } + } + + /// Verify a signature. + pub fn verify_detached(&self, buffer: &[u8], sig: &[u8]) -> bool { + match self { + PublicKey::Ed25519(ref public) => { + let Ok(sig) = ed25519_dalek::ed25519::SignatureBytes::try_from(sig) else { + return false; + }; + let sig = ed25519_dalek::Signature::from_bytes(&sig); + public.verify(buffer, &sig).is_ok() + } + PublicKey::RSA { ref key, ref hash } => key.verify_detached(hash, buffer, sig), + PublicKey::EC { ref key, .. } => ec_verify(key, buffer, sig).is_ok(), + } + } + + /// Compute the key fingerprint, hashed with sha2-256. + pub fn fingerprint(&self) -> String { + use super::PublicKeyBase64; + let key = self.public_key_bytes(); + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(&key[..]); + data_encoding::BASE64_NOPAD.encode(&hasher.finalize()) + } + + pub fn set_algorithm(&mut self, algorithm: SignatureHash) { + if let PublicKey::RSA { ref mut hash, .. } = self { + *hash = algorithm; + } + } +} + +impl Verify for PublicKey { + fn verify_client_auth(&self, buffer: &[u8], sig: &[u8]) -> bool { + self.verify_detached(buffer, sig) + } + fn verify_server_auth(&self, buffer: &[u8], sig: &[u8]) -> bool { + self.verify_detached(buffer, sig) + } +} + +/// Public key exchange algorithms. +#[allow(clippy::large_enum_variant)] +pub enum KeyPair { + Ed25519(ed25519_dalek::SigningKey), + RSA { + key: backend::RsaPrivate, + hash: SignatureHash, + }, + EC { + key: ec::PrivateKey, + }, +} + +impl Clone for KeyPair { + fn clone(&self) -> Self { + match self { + #[allow(clippy::expect_used)] + Self::Ed25519(kp) => { + Self::Ed25519(ed25519_dalek::SigningKey::from_bytes(&kp.to_bytes())) + } + Self::RSA { key, hash } => Self::RSA { + key: key.clone(), + hash: *hash, + }, + Self::EC { key } => Self::EC { key: key.clone() }, + } + } +} + +impl std::fmt::Debug for KeyPair { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + KeyPair::Ed25519(ref key) => write!( + f, + "Ed25519 {{ public: {:?}, secret: (hidden) }}", + key.verifying_key().as_bytes() + ), + KeyPair::RSA { .. } => write!(f, "RSA {{ (hidden) }}"), + KeyPair::EC { .. } => write!(f, "EC {{ (hidden) }}"), + } + } +} + +impl<'b> crate::encoding::Bytes for &'b KeyPair { + fn bytes(&self) -> &[u8] { + self.name().as_bytes() + } +} + +impl KeyPair { + pub fn new_rsa_with_hash( + sk: &protocol::RsaPrivateKey<'_>, + extra: Option<&RsaCrtExtra<'_>>, + hash: SignatureHash, + ) -> Result { + Ok(KeyPair::RSA { + key: RsaPrivate::new(sk, extra)?, + hash, + }) + } + + /// Copy the public key of this algorithm. + pub fn clone_public_key(&self) -> Result { + Ok(match self { + KeyPair::Ed25519(ref key) => PublicKey::Ed25519(key.verifying_key()), + KeyPair::RSA { ref key, ref hash } => PublicKey::RSA { + key: key.try_into()?, + hash: *hash, + }, + KeyPair::EC { ref key } => PublicKey::EC { + key: key.to_public_key(), + }, + }) + } + + /// Name of this key algorithm. + pub fn name(&self) -> &'static str { + match *self { + KeyPair::Ed25519(_) => ED25519.0, + KeyPair::RSA { ref hash, .. } => hash.name().0, + KeyPair::EC { ref key } => key.algorithm(), + } + } + + /// Generate a ED25519 key pair. + pub fn generate_ed25519() -> Self { + let keypair = ed25519_dalek::SigningKey::generate(&mut OsRng {}); + assert_eq!( + keypair.verifying_key().as_bytes(), + ed25519_dalek::VerifyingKey::from(&keypair).as_bytes() + ); + KeyPair::Ed25519(keypair) + } + + /// Generate a RSA key pair. + pub fn generate_rsa(bits: usize, hash: SignatureHash) -> Option { + let key = RsaPrivate::generate(bits).ok()?; + Some(KeyPair::RSA { key, hash }) + } + + /// 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::RSA { ref key, ref hash } => Ok(Signature::RSA { + bytes: key.sign(hash, to_sign)?, + hash: *hash, + }), + KeyPair::EC { ref key } => Ok(Signature::ECDSA { + algorithm: key.algorithm(), + signature: ec_signature(key, to_sign)?, + }), + } + } + + #[doc(hidden)] + /// This is used by the server to sign the initial DH kex + /// message. Note: we are not signing the same kind of thing as in + /// the function below, `add_self_signature`. + pub fn add_signature>( + &self, + buffer: &mut CryptoVec, + to_sign: H, + ) -> Result<(), Error> { + match self { + KeyPair::Ed25519(ref secret) => { + let signature = secret.sign(to_sign.as_ref()); + + buffer.push_u32_be((ED25519.0.len() + signature.to_bytes().len() + 8) as u32); + buffer.extend_ssh_string(ED25519.0.as_bytes()); + buffer.extend_ssh_string(signature.to_bytes().as_slice()); + } + KeyPair::RSA { ref key, ref hash } => { + // https://tools.ietf.org/html/draft-rsa-dsa-sha2-256-02#section-2.2 + let signature = key.sign(hash, to_sign.as_ref())?; + let name = hash.name(); + buffer.push_u32_be((name.0.len() + signature.len() + 8) as u32); + buffer.extend_ssh_string(name.0.as_bytes()); + buffer.extend_ssh_string(&signature); + } + KeyPair::EC { ref key } => { + let algorithm = key.algorithm().as_bytes(); + let signature = ec_signature(key, to_sign.as_ref())?; + buffer.push_u32_be((algorithm.len() + signature.len() + 8) as u32); + buffer.extend_ssh_string(algorithm); + buffer.extend_ssh_string(&signature); + } + } + Ok(()) + } + + #[doc(hidden)] + /// This is used by the client for authentication. Note: we are + /// not signing the same kind of thing as in the above function, + /// `add_signature`. + pub fn add_self_signature(&self, buffer: &mut CryptoVec) -> Result<(), Error> { + match self { + KeyPair::Ed25519(ref secret) => { + let signature = secret.sign(buffer); + buffer.push_u32_be((ED25519.0.len() + signature.to_bytes().len() + 8) as u32); + buffer.extend_ssh_string(ED25519.0.as_bytes()); + buffer.extend_ssh_string(signature.to_bytes().as_slice()); + } + KeyPair::RSA { ref key, ref hash } => { + // https://tools.ietf.org/html/draft-rsa-dsa-sha2-256-02#section-2.2 + let signature = key.sign(hash, buffer)?; + let name = hash.name(); + buffer.push_u32_be((name.0.len() + signature.len() + 8) as u32); + buffer.extend_ssh_string(name.0.as_bytes()); + buffer.extend_ssh_string(&signature); + } + KeyPair::EC { ref key } => { + let signature = ec_signature(key, buffer)?; + let algorithm = key.algorithm().as_bytes(); + buffer.push_u32_be((algorithm.len() + signature.len() + 8) as u32); + buffer.extend_ssh_string(algorithm); + buffer.extend_ssh_string(&signature); + } + } + Ok(()) + } + + /// Create a copy of an RSA key with a specified hash algorithm. + pub fn with_signature_hash(&self, hash: SignatureHash) -> Option { + match self { + KeyPair::Ed25519(_) => None, + KeyPair::RSA { key, .. } => Some(KeyPair::RSA { + key: key.clone(), + hash, + }), + KeyPair::EC { .. } => None, + } + } +} + +/// Extra CRT parameters for RSA private key. +pub struct RsaCrtExtra<'a> { + /// `d mod (p-1)`. + pub dp: Cow<'a, [u8]>, + /// `d mod (q-1)`. + pub dq: Cow<'a, [u8]>, +} + +impl Drop for RsaCrtExtra<'_> { + fn drop(&mut self) { + zeroize_cow(&mut self.dp); + zeroize_cow(&mut self.dq); + } +} + +fn ec_signature(key: &ec::PrivateKey, b: &[u8]) -> Result, Error> { + let (r, s) = key.try_sign(b)?; + let mut buf = Vec::new(); + buf.extend_ssh_mpint(&r); + buf.extend_ssh_mpint(&s); + Ok(buf) +} + +fn ec_verify(key: &ec::PublicKey, b: &[u8], sig: &[u8]) -> Result<(), Error> { + let mut reader = sig.reader(0); + key.verify(b, reader.read_mpint()?, reader.read_mpint()?) +} + /// Parse a public key from a byte slice. -pub fn parse_public_key(p: &[u8]) -> Result { +pub fn parse_public_key(p: &[u8], prefer_hash: Option) -> Result { use ssh_encoding::Decode; - Ok(ssh_key::public::KeyData::decode(&mut p.reader(0))?.into()) + let mut key = PublicKey::try_from(&ssh_key::public::KeyData::decode(&mut p.reader(0))?)?; + key.set_algorithm(prefer_hash.unwrap_or(SignatureHash::SHA2_256)); + Ok(key) } /// Obtain a cryptographic-safe random number generator. @@ -47,25 +468,15 @@ pub fn safe_rng() -> impl rand::CryptoRng + rand::RngCore { rand::thread_rng() } -pub const ALL_KEY_TYPES: &[Algorithm] = &[ - Algorithm::Dsa, - Algorithm::Ecdsa { - curve: EcdsaCurve::NistP256, - }, - Algorithm::Ecdsa { - curve: EcdsaCurve::NistP384, - }, - Algorithm::Ecdsa { - curve: EcdsaCurve::NistP521, - }, - Algorithm::Ed25519, - Algorithm::Rsa { hash: None }, - Algorithm::Rsa { - hash: Some(HashAlg::Sha256), - }, - Algorithm::Rsa { - hash: Some(HashAlg::Sha512), - }, - Algorithm::SkEcdsaSha2NistP256, - Algorithm::SkEd25519, -]; +/// Zeroize `Cow` if value is owned. +pub(crate) fn zeroize_cow(v: &mut Cow) +where + T: ToOwned + ?Sized, + ::Owned: zeroize::Zeroize, +{ + use zeroize::Zeroize; + match v { + Cow::Owned(v) => v.zeroize(), + Cow::Borrowed(_) => (), + } +} diff --git a/russh-keys/src/known_hosts.rs b/russh-keys/src/known_hosts.rs index 7aad1987..2355e970 100644 --- a/russh-keys/src/known_hosts.rs +++ b/russh-keys/src/known_hosts.rs @@ -8,14 +8,10 @@ use hmac::{Hmac, Mac}; use log::debug; use sha1::Sha1; -use crate::Error; +use crate::{key, Error, PublicKeyBase64}; /// Check whether the host is known, from its standard location. -pub fn check_known_hosts( - host: &str, - port: u16, - pubkey: &ssh_key::PublicKey, -) -> Result { +pub fn check_known_hosts(host: &str, port: u16, pubkey: &key::PublicKey) -> Result { check_known_hosts_path(host, port, pubkey, known_hosts_path()?) } @@ -23,21 +19,18 @@ pub fn check_known_hosts( pub fn check_known_hosts_path>( host: &str, port: u16, - pubkey: &ssh_key::PublicKey, + pubkey: &key::PublicKey, path: P, ) -> Result { let check = known_host_keys_path(host, port, path)? .into_iter() - .map(|(line, recorded)| { - match ( - pubkey.algorithm() == recorded.algorithm(), - *pubkey == recorded, - ) { + .map( + |(line, recorded)| match (pubkey.name() == recorded.name(), *pubkey == recorded) { (true, true) => Ok(true), (true, false) => Err(Error::KeyChanged { line }), _ => Ok(false), - } - }) + }, + ) // If any Err was returned, we stop here .collect::, Error>>()? .into_iter() @@ -66,7 +59,7 @@ fn known_hosts_path() -> Result { } /// Get the server key that matches the one recorded in the user's known_hosts file. -pub fn known_host_keys(host: &str, port: u16) -> Result, Error> { +pub fn known_host_keys(host: &str, port: u16) -> Result, Error> { known_host_keys_path(host, port, known_hosts_path()?) } @@ -75,7 +68,7 @@ pub fn known_host_keys_path>( host: &str, port: u16, path: P, -) -> Result, Error> { +) -> Result, Error> { use crate::parse_public_key_base64; let mut f = if let Ok(f) = File::open(path) { @@ -140,7 +133,7 @@ fn match_hostname(host: &str, pattern: &str) -> bool { } /// Record a host's public key into the user's known_hosts file. -pub fn learn_known_hosts(host: &str, port: u16, pubkey: &ssh_key::PublicKey) -> Result<(), Error> { +pub fn learn_known_hosts(host: &str, port: u16, pubkey: &key::PublicKey) -> Result<(), Error> { learn_known_hosts_path(host, port, pubkey, known_hosts_path()?) } @@ -148,7 +141,7 @@ pub fn learn_known_hosts(host: &str, port: u16, pubkey: &ssh_key::PublicKey) -> pub fn learn_known_hosts_path>( host: &str, port: u16, - pubkey: &ssh_key::PublicKey, + pubkey: &key::PublicKey, path: P, ) -> Result<(), Error> { if let Some(parent) = path.as_ref().parent() { @@ -179,11 +172,21 @@ pub fn learn_known_hosts_path>( } else { write!(file, "{} ", host)? } - file.write_all(pubkey.to_openssh()?.as_bytes())?; + write_public_key_base64(&mut file, pubkey)?; file.write_all(b"\n")?; Ok(()) } +/// Write a public key onto the provided `Write`, encoded in base-64. +pub fn write_public_key_base64( + mut w: W, + publickey: &key::PublicKey, +) -> Result<(), Error> { + let pk = publickey.public_key_base64(); + writeln!(w, "{} {}", publickey.name(), pk)?; + Ok(()) +} + #[cfg(test)] mod test { use std::fs::File; diff --git a/russh-keys/src/lib.rs b/russh-keys/src/lib.rs index a006053c..9dd6cd6d 100644 --- a/russh-keys/src/lib.rs +++ b/russh-keys/src/lib.rs @@ -23,7 +23,7 @@ //! #[derive(Clone)] //! struct X{} //! impl agent::server::Agent for X { -//! fn confirm(self, _: std::sync::Arc) -> Box + Send + Unpin> { +//! fn confirm(self, _: std::sync::Arc) -> Box + Send + Unpin> { //! Box::new(futures::future::ready((self, true))) //! } //! } @@ -45,14 +45,14 @@ //! russh_keys::agent::server::serve(tokio_stream::wrappers::UnixListenerStream::new(listener), X {}).await //! }); //! let key = decode_secret_key(PKCS8_ENCRYPTED, Some("blabla")).unwrap(); -//! let public = key.public_key().clone(); +//! let public = key.clone_public_key().unwrap(); //! core.block_on(async move { //! let stream = tokio::net::UnixStream::connect(&agent_path).await?; //! let mut client = agent::client::AgentClient::connect(stream); //! client.add_identity(&key, &[agent::Constraint::KeyLifetime { seconds: 60 }]).await?; //! client.request_identities().await?; //! let buf = b"signed message"; -//! let sig = client.sign_request(&public, russh_cryptovec::CryptoVec::from_slice(&buf[..])).await.unwrap(); +//! let sig = client.sign_request(&public, russh_cryptovec::CryptoVec::from_slice(&buf[..])).await.1.unwrap(); //! // Here, `sig` is encoded in a format usable internally by the SSH protocol. //! Ok::<(), Error>(()) //! }).unwrap() @@ -66,25 +66,29 @@ use std::fs::File; use std::io::Read; use std::path::Path; -use std::string::FromUtf8Error; use aes::cipher::block_padding::UnpadError; use aes::cipher::inout::PadError; +use byteorder::{BigEndian, WriteBytesExt}; use data_encoding::BASE64_MIME; -use encoding::Encoding; -use helpers::EncodedExt; -use russh_cryptovec::CryptoVec; -use signature::Signer; -use ssh_key::Signature; +use ssh_key::Certificate; use thiserror::Error; +pub mod ec; pub mod encoding; pub mod key; +pub mod protocol; +pub mod signature; mod format; -mod helpers; pub use format::*; -pub use ssh_key::{self, Algorithm, Certificate, EcdsaCurve, HashAlg, PrivateKey, PublicKey}; + +#[cfg(feature = "openssl")] +#[path = "backend_openssl.rs"] +mod backend; +#[cfg(not(feature = "openssl"))] +#[path = "backend_rust.rs"] +mod backend; /// OpenSSH agent protocol implementation pub mod agent; @@ -145,6 +149,11 @@ pub enum Error { #[error(transparent)] IO(#[from] std::io::Error), + #[cfg(feature = "openssl")] + #[error(transparent)] + Openssl(#[from] openssl::error::ErrorStack), + + #[cfg(not(feature = "openssl"))] #[error("Rsa: {0}")] Rsa(#[from] rsa::Error), @@ -180,9 +189,6 @@ pub enum Error { )] BadAuthSock, - #[error(transparent)] - Utf8(#[from] FromUtf8Error), - #[error("ASN1 decoding error: {0}")] #[cfg(feature = "legacy-ed25519-pkcs8-parser")] LegacyASN1(::yasna::ASN1Error), @@ -199,12 +205,20 @@ impl From for Error { } } +const KEYTYPE_ECDSA_SHA2_NISTP256: &[u8] = ECDSA_SHA2_NISTP256.as_bytes(); +const KEYTYPE_ECDSA_SHA2_NISTP384: &[u8] = ECDSA_SHA2_NISTP384.as_bytes(); +const KEYTYPE_ECDSA_SHA2_NISTP521: &[u8] = ECDSA_SHA2_NISTP521.as_bytes(); + +const ECDSA_SHA2_NISTP256: &str = "ecdsa-sha2-nistp256"; +const ECDSA_SHA2_NISTP384: &str = "ecdsa-sha2-nistp384"; +const ECDSA_SHA2_NISTP521: &str = "ecdsa-sha2-nistp521"; + /// 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(); /// ``` -pub fn load_public_key>(path: P) -> Result { +pub fn load_public_key>(path: P) -> Result { let mut pubkey = String::new(); let mut file = File::open(path.as_ref())?; file.read_to_string(&mut pubkey)?; @@ -224,9 +238,9 @@ pub fn load_public_key>(path: P) -> Result Result { +pub fn parse_public_key_base64(key: &str) -> Result { let base = BASE64_MIME.decode(key.as_bytes())?; - key::parse_public_key(&base) + key::parse_public_key(&base, None) } pub trait PublicKeyBase64 { @@ -240,23 +254,78 @@ pub trait PublicKeyBase64 { } } -impl PublicKeyBase64 for ssh_key::PublicKey { +impl PublicKeyBase64 for key::PublicKey { fn public_key_bytes(&self) -> Vec { - self.key_data().encoded().unwrap_or_default() + let mut s = Vec::new(); + match *self { + key::PublicKey::Ed25519(ref publickey) => { + let name = b"ssh-ed25519"; + #[allow(clippy::unwrap_used)] // Vec<>.write can't fail + s.write_u32::(name.len() as u32).unwrap(); + s.extend_from_slice(name); + #[allow(clippy::unwrap_used)] // Vec<>.write can't fail + s.write_u32::(publickey.as_bytes().len() as u32) + .unwrap(); + s.extend_from_slice(publickey.as_bytes()); + } + key::PublicKey::RSA { ref key, .. } => { + use encoding::Encoding; + let name = b"ssh-rsa"; + #[allow(clippy::unwrap_used)] // Vec<>.write_all can't fail + s.write_u32::(name.len() as u32).unwrap(); + s.extend_from_slice(name); + s.extend_ssh(&protocol::RsaPublicKey::from(key)); + } + key::PublicKey::EC { ref key } => { + write_ec_public_key(&mut s, key); + } + } + s } } -impl PublicKeyBase64 for PrivateKey { +impl PublicKeyBase64 for key::KeyPair { fn public_key_bytes(&self) -> Vec { - self.public_key().public_key_bytes() + let name = self.name().as_bytes(); + let mut s = Vec::new(); + #[allow(clippy::unwrap_used)] // Vec<>.write_all can't fail + s.write_u32::(name.len() as u32).unwrap(); + s.extend_from_slice(name); + match *self { + key::KeyPair::Ed25519(ref key) => { + let public = key.verifying_key().to_bytes(); + #[allow(clippy::unwrap_used)] // Vec<>.write can't fail + s.write_u32::(public.len() as u32).unwrap(); + s.extend_from_slice(public.as_slice()); + } + key::KeyPair::RSA { ref key, .. } => { + use encoding::Encoding; + s.extend_ssh(&protocol::RsaPublicKey::from(key)); + } + key::KeyPair::EC { ref key } => { + write_ec_public_key(&mut s, &key.to_public_key()); + } + } + s } } +fn write_ec_public_key(buf: &mut Vec, key: &ec::PublicKey) { + let algorithm = key.algorithm().as_bytes(); + let ident = key.ident().as_bytes(); + let q = key.to_sec1_bytes(); + + use encoding::Encoding; + buf.extend_ssh_string(algorithm); + buf.extend_ssh_string(ident); + buf.extend_ssh_string(&q); +} + /// Load a secret key, deciphering it with the supplied password if necessary. pub fn load_secret_key>( secret_: P, password: Option<&str>, -) -> Result { +) -> Result { let mut secret_file = std::fs::File::open(secret_)?; let mut secret = String::new(); secret_file.read_to_string(&mut secret)?; @@ -281,35 +350,13 @@ fn is_base64_char(c: char) -> bool { || c == '=' } -#[doc(hidden)] -pub fn add_signature>( - signer: &S, - to_sign: &[u8], - output: &mut CryptoVec, -) -> Result<(), ssh_key::Error> { - let sig = signer.sign(to_sign); - output.extend_ssh_string(sig.encoded()?.as_slice()); - Ok(()) -} - -#[doc(hidden)] -pub fn add_self_signature>( - signer: &S, - buffer: &mut CryptoVec, -) -> Result<(), ssh_key::Error> { - let sig = signer.sign(buffer); - buffer.extend_ssh_string(sig.encoded()?.as_slice()); - Ok(()) -} - #[cfg(test)] mod test { - #[cfg(unix)] use futures::Future; + use log::debug; use super::*; - use crate::key::PublicKeyExt; const ED25519_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABDLGyfA39 @@ -378,12 +425,10 @@ MC4CAQAwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC #[test] fn test_decode_rfc8410_ed25519_private_only_key() { env_logger::try_init().unwrap_or(()); - assert!( - decode_secret_key(RFC8410_ED25519_PRIVATE_ONLY_KEY, None) - .unwrap() - .algorithm() - == ssh_key::Algorithm::Ed25519, - ); + assert!(matches!( + decode_secret_key(RFC8410_ED25519_PRIVATE_ONLY_KEY, None), + Ok(key::KeyPair::Ed25519 { .. }) + )); // We always encode public key, skip test_decode_encode_symmetry. } @@ -397,12 +442,10 @@ Z9w7lshQhqowtrbLDFw4rXAxZuE= #[test] fn test_decode_rfc8410_ed25519_private_public_key() { env_logger::try_init().unwrap_or(()); - assert!( - decode_secret_key(RFC8410_ED25519_PRIVATE_PUBLIC_KEY, None) - .unwrap() - .algorithm() - == ssh_key::Algorithm::Ed25519, - ); + assert!(matches!( + decode_secret_key(RFC8410_ED25519_PRIVATE_PUBLIC_KEY, None), + Ok(key::KeyPair::Ed25519 { .. }) + )); // We can't encode attributes, skip test_decode_encode_symmetry. } @@ -425,12 +468,12 @@ EAAAAgLAmXR6IlN0SdiD6o8qr+vUr0mXLbajs/m0UlegElOmoAAAANcm9iZXJ0QGJic2Rl dgECAw== -----END OPENSSH PRIVATE KEY----- "; - assert!( - decode_secret_key(key, None).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP256 - }, - ); + assert!(matches!( + decode_secret_key(key, None), + Ok(key::KeyPair::EC { + key: ec::PrivateKey::P256(_), + }) + )); } #[test] @@ -447,12 +490,12 @@ it35ecLB3uFUyQDk96h/ONck3Cu/ZdHcIR4R3oOtG8i+xfcThM69pKhnmFMK5e31Wi+4Xx Ylv0h4Wyzto8NfLQAAAA1yb2JlcnRAYmJzZGV2AQID -----END OPENSSH PRIVATE KEY----- "; - assert!( - decode_secret_key(key, None).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP384 - }, - ); + assert!(matches!( + decode_secret_key(key, None), + Ok(key::KeyPair::EC { + key: ec::PrivateKey::P384(_), + }) + )); } #[test] @@ -471,12 +514,12 @@ Ve0k2ddxoEsSE15H4lgNHM2iuYKzIqZJOReHRCTff6QGgMYPDqDfFfL1Hc1Ntql0pwAAAA 1yb2JlcnRAYmJzZGV2AQIDBAU= -----END OPENSSH PRIVATE KEY----- "; - assert!( - decode_secret_key(key, None).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP521 - }, - ); + assert!(matches!( + decode_secret_key(key, None), + Ok(key::KeyPair::EC { + key: ec::PrivateKey::P521(_), + }) + )); } #[test] @@ -486,8 +529,8 @@ Ve0k2ddxoEsSE15H4lgNHM2iuYKzIqZJOReHRCTff6QGgMYPDqDfFfL1Hc1Ntql0pwAAAA ) .unwrap(); assert_eq!( - format!("{}", key.fingerprint(ssh_key::HashAlg::Sha256)), - "SHA256:ldyiXa1JQakitNU5tErauu8DvWQ1dZ7aXu+rm7KQuog" + key.fingerprint(), + "ldyiXa1JQakitNU5tErauu8DvWQ1dZ7aXu+rm7KQuog" ); } @@ -496,12 +539,12 @@ Ve0k2ddxoEsSE15H4lgNHM2iuYKzIqZJOReHRCTff6QGgMYPDqDfFfL1Hc1Ntql0pwAAAA env_logger::try_init().unwrap_or(()); let key = "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMxBTpMIGvo7CnordO7wP0QQRqpBwUjOLl4eMhfucfE1sjTYyK5wmTl1UqoSDS1PtRVTBdl+0+9pquFb46U7fwg="; - assert!( - parse_public_key_base64(key).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP256 - }, - ); + assert!(matches!( + parse_public_key_base64(key), + Ok(key::PublicKey::EC { + key: ec::PublicKey::P256(_), + }) + )); } #[test] @@ -509,12 +552,12 @@ Ve0k2ddxoEsSE15H4lgNHM2iuYKzIqZJOReHRCTff6QGgMYPDqDfFfL1Hc1Ntql0pwAAAA env_logger::try_init().unwrap_or(()); let key = "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBBVFgxJxpCaAALZG/S5BHT8/IUQ5mfuKaj7Av9g7Jw59fBEGHfPBz1wFtHGYw5bdLmfVZTIDfogDid5zqJeAKr1AcD06DKTXDzd2EpUjqeLfQ5b3erHuX758fgu/pSDGRA=="; - assert!( - parse_public_key_base64(key).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP384 - } - ); + assert!(matches!( + parse_public_key_base64(key), + Ok(key::PublicKey::EC { + key: ec::PublicKey::P384(_), + }) + )); } #[test] @@ -522,12 +565,12 @@ Ve0k2ddxoEsSE15H4lgNHM2iuYKzIqZJOReHRCTff6QGgMYPDqDfFfL1Hc1Ntql0pwAAAA env_logger::try_init().unwrap_or(()); let key = "AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAAQepXEpOrzlX22r4E5zEHjhHWeZUe//zaevTanOWRBnnaCGWJFGCdjeAbNOuAmLtXc+HZdJTCZGREeSLSrpJa71QDCgZl0N7DkDUanCpHZJe/DCK6qwtHYbEMn28iLMlGCOrCIa060EyJHbp1xcJx4I1SKj/f/fm3DhhID/do6zyf8Cg=="; - assert!( - parse_public_key_base64(key).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP521 - } - ); + assert!(matches!( + parse_public_key_base64(key), + Ok(key::PublicKey::EC { + key: ec::PublicKey::P521(_), + }) + )); } #[test] @@ -576,35 +619,26 @@ QaChXiDsryJZwsRnruvMRX9nedtqHrgnIsJLTXjppIhGhq5Kg4RQfOU= fn test_decode_pkcs8_rsa_secret_key() { // Generated using: ssh-keygen -t rsa -b 1024 -m pkcs8 -f $file let key = "-----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDTwWfiCKHw/1F6 -pvm6hZpFSjCVSu4Pp0/M4xT9Cec1+2uj/6uEE9Vh/UhlerkxVbrW/YaqjnlAiemZ -0RGN+sq7b8LxsgvOAo7gdBv13TLkKxNFiRbSy8S257uA9/K7G4Uw+NW22zoLSKCp -pdJOFzaYMIT/UX9EOq9hIIn4bS4nXJ4V5+aHBtMddHHDQPEDHBHuifpP2L4Wopzu -WoQoVtN9cwHSLh0Bd7uT+X9useIJrFzcsxVXwD2WGfR59Ue3rxRu6JqC46Klf55R -5NQ8OQ+7NHXjW5HO076W1GXcnhGKT5CGjglTdk5XxQkNZsz72cHu7RDaADdWAWnE -hSyH7flrAgMBAAECggEAbFdpCjn2eTJ4grOJ1AflTYxO3SOQN8wXxTFuHKUDehgg -E7GNFK99HnyTnPA0bmx5guQGEZ+BpCarsXpJbAYj0dC1wimhZo7igS6G272H+zua -yZoBZmrBQ/++bJbvxxGmjM7TsZHq2bkYEpR3zGKOGUHB2kvdPJB2CNC4JrXdxl7q -djjsr5f/SreDmHqcNBe1LcyWLSsuKTfwTKhsE1qEe6QA2uOpUuFrsdPoeYrfgapu -sK6qnpxvOTJHCN/9jjetrP2fGl78FMBYfXzjAyKSKzLvzOwMAmcHxy50RgUvezx7 -A1RwMpB7VoV0MOpcAjlQ1T7YDH9avdPMzp0EZ24y+QKBgQD/MxDJjHu33w13MnIg -R4BrgXvrgL89Zde5tML2/U9C2LRvFjbBvgnYdqLsuqxDxGY/8XerrAkubi7Fx7QI -m2uvTOZF915UT/64T35zk8nAAFhzicCosVCnBEySvdwaaBKoj/ywemGrwoyprgFe -r8LGSo42uJi0zNf5IxmVzrDlRwKBgQDUa3P/+GxgpUYnmlt63/7sII6HDssdTHa9 -x5uPy8/2ackNR7FruEAJR1jz6akvKnvtbCBeRxLeOFwsseFta8rb2vks7a/3I8ph -gJlbw5Bttpc+QsNgC61TdSKVsfWWae+YT77cfGPM4RaLlxRnccW1/HZjP2AMiDYG -WCiluO+svQKBgQC3a/yk4FQL1EXZZmigysOCgY6Ptfm+J3TmBQYcf/R4F0mYjl7M -4coxyxNPEty92Gulieh5ey0eMhNsFB1SEmNTm/HmV+V0tApgbsJ0T8SyO41Xfar7 -lHZjlLN0xQFt+V9vyA3Wyh9pVGvFiUtywuE7pFqS+hrH2HNindfF1MlQAQKBgQDF -YxBIxKzY5duaA2qMdMcq3lnzEIEXua0BTxGz/n1CCizkZUFtyqnetWjoRrGK/Zxp -FDfDw6G50397nNPQXQEFaaZv5HLGYYC3N8vKJKD6AljqZxmsD03BprA7kEGYwtn8 -m+XMdt46TNMpZXt1YJiLMo1ETmjPXGdvX85tqLs2tQKBgQDCbwd+OBzSiic3IQlD -E/OHAXH6HNHmUL3VD5IiRh4At2VAIl8JsmafUvvbtr5dfT3PA8HB6sDG4iXQsBbR -oTSAo/DtIWt1SllGx6MvcPqL1hp1UWfoIGTnE3unHtgPId+DnjMbTcuZOuGl7evf -abw8VeY2goORjpBXsfydBETbgQ== +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKMR20Sc+tU5dS7C +YzIuWnzobqTrIi9JExPTq4GEj01HJ1RJoOoezuiZuIg3iSRRETjXR+pKSzlLEh4v +9VmaDNQMT08EHYCc7NEKXb3c3k/4RNSHtvxKAsyK2ucrvaJGO5GDP7W+yQXpt8Os +KlD8G5LHZJMrZ5m1a+sHYdGzphRXAgMBAAECgYBSG8CjaMOoL3lApSJbdxmbAVIM ++lRJKOtRNWiLG5soVyaHe1dp6z9VwWk4NXZ5cdRRIZ0VbHk6DQG/b3iDuFyybqu3 +M7B40+4N7DCJfoWxALCEDSPQQ/Rp7rQ15YdNahZqe+/c8BHVxHdUZNXvMY8QX8jI +ZmoH8e17tRFKB0SZqQJBANjtPcEo5goaaZlly5VWs8SdNrG/ZM4vKQgpwQmtiNJg +TznqMPBcc8Qk43a6BlPDdn8CrBBjeYRF7qGh0cVdca0CQQDAcTQzF+HfWImqttB0 +dCo+jOqKOovXTTJcp4JUMzgvnMHwQZUJRNQxxqkIrmh/gUwWacSK/yxpLgKlXzBz +msaTAkEAk7VPVISVxxFfEE2pR0HnXJy0TmoFqQOhy+YqhH1+acmciNH3iuNZDJkV +rZVTk5vHxwo5wVsKtk+sArEeFmbfbQJAMbUL5qakkSwtYwsVjP70anO7oTi+Jj6q +Y4RhBZ61RJcZARXviRVeOf02bCeglk6veJqZSc3fist3o3+S5El2QQJBAJjjKA9q +bjFFWPDS9kyrpZL1SOjRIM/Mb0K1hCQd/kfbRTCamqvfuPDQ2A9N40bfBiQFQPph +csKph4+a9f37jyE= -----END PRIVATE KEY----- "; - assert!(decode_secret_key(key, None).unwrap().algorithm().is_rsa()); + assert!(matches!( + decode_secret_key(key, None), + Ok(key::KeyPair::RSA { .. }) + )); test_decode_encode_symmetry(key); } @@ -617,12 +651,12 @@ ydj6EE8QkZ91jtGoGmdYAVd7LaqhRANCAATWkGOof7R/PAUuOr2+ZPUgB8rGVvgr qa92U3p4fkJToKXku5eq/32OBj23YMtz76jO3yfMbtG3l1JWLowPA8tV -----END PRIVATE KEY----- "; - assert!( - decode_secret_key(key, None).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP256 - }, - ); + assert!(matches!( + decode_secret_key(key, None), + Ok(key::KeyPair::EC { + key: ec::PrivateKey::P256(_) + }) + )); test_decode_encode_symmetry(key); } @@ -636,12 +670,12 @@ MrzeDXiUwy9LM8qJGNXiMYou0pVjFZPZT3jAsrUQo47PLQ6hZANiAARuEHbXJBYK CI3WfCsQvVjoC7m8qRyxuvR3Rv8gGXR1coQciIoCurLnn9zOFvXCS2Y= -----END PRIVATE KEY----- "; - assert!( - decode_secret_key(key, None).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP384 - }, - ); + assert!(matches!( + decode_secret_key(key, None), + Ok(key::KeyPair::EC { + key: ec::PrivateKey::P384(_) + }) + )); test_decode_encode_symmetry(key); } @@ -657,12 +691,12 @@ iaOYDwInbFDsHu8j3TGs29KxyVXMexeV6ROQyXzjVC/quT1R5cOQ7EadE4HvaWhT Ow== -----END PRIVATE KEY----- "; - assert!( - decode_secret_key(key, None).unwrap().algorithm() - == ssh_key::Algorithm::Ecdsa { - curve: ssh_key::EcdsaCurve::NistP521 - }, - ); + assert!(matches!( + decode_secret_key(key, None), + Ok(key::KeyPair::EC { + key: ec::PrivateKey::P521(_) + }) + )); test_decode_encode_symmetry(key); } @@ -678,13 +712,17 @@ ocyR -----END PRIVATE KEY----- "; - assert!(decode_secret_key(key, None)?.algorithm() == ssh_key::Algorithm::Ed25519,); + assert!(matches!( + decode_secret_key(key, None)?, + key::KeyPair::Ed25519(_) + )); - let k = decode_secret_key(key, None)?; - let inner = k.key_data().ed25519().unwrap(); + let key::KeyPair::Ed25519(inner) = decode_secret_key(key, None)? else { + panic!(); + }; assert_eq!( - &inner.private.to_bytes(), + &inner.to_bytes(), &[ 17, 240, 225, 197, 207, 164, 104, 0, 248, 167, 111, 60, 94, 96, 198, 188, 204, 47, 234, 26, 223, 36, 36, 208, 156, 200, 109, 107, 230, 168, 206, 71 @@ -709,6 +747,177 @@ ocyR assert_eq!(original_key_bytes, encoded_key_bytes); } + fn ecdsa_sign_verify(key: &str, public: &str) { + let key = decode_secret_key(key, None).unwrap(); + let buf = b"blabla"; + let sig = key.sign_detached(buf).unwrap(); + // Verify using the provided public key. + { + let public = parse_public_key_base64(public).unwrap(); + assert!(public.verify_detached(buf, sig.as_ref())); + } + // Verify using public key derived from the secret key. + { + let public = key.clone_public_key().unwrap(); + assert!(public.verify_detached(buf, sig.as_ref())); + } + // Sanity check that it uses a different random number. + { + let sig2 = key.sign_detached(buf).unwrap(); + match (sig, sig2) { + ( + key::Signature::ECDSA { + algorithm, + signature, + }, + key::Signature::ECDSA { + algorithm: algorithm2, + signature: signature2, + }, + ) => { + assert_eq!(algorithm, algorithm2); + assert_ne!(signature, signature2); + } + _ => assert!(false), + } + } + // Verify (r, s) = (0, 0) is an invalid signature. (CVE-2022-21449) + { + use crate::encoding::Encoding; + let mut sig = Vec::new(); + sig.extend_ssh_string(&[0]); + sig.extend_ssh_string(&[0]); + let public = key.clone_public_key().unwrap(); + assert!(!public.verify_detached(buf, &sig)); + } + } + + #[test] + fn test_ecdsa_sha2_nistp256_sign_verify() { + env_logger::try_init().unwrap_or(()); + let key = "-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQRQh23nB1wSlbAwhX3hrbNa35Z6vuY1 +CnEhAjk4FSWR1/tcna7RKCMXdYEiPs5rHr+mMoJxeQxmCd+ny8uIBrg1AAAAqKgQe5KoEH +uSAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFCHbecHXBKVsDCF +feGts1rflnq+5jUKcSECOTgVJZHX+1ydrtEoIxd1gSI+zmsev6YygnF5DGYJ36fLy4gGuD +UAAAAgFOgyq4FDOtEe+vBy1O1dqMLjXrKmqcgPpOO3+9cbPM0AAAAKZWNkc2FAdGVzdAEC +AwQFBg== +-----END OPENSSH PRIVATE KEY----- +"; + let public = "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFCHbecHXBKVsDCFfeGts1rflnq+5jUKcSECOTgVJZHX+1ydrtEoIxd1gSI+zmsev6YygnF5DGYJ36fLy4gGuDU="; + ecdsa_sign_verify(key, public); + } + + #[test] + fn test_ecdsa_sha2_nistp384_sign_verify() { + env_logger::try_init().unwrap_or(()); + let key = "-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS +1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQRC1ed+3MGnknPWE6rdbw9p5f91AJSC +a469EDg5+EkDdVEEN1dWakAtI+gLRlpMotYD0Cso1Nx2MU9nW5fWLBmLtOWU6C1SX6INXB +527U0Ex5AYetNPBIhdTWB1UhbVkxgAAADYiT5XRYk+V0UAAAATZWNkc2Etc2hhMi1uaXN0 +cDM4NAAAAAhuaXN0cDM4NAAAAGEEQtXnftzBp5Jz1hOq3W8PaeX/dQCUgmuOvRA4OfhJA3 +VRBDdXVmpALSPoC0ZaTKLWA9ArKNTcdjFPZ1uX1iwZi7TllOgtUl+iDVwedu1NBMeQGHrT +TwSIXU1gdVIW1ZMYAAAAMH13rmHaaOv7SG4v/e3AV6yY49DzZD8YTzHRS62KDUPB/6t774 +PCeBxYsjjIg5q1FwAAAAplY2RzYUB0ZXN0AQIDBAUG +-----END OPENSSH PRIVATE KEY----- +"; + let public = "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBELV537cwaeSc9YTqt1vD2nl/3UAlIJrjr0QODn4SQN1UQQ3V1ZqQC0j6AtGWkyi1gPQKyjU3HYxT2dbl9YsGYu05ZToLVJfog1cHnbtTQTHkBh6008EiF1NYHVSFtWTGA=="; + ecdsa_sign_verify(key, public); + } + + #[test] + fn test_ecdsa_sha2_nistp521_sign_verify() { + env_logger::try_init().unwrap_or(()); + let key = "-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS +1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBVr19z0rsH1q3nly7RMJBfcHQER5H +oyqEAfX6NnGsa6atBcILGTKYNk/wqf58WabI1XY0ZGsJrx9twIbD6Wu0IcMAlS4MEYNjk7 +/J0FWEfYVKRIRRSK8bzT2uiDxRwmH1ZkQSEE/ghur46O4pA4H++w699LU3alWtDx+bJfx7 +zu4XjHwAAAEQqaEnO6mhJzsAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ +AAAIUEAVa9fc9K7B9at55cu0TCQX3B0BEeR6MqhAH1+jZxrGumrQXCCxkymDZP8Kn+fFmm +yNV2NGRrCa8fbcCGw+lrtCHDAJUuDBGDY5O/ydBVhH2FSkSEUUivG809rog8UcJh9WZEEh +BP4Ibq+OjuKQOB/vsOvfS1N2pVrQ8fmyX8e87uF4x8AAAAQgE10hd4g3skdWl4djRX4kE3 +ZgmnWhuwhyxErC5UkMHiEvTOZllxBvefs7XeJqL11pqQIHY4Gb5OQGiCNHiRRjg0egAAAA +1yb2JlcnRAYmJzZGV2AQIDBAU= +-----END OPENSSH PRIVATE KEY----- +"; + let public = "AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFWvX3PSuwfWreeXLtEwkF9wdARHkejKoQB9fo2caxrpq0FwgsZMpg2T/Cp/nxZpsjVdjRkawmvH23AhsPpa7QhwwCVLgwRg2OTv8nQVYR9hUpEhFFIrxvNPa6IPFHCYfVmRBIQT+CG6vjo7ikDgf77Dr30tTdqVa0PH5sl/HvO7heMfA=="; + ecdsa_sign_verify(key, public); + } + + pub const PKCS8_RSA: &str = "-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwBGetHjW+3bDQpVktdemnk7JXgu1NBWUM+ysifYLDBvJ9ttX +GNZSyQKA4v/dNr0FhAJ8I9BuOTjYCy1YfKylhl5D/DiSSXFPsQzERMmGgAlYvU2U ++FTxpBC11EZg69CPVMKKevfoUD+PZA5zB7Hc1dXFfwqFc5249SdbAwD39VTbrOUI +WECvWZs6/ucQxHHXP2O9qxWqhzb/ddOnqsDHUNoeceiNiCf2anNymovrIMjAqq1R +t2UP3f06/Zt7Jx5AxKqS4seFkaDlMAK8JkEDuMDOdKI36raHkKanfx8CnGMSNjFQ +QtvnpD8VSGkDTJN3Qs14vj2wvS477BQXkBKN1QIDAQABAoIBABb6xLMw9f+2ENyJ +hTggagXsxTjkS7TElCu2OFp1PpMfTAWl7oDBO7xi+UqvdCcVbHCD35hlWpqsC2Ui +8sBP46n040ts9UumK/Ox5FWaiuYMuDpF6vnfJ94KRcb0+KmeFVf9wpW9zWS0hhJh +jC+yfwpyfiOZ/ad8imGCaOguGHyYiiwbRf381T/1FlaOGSae88h+O8SKTG1Oahq4 +0HZ/KBQf9pij0mfVQhYBzsNu2JsHNx9+DwJkrXT7K9SHBpiBAKisTTCnQmS89GtE +6J2+bq96WgugiM7X6OPnmBmE/q1TgV18OhT+rlvvNi5/n8Z1ag5Xlg1Rtq/bxByP +CeIVHsECgYEA9dX+LQdv/Mg/VGIos2LbpJUhJDj0XWnTRq9Kk2tVzr+9aL5VikEb +09UPIEa2ToL6LjlkDOnyqIMd/WY1W0+9Zf1ttg43S/6Rvv1W8YQde0Nc7QTcuZ1K +9jSSP9hzsa3KZtx0fCtvVHm+ac9fP6u80tqumbiD2F0cnCZcSxOb4+UCgYEAyAKJ +70nNKegH4rTCStAqR7WGAsdPE3hBsC814jguplCpb4TwID+U78Xxu0DQF8WtVJ10 +SJuR0R2q4L9uYWpo0MxdawSK5s9Am27MtJL0mkFQX0QiM7hSZ3oqimsdUdXwxCGg +oktxCUUHDIPJNVd4Xjg0JTh4UZT6WK9hl1zLQzECgYEAiZRCFGc2KCzVLF9m0cXA +kGIZUxFAyMqBv+w3+zq1oegyk1z5uE7pyOpS9cg9HME2TAo4UPXYpLAEZ5z8vWZp +45sp/BoGnlQQsudK8gzzBtnTNp5i/MnnetQ/CNYVIVnWjSxRUHBqdMdRZhv0/Uga +e5KA5myZ9MtfSJA7VJTbyHUCgYBCcS13M1IXaMAt3JRqm+pftfqVs7YeJqXTrGs/ +AiDlGQigRk4quFR2rpAV/3rhWsawxDmb4So4iJ16Wb2GWP4G1sz1vyWRdSnmOJGC +LwtYrvfPHegqvEGLpHa7UsgDpol77hvZriwXwzmLO8A8mxkeW5dfAfpeR5o+mcxW +pvnTEQKBgQCKx6Ln0ku6jDyuDzA9xV2/PET5D75X61R2yhdxi8zurY/5Qon3OWzk +jn/nHT3AZghGngOnzyv9wPMKt9BTHyTB6DlB6bRVLDkmNqZh5Wi8U1/IjyNYI0t2 +xV/JrzLAwPoKk3bkqys3bUmgo6DxVC/6RmMwPQ0rmpw78kOgEej90g== +-----END RSA PRIVATE KEY----- +"; + + #[test] + fn test_loewenheim() -> Result<(), Error> { + env_logger::try_init().unwrap_or(()); + let key = "-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,80E4FCAD049EE007CCE1C65D52CDB87A + +ZKBKtex8+DA/d08TTPp4vY8RV+r+1nUC1La+r0dSiXsfunRNDPcYhHbyA/Fdr9kQ ++d1/E3cEb0k2nq7xYyMzy8hpNp/uHu7UfllGdaBusiPjHR+feg6AQfbM0FWpdGzo +9l/Vho5Ocw8abQq1Q9aPW5QQXBURC7HtCQXbpuYjUAQBeea1LzPCw6UIF80GUUkY +1AycXxVfx1AeURAKTZR4hsxC5pqI4yhAvVNXxP+tTTa9NE8lOP0yqVNurfIqyAnp +5ELMwNdHXZyUcT+EH5PsC69ocQgEZqLs0chvke62woMOjeSpsW5cIjGohW9lOD1f +nJkECVZ50kE0SDvcL4Y338tHwMt7wdwdj1dkAWSUjAJT4ShjqV/TzaLAiNAyRxLl +cm3mAccaFIIBZG/bPLGI0B5+mf9VExXGJrbGlvURhtE3nwmjLg1vT8lVfqbyL3a+ +0tFvmDYn71L97t/3hcD2tVnKLv9g8+/OCsUAk3+/0eS7D6GpmlOMRHdLLUHc4SOm +bIDT/dE6MjsCSm7n/JkTb8P+Ta1Hp94dUnX4pfjzZ+O8V1H8wv7QW5KsuJhJ8cn4 +eS3BEgNH1I4FCCjLsZdWve9ehV3/19WXh+BF4WXFq9b3plmfJgTiZslvjy4dgThm +OhEK44+fN1UhzguofxTR4Maz7lcehQxGAxp14hf1EnaAEt3LVjEPEShgK5dx1Ftu +LWFz9nR4vZcMsaiszElrevqMhPQHXY7cnWqBenkMfkdcQDoZjKvV86K98kBIDMu+ +kf855vqRF8b2n/6HPdm3eqFh/F410nSB0bBSglUfyOZH1nS+cs79RQZEF9fNUmpH +EPQtQ/PALohicj9Vh7rRaMKpsORdC8/Ahh20s01xL6siZ334ka3BLYT94UG796/C +4K1S2kPdUP8POJ2HhaK2l6qaG8tcEX7HbwwZeKiEHVNvWuIGQO9TiDONLycp9x4y +kNM3sv2pI7vEhs7d2NapWgNha1RcTSv0CQ6Th/qhGo73LBpVmKwombVImHAyMGAE +aVF32OycVd9c9tDgW5KdhWedbeaxD6qkSs0no71083kYIS7c6iC1R3ZeufEkMhmx +dwrciWTJ+ZAk6rS975onKz6mo/4PytcCY7Df/6xUxHF3iJCnuK8hNpLdJcdOiqEK +zj/d5YGyw3J2r+NrlV1gs3FyvR3eMCWWH2gpIQISBpnEANY40PxA/ogH+nCUvI/O +n8m437ZeLTg6lnPqsE4nlk2hUEwRdy/SVaQURbn7YlcYIt0e81r5sBXb4MXkLrf0 +XRWmpSggdcaaMuXi7nVSdkgCMjGP7epS7HsfP46OrTtJLHn5LxvdOEaW53nPOVQg +/PlVfDbwWl8adE3i3PDQOw9jhYXnYS3sv4R8M8y2GYEXbINrTJyUGrlNggKFS6oh +Hjgt0gsM2N/D8vBrQwnRtyymRnFd4dXFEYKAyt+vk0sa36eLfl0z6bWzIchkJbdu +raMODVc+NiJE0Qe6bwAi4HSpJ0qw2lKwVHYB8cdnNVv13acApod326/9itdbb3lt +KJaj7gc0n6gmKY6r0/Ddufy1JZ6eihBCSJ64RARBXeg2rZpyT+xxhMEZLK5meOeR +-----END RSA PRIVATE KEY----- +"; + let key = decode_secret_key(key, Some("passphrase")).unwrap(); + let public = key.clone_public_key()?; + let buf = b"blabla"; + let sig = key.sign_detached(buf).unwrap(); + assert!(public.verify_detached(buf, sig.as_ref())); + Ok(()) + } + #[test] fn test_o01eg() { env_logger::try_init().unwrap_or(()); @@ -746,36 +955,6 @@ br8gXU8KyiY9sZVbmplRPF+ar462zcI2kt0a18mr0vbrdqp2eMjb37QDbVBJ+rPE "; decode_secret_key(key, Some("12345")).unwrap(); } - - pub const PKCS8_RSA: &str = "-----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAwBGetHjW+3bDQpVktdemnk7JXgu1NBWUM+ysifYLDBvJ9ttX -GNZSyQKA4v/dNr0FhAJ8I9BuOTjYCy1YfKylhl5D/DiSSXFPsQzERMmGgAlYvU2U -+FTxpBC11EZg69CPVMKKevfoUD+PZA5zB7Hc1dXFfwqFc5249SdbAwD39VTbrOUI -WECvWZs6/ucQxHHXP2O9qxWqhzb/ddOnqsDHUNoeceiNiCf2anNymovrIMjAqq1R -t2UP3f06/Zt7Jx5AxKqS4seFkaDlMAK8JkEDuMDOdKI36raHkKanfx8CnGMSNjFQ -QtvnpD8VSGkDTJN3Qs14vj2wvS477BQXkBKN1QIDAQABAoIBABb6xLMw9f+2ENyJ -hTggagXsxTjkS7TElCu2OFp1PpMfTAWl7oDBO7xi+UqvdCcVbHCD35hlWpqsC2Ui -8sBP46n040ts9UumK/Ox5FWaiuYMuDpF6vnfJ94KRcb0+KmeFVf9wpW9zWS0hhJh -jC+yfwpyfiOZ/ad8imGCaOguGHyYiiwbRf381T/1FlaOGSae88h+O8SKTG1Oahq4 -0HZ/KBQf9pij0mfVQhYBzsNu2JsHNx9+DwJkrXT7K9SHBpiBAKisTTCnQmS89GtE -6J2+bq96WgugiM7X6OPnmBmE/q1TgV18OhT+rlvvNi5/n8Z1ag5Xlg1Rtq/bxByP -CeIVHsECgYEA9dX+LQdv/Mg/VGIos2LbpJUhJDj0XWnTRq9Kk2tVzr+9aL5VikEb -09UPIEa2ToL6LjlkDOnyqIMd/WY1W0+9Zf1ttg43S/6Rvv1W8YQde0Nc7QTcuZ1K -9jSSP9hzsa3KZtx0fCtvVHm+ac9fP6u80tqumbiD2F0cnCZcSxOb4+UCgYEAyAKJ -70nNKegH4rTCStAqR7WGAsdPE3hBsC814jguplCpb4TwID+U78Xxu0DQF8WtVJ10 -SJuR0R2q4L9uYWpo0MxdawSK5s9Am27MtJL0mkFQX0QiM7hSZ3oqimsdUdXwxCGg -oktxCUUHDIPJNVd4Xjg0JTh4UZT6WK9hl1zLQzECgYEAiZRCFGc2KCzVLF9m0cXA -kGIZUxFAyMqBv+w3+zq1oegyk1z5uE7pyOpS9cg9HME2TAo4UPXYpLAEZ5z8vWZp -45sp/BoGnlQQsudK8gzzBtnTNp5i/MnnetQ/CNYVIVnWjSxRUHBqdMdRZhv0/Uga -e5KA5myZ9MtfSJA7VJTbyHUCgYBCcS13M1IXaMAt3JRqm+pftfqVs7YeJqXTrGs/ -AiDlGQigRk4quFR2rpAV/3rhWsawxDmb4So4iJ16Wb2GWP4G1sz1vyWRdSnmOJGC -LwtYrvfPHegqvEGLpHa7UsgDpol77hvZriwXwzmLO8A8mxkeW5dfAfpeR5o+mcxW -pvnTEQKBgQCKx6Ln0ku6jDyuDzA9xV2/PET5D75X61R2yhdxi8zurY/5Qon3OWzk -jn/nHT3AZghGngOnzyv9wPMKt9BTHyTB6DlB6bRVLDkmNqZh5Wi8U1/IjyNYI0t2 -xV/JrzLAwPoKk3bkqys3bUmgo6DxVC/6RmMwPQ0rmpw78kOgEej90g== ------END RSA PRIVATE KEY----- -"; - #[test] fn test_pkcs8() { env_logger::try_init().unwrap_or(()); @@ -817,6 +996,7 @@ Cog3JMeTrb3LiPHgN6gU2P30MRp6L1j1J/MtlOAr5rux #[test] fn test_gpg() { env_logger::try_init().unwrap_or(()); + let algo = [115, 115, 104, 45, 114, 115, 97]; let key = [ 0, 0, 0, 7, 115, 115, 104, 45, 114, 115, 97, 0, 0, 0, 3, 1, 0, 1, 0, 0, 1, 129, 0, 163, 72, 59, 242, 4, 248, 139, 217, 57, 126, 18, 195, 170, 3, 94, 154, 9, 150, 89, 171, 236, @@ -841,7 +1021,8 @@ Cog3JMeTrb3LiPHgN6gU2P30MRp6L1j1J/MtlOAr5rux 117, 254, 51, 45, 93, 184, 80, 225, 158, 29, 76, 38, 69, 72, 71, 76, 50, 191, 210, 95, 152, 175, 26, 207, 91, 7, ]; - ssh_key::PublicKey::decode(&key).unwrap(); + debug!("algo = {:?}", std::str::from_utf8(&algo)); + key::PublicKey::parse(&algo, &key).unwrap(); } #[test] @@ -852,7 +1033,7 @@ Cog3JMeTrb3LiPHgN6gU2P30MRp6L1j1J/MtlOAr5rux } #[cfg(unix)] - async fn test_client_agent(key: PrivateKey) -> Result<(), Box> { + async fn test_client_agent(key: key::KeyPair) -> Result<(), Box> { env_logger::try_init().unwrap_or(()); use std::process::Stdio; @@ -871,24 +1052,22 @@ Cog3JMeTrb3LiPHgN6gU2P30MRp6L1j1J/MtlOAr5rux tokio::time::sleep(std::time::Duration::from_millis(10)).await; } - let public = key.public_key(); + let public = key.clone_public_key()?; let stream = tokio::net::UnixStream::connect(&agent_path).await?; let mut client = agent::client::AgentClient::connect(stream); client.add_identity(&key, &[]).await?; client.request_identities().await?; let buf = russh_cryptovec::CryptoVec::from_slice(b"blabla"); let len = buf.len(); - let buf = client.sign_request(&public, buf).await.unwrap(); + let (_, buf) = client.sign_request(&public, buf).await; + let buf = buf?; let (a, b) = buf.split_at(len); - - match key.public_key().key_data() { - ssh_key::public::KeyData::Ed25519 { .. } => { + match key { + key::KeyPair::Ed25519 { .. } => { let sig = &b[b.len() - 64..]; - let sig = ssh_key::Signature::new(key.algorithm(), sig)?; - use signature::Verifier; - assert!(Verifier::verify(public, a, &sig).is_ok()); + assert!(public.verify_detached(a, sig)); } - ssh_key::public::KeyData::Ecdsa { .. } => {} + key::KeyPair::EC { .. } => {} _ => {} } @@ -928,14 +1107,13 @@ Cog3JMeTrb3LiPHgN6gU2P30MRp6L1j1J/MtlOAr5rux let core = tokio::runtime::Runtime::new().unwrap(); use agent; - use signature::Verifier; #[derive(Clone)] struct X {} impl agent::server::Agent for X { fn confirm( self, - _: std::sync::Arc, + _: std::sync::Arc, ) -> Box + Send + Unpin> { Box::new(futures::future::ready((self, true))) } @@ -954,24 +1132,25 @@ Cog3JMeTrb3LiPHgN6gU2P30MRp6L1j1J/MtlOAr5rux }); let key = decode_secret_key(PKCS8_ENCRYPTED, Some("blabla")).unwrap(); core.block_on(async move { - let public = key.public_key(); - let stream = tokio::net::UnixStream::connect(&agent_path).await.unwrap(); + let public = key.clone_public_key()?; + let stream = tokio::net::UnixStream::connect(&agent_path).await?; let mut client = agent::client::AgentClient::connect(stream); client .add_identity(&key, &[agent::Constraint::KeyLifetime { seconds: 60 }]) - .await - .unwrap(); - client.request_identities().await.unwrap(); + .await?; + client.request_identities().await?; let buf = russh_cryptovec::CryptoVec::from_slice(b"blabla"); let len = buf.len(); - let buf = client.sign_request(&public, buf).await.unwrap(); + let (_, buf) = client.sign_request(&public, buf).await; + let buf = buf?; let (a, b) = buf.split_at(len); - if let ssh_key::public::KeyData::Ed25519 { .. } = public.key_data() { + if let key::KeyPair::Ed25519 { .. } = key { let sig = &b[b.len() - 64..]; - let sig = ssh_key::Signature::new(key.algorithm(), sig).unwrap(); - assert!(Verifier::verify(public, a, &sig).is_ok()); + assert!(public.verify_detached(a, sig)); } + Ok::<(), Error>(()) }) + .unwrap() } #[cfg(unix)] diff --git a/russh-keys/src/protocol.rs b/russh-keys/src/protocol.rs new file mode 100644 index 00000000..ff9a2933 --- /dev/null +++ b/russh-keys/src/protocol.rs @@ -0,0 +1,87 @@ +use std::borrow::Cow; + +use crate::encoding::{Encoding, Position, SshRead, SshWrite}; +use crate::key::zeroize_cow; + +type Result = std::result::Result; + +/// SSH RSA public key. +pub struct RsaPublicKey<'a> { + /// `e`: RSA public exponent. + pub public_exponent: Cow<'a, [u8]>, + /// `n`: RSA modulus. + pub modulus: Cow<'a, [u8]>, +} + +impl<'a> SshRead<'a> for RsaPublicKey<'a> { + fn read_ssh(pos: &mut Position<'a>) -> Result { + Ok(Self { + public_exponent: Cow::Borrowed(pos.read_mpint()?), + modulus: Cow::Borrowed(pos.read_mpint()?), + }) + } +} + +impl SshWrite for RsaPublicKey<'_> { + fn write_ssh(&self, encoder: &mut E) { + encoder.extend_ssh_mpint(&self.public_exponent); + encoder.extend_ssh_mpint(&self.modulus); + } +} + +/// SSH RSA private key. +pub struct RsaPrivateKey<'a> { + /// RSA public key. + pub public_key: RsaPublicKey<'a>, + /// `d`: RSA private exponent. + pub private_exponent: Cow<'a, [u8]>, + /// CRT coefficient: `(inverse of q) mod p`. + pub coefficient: Cow<'a, [u8]>, + /// `p`: first prime factor of `n`. + pub prime1: Cow<'a, [u8]>, + /// `q`: Second prime factor of `n`. + pub prime2: Cow<'a, [u8]>, + /// Comment. + pub comment: Cow<'a, [u8]>, +} + +impl<'a> SshRead<'a> for RsaPrivateKey<'a> { + fn read_ssh(pos: &mut Position<'a>) -> Result { + Ok(Self { + // Note the field order. + public_key: RsaPublicKey { + modulus: Cow::Borrowed(pos.read_mpint()?), + public_exponent: Cow::Borrowed(pos.read_mpint()?), + }, + private_exponent: Cow::Borrowed(pos.read_mpint()?), + coefficient: Cow::Borrowed(pos.read_mpint()?), + prime1: Cow::Borrowed(pos.read_mpint()?), + prime2: Cow::Borrowed(pos.read_mpint()?), + comment: Cow::Borrowed(pos.read_string()?), + }) + } +} + +impl SshWrite for RsaPrivateKey<'_> { + fn write_ssh(&self, encoder: &mut E) { + // Note the field order. + encoder.extend_ssh_mpint(&self.public_key.modulus); + encoder.extend_ssh_mpint(&self.public_key.public_exponent); + encoder.extend_ssh_mpint(&self.private_exponent); + encoder.extend_ssh_mpint(&self.coefficient); + encoder.extend_ssh_mpint(&self.prime1); + encoder.extend_ssh_mpint(&self.prime2); + encoder.extend_ssh_string(&self.comment); + } +} + +impl Drop for RsaPrivateKey<'_> { + fn drop(&mut self) { + // Private parts only. + zeroize_cow(&mut self.private_exponent); + zeroize_cow(&mut self.coefficient); + zeroize_cow(&mut self.prime1); + zeroize_cow(&mut self.prime2); + zeroize_cow(&mut self.comment); + } +} diff --git a/russh-keys/src/signature.rs b/russh-keys/src/signature.rs new file mode 100644 index 00000000..7bf05c21 --- /dev/null +++ b/russh-keys/src/signature.rs @@ -0,0 +1,190 @@ +use std::fmt; + +use byteorder::{BigEndian, WriteBytesExt}; +use serde; +use serde::de::{SeqAccess, Visitor}; +use serde::ser::SerializeTuple; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::key::SignatureHash; +use crate::Error; + +pub struct SignatureBytes(pub [u8; 64]); + +/// The type of a signature, depending on the algorithm used. +#[derive(Serialize, Deserialize, Clone)] +pub enum Signature { + /// An Ed25519 signature + Ed25519(SignatureBytes), + /// An RSA signature + RSA { hash: SignatureHash, bytes: Vec }, + /// An ECDSA signature + ECDSA { + /// Algorithm name defined in RFC 5656 section 3.1.2, in the form of + /// `"ecdsa-sha2-[identifier]"`. + algorithm: &'static str, + /// Signature blob defined in RFC 5656 section 3.1.2. + signature: Vec, + }, +} + +impl Signature { + pub fn to_base64(&self) -> String { + use crate::encoding::Encoding; + let mut bytes_ = Vec::new(); + match self { + Signature::Ed25519(ref bytes) => { + let t = b"ssh-ed25519"; + #[allow(clippy::unwrap_used)] // Vec<>.write_all can't fail + bytes_ + .write_u32::((t.len() + bytes.0.len() + 8) as u32) + .unwrap(); + bytes_.extend_ssh_string(t); + bytes_.extend_ssh_string(&bytes.0[..]); + } + Signature::RSA { + ref hash, + ref bytes, + } => { + let t = match hash { + SignatureHash::SHA2_256 => &b"rsa-sha2-256"[..], + SignatureHash::SHA2_512 => &b"rsa-sha2-512"[..], + SignatureHash::SHA1 => &b"ssh-rsa"[..], + }; + #[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::ECDSA { + algorithm, + signature, + } => { + let algorithm = algorithm.as_bytes(); + #[allow(clippy::unwrap_used)] // Vec<>.write_all can't fail + bytes_ + .write_u32::((algorithm.len() + signature.len() + 8) as u32) + .unwrap(); + bytes_.extend_ssh_string(algorithm); + bytes_.extend_ssh_string(signature); + } + } + data_encoding::BASE64_NOPAD.encode(&bytes_[..]) + } + + pub fn from_base64(s: &[u8]) -> Result { + let bytes_ = data_encoding::BASE64_NOPAD.decode(s)?; + use crate::encoding::Reader; + let mut r = bytes_.reader(0); + let sig = r.read_string()?; + let mut r = sig.reader(0); + let typ = r.read_string()?; + let bytes = r.read_string()?; + match typ { + b"ssh-ed25519" => { + let mut bytes_ = [0; 64]; + bytes_.clone_from_slice(bytes); + Ok(Signature::Ed25519(SignatureBytes(bytes_))) + } + b"rsa-sha2-256" => Ok(Signature::RSA { + hash: SignatureHash::SHA2_256, + bytes: bytes.to_vec(), + }), + b"rsa-sha2-512" => Ok(Signature::RSA { + hash: SignatureHash::SHA2_512, + bytes: bytes.to_vec(), + }), + b"ssh-rsa" => Ok(Signature::RSA { + hash: SignatureHash::SHA1, + bytes: bytes.to_vec(), + }), + crate::KEYTYPE_ECDSA_SHA2_NISTP256 => Ok(Signature::ECDSA { + algorithm: crate::ECDSA_SHA2_NISTP256, + signature: bytes.to_vec(), + }), + crate::KEYTYPE_ECDSA_SHA2_NISTP384 => Ok(Signature::ECDSA { + algorithm: crate::ECDSA_SHA2_NISTP384, + signature: bytes.to_vec(), + }), + crate::KEYTYPE_ECDSA_SHA2_NISTP521 => Ok(Signature::ECDSA { + algorithm: crate::ECDSA_SHA2_NISTP521, + signature: bytes.to_vec(), + }), + _ => Err(Error::UnknownSignatureType { + sig_type: std::str::from_utf8(typ).unwrap_or("").to_string(), + }), + } + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + match *self { + Signature::Ed25519(ref signature) => &signature.0, + Signature::RSA { ref bytes, .. } => &bytes[..], + Signature::ECDSA { ref signature, .. } => &signature[..], + } + } +} + +impl AsRef<[u8]> for SignatureBytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl<'de> Deserialize<'de> for SignatureBytes { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct Vis; + impl<'de> Visitor<'de> for Vis { + type Value = SignatureBytes; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("64 bytes") + } + fn visit_seq>(self, mut seq: A) -> Result { + let mut result = [0; 64]; + for x in result.iter_mut() { + if let Some(y) = seq.next_element()? { + *x = y + } else { + return Err(serde::de::Error::invalid_length(64, &self)); + } + } + Ok(SignatureBytes(result)) + } + } + deserializer.deserialize_tuple(64, Vis) + } +} + +impl Serialize for SignatureBytes { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut tup = serializer.serialize_tuple(64)?; + for byte in self.0.iter() { + tup.serialize_element(byte)?; + } + tup.end() + } +} + +impl fmt::Debug for SignatureBytes { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "{:?}", &self.0[..]) + } +} + +impl Clone for SignatureBytes { + fn clone(&self) -> Self { + let mut result = SignatureBytes([0; 64]); + result.0.clone_from_slice(&self.0); + result + } +} diff --git a/russh/Cargo.toml b/russh/Cargo.toml index cfa55211..7ff2ed92 100644 --- a/russh/Cargo.toml +++ b/russh/Cargo.toml @@ -14,6 +14,8 @@ rust-version = "1.65" [features] default = ["flate2"] +openssl = ["russh-keys/openssl", "dep:openssl"] +vendored-openssl = ["openssl/vendored", "russh-keys/vendored-openssl"] legacy-ed25519-pkcs8-parser = ["russh-keys/legacy-ed25519-pkcs8-parser"] [dependencies] @@ -46,7 +48,6 @@ russh-cryptovec = { version = "0.7.0", path = "../cryptovec" } russh-keys = { version = "0.46.0", path = "../russh-keys" } sha1 = { workspace = true } sha2 = { workspace = true } -signature = { workspace = true } ssh-encoding = { workspace = true } ssh-key = { workspace = true } subtle = "2.4" @@ -74,6 +75,10 @@ tokio-fd = "0.3" termion = "2" ratatui = "0.26.0" +[package.metadata.docs.rs] +features = ["openssl"] + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +openssl = { workspace = true, optional = true } russh-sftp = "2.0.5" tokio = { workspace = true } diff --git a/russh/examples/client_exec_interactive.rs b/russh/examples/client_exec_interactive.rs index 0626acd4..7a428301 100644 --- a/russh/examples/client_exec_interactive.rs +++ b/russh/examples/client_exec_interactive.rs @@ -71,7 +71,7 @@ impl client::Handler for Client { async fn check_server_key( &mut self, - _server_public_key: &ssh_key::PublicKey, + _server_public_key: &key::PublicKey, ) -> Result { Ok(true) } diff --git a/russh/examples/client_exec_simple.rs b/russh/examples/client_exec_simple.rs index 5c2a857a..50787575 100644 --- a/russh/examples/client_exec_simple.rs +++ b/russh/examples/client_exec_simple.rs @@ -18,7 +18,7 @@ use tokio::net::ToSocketAddrs; #[tokio::main] async fn main() -> Result<()> { env_logger::builder() - .filter_level(log::LevelFilter::Debug) + .filter_level(log::LevelFilter::Info) .init(); // CLI options are defined later in this file @@ -62,7 +62,7 @@ impl client::Handler for Client { async fn check_server_key( &mut self, - _server_public_key: &ssh_key::PublicKey, + _server_public_key: &key::PublicKey, ) -> Result { Ok(true) } diff --git a/russh/examples/echoserver.rs b/russh/examples/echoserver.rs index 4506b49d..f3843755 100644 --- a/russh/examples/echoserver.rs +++ b/russh/examples/echoserver.rs @@ -2,11 +2,9 @@ use std::collections::HashMap; use std::sync::Arc; use async_trait::async_trait; -use rand_core::OsRng; use russh::keys::*; use russh::server::{Msg, Server as _, Session}; use russh::*; -use russh_keys::Certificate; use tokio::sync::Mutex; #[tokio::main] @@ -19,13 +17,7 @@ async fn main() { inactivity_timeout: Some(std::time::Duration::from_secs(3600)), auth_rejection_time: std::time::Duration::from_secs(3), auth_rejection_time_initial: Some(std::time::Duration::from_secs(0)), - keys: vec![ - russh_keys::PrivateKey::random(&mut OsRng, russh_keys::Algorithm::Ed25519).unwrap(), - ], - preferred: Preferred { - // key: Cow::Borrowed(&[CERT_ECDSA_SHA2_P256]), - ..Preferred::default() - }, + keys: vec![russh_keys::key::KeyPair::generate_ed25519()], ..Default::default() }; let config = Arc::new(config); @@ -84,19 +76,8 @@ impl server::Handler for Server { async fn auth_publickey( &mut self, _: &str, - _key: &ssh_key::PublicKey, + _: &key::PublicKey, ) -> Result { - Ok(server::Auth::Reject { - proceed_with_methods: None, - }) - } - - async fn auth_openssh_certificate( - &mut self, - _user: &str, - certificate: &Certificate, - ) -> Result { - dbg!(certificate); Ok(server::Auth::Accept) } diff --git a/russh/examples/ratatui_app.rs b/russh/examples/ratatui_app.rs index e223f20c..e1382d6e 100644 --- a/russh/examples/ratatui_app.rs +++ b/russh/examples/ratatui_app.rs @@ -2,13 +2,12 @@ use std::collections::HashMap; use std::sync::Arc; use async_trait::async_trait; -use rand_core::OsRng; use ratatui::backend::CrosstermBackend; use ratatui::layout::Rect; use ratatui::style::{Color, Style}; use ratatui::widgets::{Block, Borders, Clear, Paragraph}; use ratatui::Terminal; -use russh::keys::ssh_key::PublicKey; +use russh::keys::key::PublicKey; use russh::server::*; use russh::{Channel, ChannelId}; use tokio::sync::Mutex; @@ -105,9 +104,7 @@ impl AppServer { inactivity_timeout: Some(std::time::Duration::from_secs(3600)), auth_rejection_time: std::time::Duration::from_secs(3), auth_rejection_time_initial: Some(std::time::Duration::from_secs(0)), - keys: vec![ - russh_keys::PrivateKey::random(&mut OsRng, ssh_key::Algorithm::Ed25519).unwrap(), - ], + keys: vec![russh_keys::key::KeyPair::generate_ed25519()], ..Default::default() }; diff --git a/russh/examples/ratatui_shared_app.rs b/russh/examples/ratatui_shared_app.rs index b38cb412..ae320747 100644 --- a/russh/examples/ratatui_shared_app.rs +++ b/russh/examples/ratatui_shared_app.rs @@ -2,13 +2,12 @@ use std::collections::HashMap; use std::sync::Arc; use async_trait::async_trait; -use rand_core::OsRng; use ratatui::backend::CrosstermBackend; use ratatui::layout::Rect; use ratatui::style::{Color, Style}; use ratatui::widgets::{Block, Borders, Clear, Paragraph}; use ratatui::Terminal; -use russh::keys::ssh_key::PublicKey; +use russh::keys::key::PublicKey; use russh::server::*; use russh::{Channel, ChannelId}; use tokio::sync::Mutex; @@ -107,9 +106,7 @@ impl AppServer { inactivity_timeout: Some(std::time::Duration::from_secs(3600)), auth_rejection_time: std::time::Duration::from_secs(3), auth_rejection_time_initial: Some(std::time::Duration::from_secs(0)), - keys: vec![ - russh_keys::PrivateKey::random(&mut OsRng, ssh_key::Algorithm::Ed25519).unwrap(), - ], + keys: vec![russh_keys::key::KeyPair::generate_ed25519()], ..Default::default() }; diff --git a/russh/examples/sftp_client.rs b/russh/examples/sftp_client.rs index 48454a15..f45d69fc 100644 --- a/russh/examples/sftp_client.rs +++ b/russh/examples/sftp_client.rs @@ -1,11 +1,9 @@ -use std::sync::Arc; - use async_trait::async_trait; use log::{error, info, LevelFilter}; use russh::*; use russh_keys::*; -use russh_sftp::client::SftpSession; -use russh_sftp::protocol::OpenFlags; +use russh_sftp::{client::SftpSession, protocol::OpenFlags}; +use std::sync::Arc; use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; struct Client; @@ -16,7 +14,7 @@ impl client::Handler for Client { async fn check_server_key( &mut self, - server_public_key: &ssh_key::PublicKey, + server_public_key: &key::PublicKey, ) -> Result { info!("check_server_key: {:?}", server_public_key); Ok(true) diff --git a/russh/examples/sftp_server.rs b/russh/examples/sftp_server.rs index f4a00d4d..afea2455 100644 --- a/russh/examples/sftp_server.rs +++ b/russh/examples/sftp_server.rs @@ -1,14 +1,12 @@ -use std::collections::HashMap; -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::Duration; - use async_trait::async_trait; use log::{error, info, LevelFilter}; -use rand_core::OsRng; -use russh::server::{Auth, Msg, Server as _, Session}; -use russh::{Channel, ChannelId}; +use russh::{ + server::{Auth, Msg, Server as _, Session}, + Channel, ChannelId, +}; +use russh_keys::key::KeyPair; use russh_sftp::protocol::{File, FileAttributes, Handle, Name, Status, StatusCode, Version}; +use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; use tokio::sync::Mutex; #[derive(Clone)] @@ -53,7 +51,7 @@ impl russh::server::Handler for SshSession { async fn auth_publickey( &mut self, user: &str, - public_key: &russh_keys::ssh_key::PublicKey, + public_key: &russh_keys::key::PublicKey, ) -> Result { info!("credentials: {}, {:?}", user, public_key); Ok(Auth::Accept) @@ -181,9 +179,7 @@ async fn main() { let config = russh::server::Config { auth_rejection_time: Duration::from_secs(3), auth_rejection_time_initial: Some(Duration::from_secs(0)), - keys: vec![ - russh_keys::PrivateKey::random(&mut OsRng, ssh_key::Algorithm::Ed25519).unwrap(), - ], + keys: vec![KeyPair::generate_ed25519()], ..Default::default() }; diff --git a/russh/examples/test.rs b/russh/examples/test.rs index 6089766e..be5effc0 100644 --- a/russh/examples/test.rs +++ b/russh/examples/test.rs @@ -3,7 +3,6 @@ use std::sync::{Arc, Mutex}; use async_trait::async_trait; use log::debug; -use rand_core::OsRng; use russh::keys::*; use russh::server::{Auth, Msg, Server as _, Session}; use russh::*; @@ -15,7 +14,7 @@ async fn main() -> anyhow::Result<()> { config.auth_rejection_time = std::time::Duration::from_secs(3); config .keys - .push(russh_keys::PrivateKey::random(&mut OsRng, ssh_key::Algorithm::Ed25519).unwrap()); + .push(russh_keys::key::KeyPair::generate_ed25519()); let config = Arc::new(config); let mut sh = Server { clients: Arc::new(Mutex::new(HashMap::new())), @@ -75,11 +74,7 @@ impl server::Handler for Server { Ok(()) } - async fn auth_publickey( - &mut self, - _: &str, - _: &ssh_key::PublicKey, - ) -> Result { + async fn auth_publickey(&mut self, _: &str, _: &key::PublicKey) -> Result { Ok(server::Auth::Accept) } async fn data( diff --git a/russh/src/auth.rs b/russh/src/auth.rs index 0c9a9f8b..c1625d54 100644 --- a/russh/src/auth.rs +++ b/russh/src/auth.rs @@ -15,13 +15,12 @@ use std::sync::Arc; -use async_trait::async_trait; use bitflags::bitflags; -use ssh_key::{Certificate, PrivateKey}; +use ssh_key::Certificate; use thiserror::Error; use tokio::io::{AsyncRead, AsyncWrite}; -use crate::keys::encoding; +use crate::keys::{encoding, key}; use crate::CryptoVec; bitflags! { @@ -45,15 +44,11 @@ bitflags! { } } -#[async_trait] pub trait Signer: Sized { type Error: From; + type Future: futures::Future)> + Send; - async fn auth_publickey_sign( - &mut self, - key: &ssh_key::PublicKey, - to_sign: CryptoVec, - ) -> Result; + fn auth_publickey_sign(self, key: &key::PublicKey, to_sign: CryptoVec) -> Self::Future; } #[derive(Debug, Error)] @@ -64,18 +59,20 @@ pub enum AgentAuthError { Key(#[from] russh_keys::Error), } -#[async_trait] impl Signer for russh_keys::agent::client::AgentClient { type Error = AgentAuthError; - - async fn auth_publickey_sign( - &mut self, - key: &ssh_key::PublicKey, - to_sign: CryptoVec, - ) -> Result { - self.sign_request(key, to_sign).await.map_err(Into::into) + #[allow(clippy::type_complexity)] + type Future = std::pin::Pin< + Box)> + Send>, + >; + fn auth_publickey_sign(self, key: &key::PublicKey, to_sign: CryptoVec) -> Self::Future { + let fut = self.sign_request(key, to_sign); + futures::FutureExt::boxed(async move { + let (a, b) = fut.await; + (a, b.map_err(AgentAuthError::Key)) + }) } } @@ -86,14 +83,14 @@ pub enum Method { password: String, }, PublicKey { - key: Arc, + key: Arc, }, - OpenSshCertificate { - key: Arc, + OpenSSHCertificate { + key: Arc, cert: Certificate, }, FuturePublicKey { - key: ssh_key::PublicKey, + key: key::PublicKey, }, KeyboardInteractive { submethods: String, diff --git a/russh/src/cert.rs b/russh/src/cert.rs index 79f49ed1..9991e81b 100644 --- a/russh/src/cert.rs +++ b/russh/src/cert.rs @@ -1,48 +1,62 @@ -use core::str; +use ssh_encoding::Encode; +use ssh_key::{Algorithm, Certificate, EcdsaCurve}; -use ssh_encoding::Decode; -use ssh_key::public::KeyData; -use ssh_key::{Algorithm, Certificate, HashAlg, PublicKey}; +use crate::key::PubKey; +use crate::keys::encoding::Encoding; +use crate::negotiation::Named; +use crate::CryptoVec; -#[derive(Debug)] -pub(crate) enum PublicKeyOrCertificate { - PublicKey(PublicKey), - Certificate(Certificate), -} +/// OpenSSH certificate for DSA public key +const CERT_DSA: &str = "ssh-dss-cert-v01@openssh.com"; -impl PublicKeyOrCertificate { - pub fn decode(pubkey_algo: &[u8], buf: &[u8]) -> Result { - let mut reader = buf; - match Algorithm::new_certificate_ext(str::from_utf8(pubkey_algo)?) { - Ok(Algorithm::Other(_)) | Err(ssh_key::Error::Encoding(_)) => { - // Did not match a known cert algorithm - Ok(PublicKeyOrCertificate::PublicKey( - KeyData::decode(&mut reader)?.into(), - )) - } - _ => Ok(PublicKeyOrCertificate::Certificate(Certificate::decode( - &mut reader, - )?)), - } - } -} +/// OpenSSH certificate for ECDSA (NIST P-256) public key +const CERT_ECDSA_SHA2_P256: &str = "ecdsa-sha2-nistp256-cert-v01@openssh.com"; + +/// OpenSSH certificate for ECDSA (NIST P-384) public key +const CERT_ECDSA_SHA2_P384: &str = "ecdsa-sha2-nistp384-cert-v01@openssh.com"; + +/// OpenSSH certificate for ECDSA (NIST P-521) public key +const CERT_ECDSA_SHA2_P521: &str = "ecdsa-sha2-nistp521-cert-v01@openssh.com"; + +/// OpenSSH certificate for Ed25519 public key +const CERT_ED25519: &str = "ssh-ed25519-cert-v01@openssh.com"; -trait AlgorithmExt { - fn new_certificate_ext(algo: &str) -> Result - where - Self: Sized; +/// OpenSSH certificate with RSA public key +const CERT_RSA: &str = "ssh-rsa-cert-v01@openssh.com"; + +/// OpenSSH certificate for ECDSA (NIST P-256) U2F/FIDO security key +const CERT_SK_ECDSA_SHA2_P256: &str = "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com"; + +/// OpenSSH certificate for Ed25519 U2F/FIDO security key +const CERT_SK_SSH_ED25519: &str = "sk-ssh-ed25519-cert-v01@openssh.com"; + +/// None +const NONE: &str = "none"; + +impl PubKey for Certificate { + fn push_to(&self, buffer: &mut CryptoVec) { + let mut cert_encoded = Vec::new(); + let _ = self.encode(&mut cert_encoded); + + buffer.extend_ssh_string(&cert_encoded); + } } -impl AlgorithmExt for Algorithm { - fn new_certificate_ext(algo: &str) -> Result { - match algo { - "rsa-sha2-256-cert-v01@openssh.com" => Ok(Algorithm::Rsa { - hash: Some(HashAlg::Sha256), - }), - "rsa-sha2-512-cert-v01@openssh.com" => Ok(Algorithm::Rsa { - hash: Some(HashAlg::Sha512), - }), - x => Algorithm::new_certificate(x), +impl Named for Certificate { + fn name(&self) -> &'static str { + match self.algorithm() { + Algorithm::Dsa => CERT_DSA, + Algorithm::Ecdsa { curve } => match curve { + EcdsaCurve::NistP256 => CERT_ECDSA_SHA2_P256, + EcdsaCurve::NistP384 => CERT_ECDSA_SHA2_P384, + EcdsaCurve::NistP521 => CERT_ECDSA_SHA2_P521, + }, + Algorithm::Ed25519 => CERT_ED25519, + Algorithm::Rsa { .. } => CERT_RSA, + Algorithm::SkEcdsaSha2NistP256 => CERT_SK_ECDSA_SHA2_P256, + Algorithm::SkEd25519 => CERT_SK_SSH_ED25519, + Algorithm::Other(_) => NONE, + _ => NONE, } } } diff --git a/russh/src/client/encrypted.rs b/russh/src/client/encrypted.rs index a95bdef2..931af633 100644 --- a/russh/src/client/encrypted.rs +++ b/russh/src/client/encrypted.rs @@ -17,10 +17,9 @@ use std::convert::TryInto; use std::num::Wrapping; use log::{debug, error, info, trace, warn}; -use russh_keys::add_self_signature; -use crate::cert::PublicKeyOrCertificate; use crate::client::{Handler, Msg, Prompt, Reply, Session}; +use crate::key::PubKey; use crate::keys::encoding::{Encoding, Reader}; use crate::keys::key::parse_public_key; use crate::negotiation::{Named, Select}; @@ -202,7 +201,7 @@ impl Session { }; let len = enc.write.len(); #[allow(clippy::indexing_slicing)] // length checked - if enc.write_auth_request(&self.common.auth_user, meth)? { + if enc.write_auth_request(&self.common.auth_user, meth) { debug!("enc: {:?}", &enc.write[len..]); enc.state = EncryptedState::WaitingAuthRequest(auth_request) } @@ -331,7 +330,7 @@ impl Session { &mut self.common.buffer, )? } - Some(auth_method @ auth::Method::OpenSshCertificate { .. }) => { + Some(auth_method @ auth::Method::OpenSSHCertificate { .. }) => { self.common.buffer.clear(); enc.client_send_signature( &self.common.auth_user, @@ -344,9 +343,9 @@ impl Session { self.common.buffer.clear(); let i = enc.client_make_to_sign( &self.common.auth_user, - &PublicKeyOrCertificate::PublicKey(key.clone()), + &key, &mut self.common.buffer, - )?; + ); let len = self.common.buffer.len(); let buf = std::mem::replace(&mut self.common.buffer, CryptoVec::new()); @@ -678,7 +677,8 @@ impl Session { match r.read_string() { Ok(key) => { let key2 = <&[u8]>::clone(&key); - let key = parse_public_key(key).map_err(crate::Error::from); + let key = + parse_public_key(key, None).map_err(crate::Error::from); match key { Ok(key) => keys.push(key), Err(err) => { @@ -924,11 +924,7 @@ impl Session { channel } - pub(crate) fn write_auth_request_if_needed( - &mut self, - user: &str, - meth: auth::Method, - ) -> Result { + pub(crate) fn write_auth_request_if_needed(&mut self, user: &str, meth: auth::Method) -> bool { let mut is_waiting = false; if let Some(ref mut enc) = self.common.encrypted { is_waiting = match enc.state { @@ -955,24 +951,20 @@ impl Session { is_waiting ); if is_waiting { - enc.write_auth_request(user, &meth)?; + enc.write_auth_request(user, &meth); } } self.common.auth_user.clear(); self.common.auth_user.push_str(user); self.common.auth_method = Some(meth); - Ok(is_waiting) + is_waiting } } impl Encrypted { - fn write_auth_request( - &mut self, - user: &str, - auth_method: &auth::Method, - ) -> Result { + fn write_auth_request(&mut self, user: &str, auth_method: &auth::Method) -> bool { // The server is waiting for our USERAUTH_REQUEST. - Ok(push_packet!(self.write, { + push_packet!(self.write, { self.write.push(msg::USERAUTH_REQUEST); match *auth_method { @@ -996,23 +988,20 @@ impl Encrypted { self.write.extend_ssh_string(b"publickey"); self.write.push(0); // This is a probe - debug!("write_auth_request: key - {:?}", key.algorithm()); - self.write - .extend_ssh_string(key.algorithm().as_str().as_bytes()); - self.write - .extend_ssh_string(key.public_key().to_bytes()?.as_slice()); + debug!("write_auth_request: key - {:?}", key.name()); + self.write.extend_ssh_string(key.name().as_bytes()); + key.push_to(&mut self.write); true } - auth::Method::OpenSshCertificate { ref cert, .. } => { + auth::Method::OpenSSHCertificate { ref cert, .. } => { self.write.extend_ssh_string(user.as_bytes()); self.write.extend_ssh_string(b"ssh-connection"); self.write.extend_ssh_string(b"publickey"); self.write.push(0); // This is a probe - debug!("write_auth_request: cert - {:?}", cert.algorithm()); - self.write - .extend_ssh_string(cert.algorithm().to_certificate_type().as_bytes()); - self.write.extend_ssh_string(cert.to_bytes()?.as_slice()); + debug!("write_auth_request: cert - {:?}", cert.name()); + self.write.extend_ssh_string(cert.name().as_bytes()); + cert.push_to(&mut self.write); true } auth::Method::FuturePublicKey { ref key, .. } => { @@ -1021,10 +1010,8 @@ impl Encrypted { self.write.extend_ssh_string(b"publickey"); self.write.push(0); // This is a probe - self.write - .extend_ssh_string(key.algorithm().as_str().as_bytes()); - - self.write.extend_ssh_string(key.to_bytes()?.as_slice()); + self.write.extend_ssh_string(key.name().as_bytes()); + key.push_to(&mut self.write); true } auth::Method::KeyboardInteractive { ref submethods } => { @@ -1037,15 +1024,15 @@ impl Encrypted { true } } - })) + }) } - fn client_make_to_sign( + fn client_make_to_sign( &mut self, user: &str, - key: &PublicKeyOrCertificate, + key: &Key, buffer: &mut CryptoVec, - ) -> Result { + ) -> usize { buffer.clear(); buffer.extend_ssh_string(self.session_id.as_ref()); @@ -1055,18 +1042,9 @@ impl Encrypted { buffer.extend_ssh_string(b"ssh-connection"); buffer.extend_ssh_string(b"publickey"); buffer.push(1); - - match key { - PublicKeyOrCertificate::Certificate(cert) => { - buffer.extend_ssh_string(cert.name().as_ref().as_bytes()); - buffer.extend_ssh_string(cert.to_bytes()?.as_slice()); - } - PublicKeyOrCertificate::PublicKey(key) => { - buffer.extend_ssh_string(key.name().as_ref().as_bytes()); - buffer.extend_ssh_string(key.to_bytes()?.as_slice()); - } - } - Ok(i0) + buffer.extend_ssh_string(key.name().as_bytes()); // TODO + key.push_to(buffer); + i0 } fn client_send_signature( @@ -1077,29 +1055,18 @@ impl Encrypted { ) -> Result<(), crate::Error> { match method { auth::Method::PublicKey { ref key, .. } => { - let i0 = self.client_make_to_sign( - user, - &PublicKeyOrCertificate::PublicKey(key.public_key().clone()), - buffer, - )?; + let i0 = self.client_make_to_sign(user, key.as_ref(), buffer); // Extend with self-signature. - - add_self_signature(&**key, buffer)?; - + key.add_self_signature(buffer)?; push_packet!(self.write, { #[allow(clippy::indexing_slicing)] // length checked self.write.extend(&buffer[i0..]); }) } - auth::Method::OpenSshCertificate { ref key, ref cert } => { - let i0 = self.client_make_to_sign( - user, - &PublicKeyOrCertificate::Certificate(cert.clone()), - buffer, - )?; + auth::Method::OpenSSHCertificate { ref key, ref cert } => { + let i0 = self.client_make_to_sign(user, cert, buffer); // Extend with self-signature. - add_self_signature(&**key, buffer)?; - + key.add_self_signature(buffer)?; push_packet!(self.write, { #[allow(clippy::indexing_slicing)] // length checked self.write.extend(&buffer[i0..]); diff --git a/russh/src/client/mod.rs b/russh/src/client/mod.rs index bdef67d7..7082492b 100644 --- a/russh/src/client/mod.rs +++ b/russh/src/client/mod.rs @@ -45,9 +45,7 @@ use async_trait::async_trait; use futures::task::{Context, Poll}; use futures::Future; use log::{debug, error, info, trace}; -use russh_keys::encoding::Encoding; -use signature::Verifier; -use ssh_key::{Certificate, PrivateKey, PublicKey, Signature}; +use ssh_key::Certificate; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadHalf, WriteHalf}; use tokio::pin; use tokio::sync::mpsc::{ @@ -57,8 +55,9 @@ use tokio::sync::{oneshot, Mutex}; use crate::channels::{Channel, ChannelMsg, ChannelRef}; use crate::cipher::{self, clear, CipherPair, OpeningKey}; +use crate::key::PubKey; use crate::keys::encoding::Reader; -use crate::keys::key::parse_public_key; +use crate::keys::key::{self, parse_public_key, PublicKey, SignatureHash}; use crate::session::{ CommonSession, EncryptedState, Exchange, GlobalRequestResponse, Kex, KexDhDone, KexInit, NewKeys, @@ -107,7 +106,7 @@ enum Reply { AuthFailure, ChannelOpenFailure, SignRequest { - key: ssh_key::PublicKey, + key: key::PublicKey, data: CryptoVec, }, AuthInfoRequest { @@ -350,7 +349,7 @@ impl Handle { pub async fn authenticate_publickey>( &mut self, user: U, - key: Arc, + key: Arc, ) -> Result { let user = user.into(); self.sender @@ -367,14 +366,14 @@ impl Handle { pub async fn authenticate_openssh_cert>( &mut self, user: U, - key: Arc, + key: Arc, cert: Certificate, ) -> Result { let user = user.into(); self.sender .send(Msg::Authenticate { user, - method: auth::Method::OpenSshCertificate { key, cert }, + method: auth::Method::OpenSSHCertificate { key, cert }, }) .await .map_err(|_| crate::Error::SendError)?; @@ -385,12 +384,12 @@ impl Handle { /// [`Signer`][auth::Signer] trait. Currently, this crate only provides an /// implementation for an [SSH /// agent][russh_keys::agent::client::AgentClient]. - pub async fn authenticate_agent, S: auth::Signer>( + pub async fn authenticate_future, S: auth::Signer>( &mut self, user: U, - key: ssh_key::PublicKey, - signer: &mut S, - ) -> Result { + key: key::PublicKey, + mut future: S, + ) -> (S, Result) { let user = user.into(); if self .sender @@ -401,24 +400,25 @@ impl Handle { .await .is_err() { - return Err((crate::SendError {}).into()); + return (future, Err((crate::SendError {}).into())); } loop { let reply = self.receiver.recv().await; match reply { - Some(Reply::AuthSuccess) => return Ok(true), - Some(Reply::AuthFailure) => return Ok(false), + Some(Reply::AuthSuccess) => return (future, Ok(true)), + Some(Reply::AuthFailure) => return (future, Ok(false)), Some(Reply::SignRequest { key, data }) => { - let data = signer.auth_publickey_sign(&key, data).await; + let (f, data) = future.auth_publickey_sign(&key, data).await; + future = f; let data = match data { Ok(data) => data, - Err(e) => return Err(e), + Err(e) => return (future, Err(e)), }; if self.sender.send(Msg::Signed { data }).await.is_err() { - return Err((crate::SendError {}).into()); + return (future, Err((crate::SendError {}).into())); } } - None => return Ok(false), + None => return (future, Ok(false)), _ => {} } } @@ -1072,7 +1072,7 @@ impl Session { fn handle_msg(&mut self, msg: Msg) -> Result<(), crate::Error> { match msg { Msg::Authenticate { user, method } => { - self.write_auth_request_if_needed(&user, method)?; + self.write_auth_request_if_needed(&user, method); } Msg::Signed { .. } => {} Msg::AuthInfoResponse { .. } => {} @@ -1304,7 +1304,11 @@ impl KexDhDone { ) -> Result { let mut reader = buf.reader(1); let pubkey = reader.read_string().map_err(crate::Error::from)?; // server public key. - let pubkey = parse_public_key(pubkey).map_err(crate::Error::from)?; + let pubkey = parse_public_key( + pubkey, + SignatureHash::from_rsa_hostkey_algo(self.names.key.0.as_bytes()), + ) + .map_err(crate::Error::from)?; debug!("server_public_Key: {:?}", pubkey); if !rekey { let check = handler.check_server_key(&pubkey).await?; @@ -1325,7 +1329,7 @@ impl KexDhDone { debug!("kexdhdone.exchange = {:?}", self.exchange); let mut pubkey_vec = CryptoVec::new(); - pubkey_vec.extend_ssh_string(&pubkey.to_bytes().map_err(crate::Error::from)?); + pubkey.push_to(&mut pubkey_vec); let hash = self.kex @@ -1338,12 +1342,9 @@ impl KexDhDone { debug!("sig_type: {:?}", sig_type); sig_reader.read_string().map_err(crate::Error::from)? }; + use crate::keys::key::Verify; debug!("signature: {:?}", signature); - let signature = Signature::new(pubkey.algorithm(), signature).map_err(|e| { - debug!("signature ctor failed: {e:?}"); - crate::Error::WrongServerSig - })?; - if Verifier::verify(&pubkey, hash.as_ref(), &signature).is_err() { + if !pubkey.verify_server_auth(hash.as_ref(), signature) { debug!("wrong server sig"); return Err(crate::Error::WrongServerSig.into()); } @@ -1542,7 +1543,7 @@ pub trait Handler: Sized + Send { #[allow(unused_variables)] async fn check_server_key( &mut self, - server_public_key: &ssh_key::PublicKey, + server_public_key: &key::PublicKey, ) -> Result { Ok(false) } diff --git a/russh/src/key.rs b/russh/src/key.rs new file mode 100644 index 00000000..b24fb176 --- /dev/null +++ b/russh/src/key.rs @@ -0,0 +1,77 @@ +// Copyright 2016 Pierre-Étienne Meunier +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +use crate::keys::encoding::*; +use crate::keys::key::*; +use crate::keys::{ec, protocol}; +use crate::CryptoVec; + +#[doc(hidden)] +pub trait PubKey { + fn push_to(&self, buffer: &mut CryptoVec); +} + +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::RSA { ref key, .. } => { + buffer.extend_wrapped(|buffer| { + buffer.extend_ssh_string(SSH_RSA.0.as_bytes()); + buffer.extend_ssh(&protocol::RsaPublicKey::from(key)); + }); + } + PublicKey::EC { ref key } => { + write_ec_public_key(buffer, key); + } + } + } +} + +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::RSA { ref key, .. } => { + buffer.extend_wrapped(|buffer| { + buffer.extend_ssh_string(SSH_RSA.0.as_bytes()); + buffer.extend_ssh(&protocol::RsaPublicKey::from(key)); + }); + } + KeyPair::EC { ref key } => { + write_ec_public_key(buffer, &key.to_public_key()); + } + } + } +} + +pub(crate) fn write_ec_public_key(buf: &mut CryptoVec, key: &ec::PublicKey) { + let algorithm = key.algorithm().as_bytes(); + let ident = key.ident().as_bytes(); + let q = key.to_sec1_bytes(); + + buf.push_u32_be((algorithm.len() + ident.len() + q.len() + 12) as u32); + buf.extend_ssh_string(algorithm); + buf.extend_ssh_string(ident); + buf.extend_ssh_string(&q); +} diff --git a/russh/src/lib.rs b/russh/src/lib.rs index 0360eaf4..fed29b36 100644 --- a/russh/src/lib.rs +++ b/russh/src/lib.rs @@ -102,7 +102,6 @@ mod tests; mod auth; -mod cert; /// Cipher names pub mod cipher; /// Compression algorithm names @@ -115,6 +114,8 @@ pub mod mac; /// Re-export of the `russh-keys` crate. pub use russh_keys as keys; +mod cert; +mod key; mod msg; mod negotiation; mod ssh_read; @@ -295,6 +296,10 @@ pub enum Error { #[error(transparent)] Join(#[from] russh_util::runtime::JoinError), + #[error(transparent)] + #[cfg(feature = "openssl")] + Openssl(#[from] openssl::error::ErrorStack), + #[error(transparent)] Elapsed(#[from] tokio::time::error::Elapsed), @@ -303,12 +308,6 @@ pub enum Error { message_type: u8, sequence_number: usize, }, - - #[error("SshKey: {0}")] - SshKey(#[from] ssh_key::Error), - - #[error("SshEncoding: {0}")] - SshEncoding(#[from] ssh_encoding::Error), } pub(crate) fn strict_kex_violation(message_type: u8, sequence_number: usize) -> crate::Error { diff --git a/russh/src/negotiation.rs b/russh/src/negotiation.rs index b08aa324..2bec9f4a 100644 --- a/russh/src/negotiation.rs +++ b/russh/src/negotiation.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; // Copyright 2016 Pierre-Étienne Meunier // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,16 +13,16 @@ // See the License for the specific language governing permissions and // limitations under the License. // -use std::borrow::Cow; use std::str::from_utf8; use log::debug; use rand::RngCore; -use ssh_key::{Algorithm, Certificate, EcdsaCurve, HashAlg, PrivateKey, PublicKey}; use crate::cipher::CIPHERS; use crate::kex::{EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT, EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER}; use crate::keys::encoding::{Encoding, Reader}; +use crate::keys::key; +use crate::keys::key::{KeyPair, PublicKey}; #[cfg(not(target_arch = "wasm32"))] use crate::server::Config; use crate::{cipher, compression, kex, mac, msg, AlgorithmKind, CryptoVec, Error}; @@ -29,13 +30,13 @@ use crate::{cipher, compression, kex, mac, msg, AlgorithmKind, CryptoVec, Error} #[cfg(target_arch = "wasm32")] /// WASM-only stub pub struct Config { - keys: Vec, + keys: Vec, } #[derive(Debug, Clone)] pub struct Names { pub kex: kex::Name, - pub key: Algorithm, + pub key: key::Name, pub cipher: cipher::Name, pub client_mac: mac::Name, pub server_mac: mac::Name, @@ -51,7 +52,7 @@ pub struct Preferred { /// Preferred key exchange algorithms. pub kex: Cow<'static, [kex::Name]>, /// Preferred host & public key algorithms. - pub key: Cow<'static, [Algorithm]>, + pub key: Cow<'static, [key::Name]>, /// Preferred symmetric ciphers. pub cipher: Cow<'static, [cipher::Name]>, /// Preferred MAC algorithms. @@ -63,12 +64,12 @@ pub struct Preferred { impl Preferred { pub(crate) fn possible_host_key_algos_for_keys( &self, - available_host_keys: &[PrivateKey], - ) -> Vec { + available_host_keys: &[KeyPair], + ) -> Vec { self.key .iter() - .filter(|n| available_host_keys.iter().any(|k| k.algorithm() == **n)) - .cloned() + .filter(|n| available_host_keys.iter().any(|k| k.name() == n.0)) + .copied() .collect::>() } } @@ -113,23 +114,11 @@ impl Preferred { pub const DEFAULT: Preferred = Preferred { kex: Cow::Borrowed(SAFE_KEX_ORDER), key: Cow::Borrowed(&[ - Algorithm::Ed25519, - Algorithm::Ecdsa { - curve: EcdsaCurve::NistP256, - }, - Algorithm::Ecdsa { - curve: EcdsaCurve::NistP384, - }, - Algorithm::Ecdsa { - curve: EcdsaCurve::NistP521, - }, - Algorithm::Rsa { - hash: Some(HashAlg::Sha512), - }, - Algorithm::Rsa { - hash: Some(HashAlg::Sha256), - }, - Algorithm::Rsa { hash: None }, + key::ED25519, + key::ECDSA_SHA2_NISTP256, + key::ECDSA_SHA2_NISTP521, + key::RSA_SHA2_256, + key::RSA_SHA2_512, ]), cipher: Cow::Borrowed(CIPHER_ORDER), mac: Cow::Borrowed(HMAC_ORDER), @@ -152,32 +141,36 @@ impl Default for Preferred { } /// Named algorithms. -pub trait Named<'a> { +pub trait Named { /// The name of this algorithm. - fn name(&'a self) -> impl AsRef + 'a; + fn name(&self) -> &'static str; } -impl Named<'static> for () { - fn name(&'static self) -> impl AsRef + 'static { +impl Named for () { + fn name(&self) -> &'static str { "" } } -impl<'a> Named<'a> for PublicKey { - fn name(&'a self) -> impl AsRef + 'a { - self.algorithm() - } -} +use crate::keys::key::ED25519; -impl<'a> Named<'a> for PrivateKey { - fn name(&'a self) -> impl AsRef + 'a { - self.algorithm() +impl Named for PublicKey { + fn name(&self) -> &'static str { + match self { + PublicKey::Ed25519(_) => ED25519.0, + PublicKey::RSA { ref hash, .. } => hash.name().0, + PublicKey::EC { ref key } => key.algorithm(), + } } } -impl<'a> Named<'a> for Certificate { - fn name(&'a self) -> impl AsRef + 'a { - self.algorithm() +impl Named for KeyPair { + fn name(&self) -> &'static str { + match self { + KeyPair::Ed25519 { .. } => ED25519.0, + KeyPair::RSA { ref hash, .. } => hash.name().0, + KeyPair::EC { ref key } => key.algorithm(), + } } } @@ -200,7 +193,7 @@ pub(crate) trait Select { fn read_kex( buffer: &[u8], pref: &Preferred, - available_host_keys: Option<&[PrivateKey]>, + available_host_keys: Option<&[KeyPair]>, ) -> Result { let mut r = buffer.reader(17); @@ -425,7 +418,7 @@ pub fn write_kex( prefs .key .iter() - .filter(|algo| server_config.keys.iter().any(|k| k.algorithm() == **algo)), + .filter(|name| server_config.keys.iter().any(|k| k.name() == name.0)), ); } else { buf.extend_list(prefs.key.iter()); diff --git a/russh/src/server/encrypted.rs b/russh/src/server/encrypted.rs index 5073d5fd..60d89b2f 100644 --- a/russh/src/server/encrypted.rs +++ b/russh/src/server/encrypted.rs @@ -12,23 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. // -use core::str; use std::cell::RefCell; -use std::time::SystemTime; use auth::*; use byteorder::{BigEndian, ByteOrder}; -use cert::PublicKeyOrCertificate; use log::{debug, error, info, trace, warn}; use negotiation::Select; -use signature::Verifier; -use ssh_key::{Algorithm, PublicKey, Signature}; use tokio::time::Instant; use {msg, negotiation}; use super::super::*; use super::*; use crate::keys::encoding::{Encoding, Position, Reader}; +use crate::keys::key; +use crate::keys::key::Verify; use crate::msg::SSH_OPEN_ADMINISTRATIVELY_PROHIBITED; use crate::parsing::{ChannelOpenConfirmation, ChannelType, OpenChannelMessage}; @@ -407,42 +404,14 @@ impl Encrypted { } else { unreachable!() }; - let is_real = r.read_byte().map_err(crate::Error::from)?; let pubkey_algo = r.read_string().map_err(crate::Error::from)?; let pubkey_key = r.read_string().map_err(crate::Error::from)?; - - let key_or_cert = PublicKeyOrCertificate::decode(pubkey_algo, pubkey_key); - - // Parse the public key or certificate - match key_or_cert { - Ok(pk_or_cert) => { + debug!("algo: {:?}, key: {:?}", pubkey_algo, pubkey_key); + match key::PublicKey::parse(pubkey_algo, pubkey_key) { + Ok(mut pubkey) => { debug!("is_real = {:?}", is_real); - // Handle certificates specifically - let pubkey = match pk_or_cert { - PublicKeyOrCertificate::PublicKey(ref pk) => pk.clone(), - PublicKeyOrCertificate::Certificate(ref cert) => { - // Validate certificate expiration - let now = SystemTime::now(); - if now < cert.valid_after_time() || now > cert.valid_before_time() { - warn!("Certificate is expired or not yet valid"); - reject_auth_request(until, &mut self.write, auth_request).await; - return Ok(()); - } - - // Verify the certificate’s signature - if cert.verify_signature().is_err() { - warn!("Certificate signature is invalid"); - reject_auth_request(until, &mut self.write, auth_request).await; - return Ok(()); - } - - // Use certificate's public key for authentication - PublicKey::new(cert.public_key().clone(), "") - } - }; - if is_real != 0 { let pos0 = r.position; let sent_pk_ok = if let Some(CurrentRequest::PublicKey { sent_pk_ok, .. }) = @@ -454,18 +423,14 @@ impl Encrypted { }; let signature = r.read_string().map_err(crate::Error::from)?; + debug!("signature = {:?}", signature); let mut s = signature.reader(0); - let algo = s.read_string().map_err(crate::Error::from)?; - + let algo_ = s.read_string().map_err(crate::Error::from)?; + if let Some(hash) = key::SignatureHash::from_rsa_hostkey_algo(algo_) { + pubkey.set_algorithm(hash); + } + debug!("algo_: {:?}", algo_); let sig = s.read_string().map_err(crate::Error::from)?; - #[allow(clippy::indexing_slicing)] - let sig = Signature::new( - Algorithm::new(str::from_utf8(algo).map_err(crate::Error::from)?) - .map_err(crate::Error::from)?, - sig, - ) - .map_err(crate::Error::from)?; - #[allow(clippy::indexing_slicing)] // length checked let init = &buf[0..pos0]; @@ -479,7 +444,6 @@ impl Encrypted { } else { false }; - if is_valid { let session_id = self.session_id.as_ref(); #[allow(clippy::blocks_in_conditions)] @@ -488,18 +452,11 @@ impl Encrypted { buf.clear(); buf.extend_ssh_string(session_id); buf.extend(init); - - Verifier::verify(&pubkey, &buf, &sig).is_ok() + // Verify signature. + pubkey.verify_client_auth(&buf, sig) }) { debug!("signature verified"); - let auth = match pk_or_cert { - PublicKeyOrCertificate::PublicKey(ref pk) => { - handler.auth_publickey(user, pk).await? - } - PublicKeyOrCertificate::Certificate(ref cert) => { - handler.auth_openssh_certificate(user, cert).await? - } - }; + let auth = handler.auth_publickey(user, &pubkey).await?; if auth == Auth::Accept { server_auth_request_success(&mut self.write); @@ -562,9 +519,9 @@ impl Encrypted { Ok(()) } } - Err(ssh_key::Error::AlgorithmUnknown) - | Err(ssh_key::Error::AlgorithmUnsupported { .. }) - | Err(ssh_key::Error::CertificateValidation { .. }) => { + Err(russh_keys::Error::CouldNotReadKey) + | Err(russh_keys::Error::KeyIsCorrupt) + | Err(russh_keys::Error::UnsupportedKeyType { .. }) => { reject_auth_request(until, &mut self.write, auth_request).await; Ok(()) } diff --git a/russh/src/server/kex.rs b/russh/src/server/kex.rs index a278b2e4..6961e01f 100644 --- a/russh/src/server/kex.rs +++ b/russh/src/server/kex.rs @@ -1,11 +1,11 @@ use std::cell::RefCell; use log::debug; -use russh_keys::add_signature; use super::*; use crate::cipher::SealingKey; use crate::kex::KEXES; +use crate::key::PubKey; use crate::keys::encoding::{Encoding, Reader}; use crate::negotiation::Select; use crate::{msg, negotiation}; @@ -33,7 +33,7 @@ impl KexInit { } let mut key = 0; #[allow(clippy::indexing_slicing)] // length checked - while key < config.keys.len() && config.keys[key].algorithm() != algo.key { + while key < config.keys.len() && config.keys[key].name() != algo.key.as_ref() { key += 1 } let next_kex = if key < config.keys.len() { @@ -111,12 +111,7 @@ impl KexDh { debug!("server kexdhdone.exchange = {:?}", kexdhdone.exchange); let mut pubkey_vec = CryptoVec::new(); - pubkey_vec.extend_ssh_string( - config.keys[kexdhdone.key] - .public_key() - .to_bytes()? - .as_slice(), - ); + config.keys[kexdhdone.key].push_to(&mut pubkey_vec); let hash = kexdhdone.kex.compute_exchange_hash( &pubkey_vec, @@ -126,21 +121,14 @@ impl KexDh { debug!("exchange hash: {:?}", hash); buffer.clear(); buffer.push(msg::KEX_ECDH_REPLY); - buffer.extend_ssh_string( - config.keys[kexdhdone.key] - .public_key() - .to_bytes()? - .as_slice(), - ); + config.keys[kexdhdone.key].push_to(&mut buffer); // Server ephemeral buffer.extend_ssh_string(&kexdhdone.exchange.server_ephemeral); // Hash signature debug!("signing with key {:?}", kexdhdone.key); debug!("hash: {:?}", hash); debug!("key: {:?}", config.keys[kexdhdone.key]); - - add_signature(&config.keys[kexdhdone.key], &hash, &mut buffer)?; - + config.keys[kexdhdone.key].add_signature(&mut buffer, &hash)?; cipher.write(&buffer, write_buffer); cipher.write(&[msg::NEWKEYS], write_buffer); Ok(hash) diff --git a/russh/src/server/mod.rs b/russh/src/server/mod.rs index fb8d333d..e345d86a 100644 --- a/russh/src/server/mod.rs +++ b/russh/src/server/mod.rs @@ -39,12 +39,12 @@ use async_trait::async_trait; use futures::future::Future; use log::{debug, error}; use russh_util::runtime::JoinHandle; -use ssh_key::{Certificate, PrivateKey}; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use tokio::net::{TcpListener, ToSocketAddrs}; use tokio::pin; use crate::cipher::{clear, CipherPair, OpeningKey}; +use crate::keys::key; use crate::session::*; use crate::ssh_read::*; use crate::sshbuffer::*; @@ -71,7 +71,7 @@ pub struct Config { /// OpenSSH clients will send an initial "none" auth to probe for authentication methods. pub auth_rejection_time_initial: Option, /// The server's keys. The first key pair in the client's preference order will be chosen. - pub keys: Vec, + pub keys: Vec, /// The bytes and time limits before key re-exchange. pub limits: Limits, /// The initial size of a channel (used for flow control). @@ -206,7 +206,7 @@ pub trait Handler: Sized { async fn auth_publickey_offered( &mut self, user: &str, - public_key: &ssh_key::PublicKey, + public_key: &key::PublicKey, ) -> Result { Ok(Auth::Accept) } @@ -221,24 +221,7 @@ pub trait Handler: Sized { async fn auth_publickey( &mut self, user: &str, - public_key: &ssh_key::PublicKey, - ) -> Result { - Ok(Auth::Reject { - proceed_with_methods: None, - }) - } - - /// Check authentication using an OpenSSH certificate. This method - /// is called after the signature has been verified and key - /// ownership has been confirmed. - /// Russh guarantees that rejection happens in constant time - /// `config.auth_rejection_time`, except if this method takes more - /// time than that. - #[allow(unused_variables)] - async fn auth_openssh_certificate( - &mut self, - user: &str, - certificate: &Certificate, + public_key: &key::PublicKey, ) -> Result { Ok(Auth::Reject { proceed_with_methods: None, diff --git a/russh/src/tests.rs b/russh/src/tests.rs index fc22c127..d768b7f5 100644 --- a/russh/src/tests.rs +++ b/russh/src/tests.rs @@ -10,8 +10,6 @@ mod compress { use async_trait::async_trait; use log::debug; - use rand_core::OsRng; - use ssh_key::PrivateKey; use super::server::{Server as _, Session}; use super::*; @@ -21,14 +19,14 @@ mod compress { async fn compress_local_test() { let _ = env_logger::try_init(); - let client_key = PrivateKey::random(&mut OsRng, ssh_key::Algorithm::Ed25519).unwrap(); + let client_key = russh_keys::key::KeyPair::generate_ed25519(); let mut config = server::Config::default(); config.preferred = Preferred::COMPRESSED; config.inactivity_timeout = None; // Some(std::time::Duration::from_secs(3)); config.auth_rejection_time = std::time::Duration::from_secs(3); config .keys - .push(PrivateKey::random(&mut OsRng, ssh_key::Algorithm::Ed25519).unwrap()); + .push(russh_keys::key::KeyPair::generate_ed25519()); let config = Arc::new(config); let mut sh = Server { clients: Arc::new(Mutex::new(HashMap::new())), @@ -104,7 +102,7 @@ mod compress { async fn auth_publickey( &mut self, _: &str, - _: &russh_keys::ssh_key::PublicKey, + _: &russh_keys::key::PublicKey, ) -> Result { debug!("auth_publickey"); Ok(server::Auth::Accept) @@ -129,7 +127,7 @@ mod compress { async fn check_server_key( &mut self, - _server_public_key: &russh_keys::ssh_key::PublicKey, + _server_public_key: &russh_keys::key::PublicKey, ) -> Result { // println!("check_server_key: {:?}", server_public_key); Ok(true) @@ -139,9 +137,7 @@ mod compress { mod channels { use async_trait::async_trait; - use rand_core::OsRng; use server::Session; - use ssh_key::PrivateKey; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use super::*; @@ -166,13 +162,13 @@ mod channels { let _ = env_logger::try_init(); - let client_key = PrivateKey::random(&mut OsRng, ssh_key::Algorithm::Ed25519).unwrap(); + let client_key = russh_keys::key::KeyPair::generate_ed25519(); let mut config = server::Config::default(); config.inactivity_timeout = None; config.auth_rejection_time = std::time::Duration::from_secs(3); config .keys - .push(PrivateKey::random(&mut OsRng, ssh_key::Algorithm::Ed25519).unwrap()); + .push(russh_keys::key::KeyPair::generate_ed25519()); let config = Arc::new(config); let socket = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = socket.local_addr().unwrap(); @@ -223,7 +219,7 @@ mod channels { async fn check_server_key( &mut self, - _server_public_key: &russh_keys::ssh_key::PublicKey, + _server_public_key: &russh_keys::key::PublicKey, ) -> Result { Ok(true) } @@ -259,7 +255,7 @@ mod channels { async fn auth_publickey( &mut self, _: &str, - _: &russh_keys::ssh_key::PublicKey, + _: &russh_keys::key::PublicKey, ) -> Result { Ok(server::Auth::Accept) } @@ -305,7 +301,7 @@ mod channels { async fn check_server_key( &mut self, - _server_public_key: &russh_keys::ssh_key::PublicKey, + _server_public_key: &russh_keys::key::PublicKey, ) -> Result { Ok(true) } @@ -332,7 +328,7 @@ mod channels { async fn auth_publickey( &mut self, _: &str, - _: &russh_keys::ssh_key::PublicKey, + _: &russh_keys::key::PublicKey, ) -> Result { Ok(server::Auth::Accept) } @@ -401,7 +397,7 @@ mod channels { async fn check_server_key( &mut self, - _server_public_key: &russh_keys::ssh_key::PublicKey, + _server_public_key: &russh_keys::key::PublicKey, ) -> Result { Ok(true) } @@ -418,7 +414,7 @@ mod channels { async fn auth_publickey( &mut self, _: &str, - _: &russh_keys::ssh_key::PublicKey, + _: &russh_keys::key::PublicKey, ) -> Result { Ok(server::Auth::Accept) } diff --git a/russh/tests/test_data_stream.rs b/russh/tests/test_data_stream.rs index 312abda3..f936893b 100644 --- a/russh/tests/test_data_stream.rs +++ b/russh/tests/test_data_stream.rs @@ -2,10 +2,9 @@ use std::net::{SocketAddr, TcpListener, TcpStream}; use std::sync::Arc; use rand::RngCore; -use rand_core::OsRng; +use russh::keys::key; use russh::server::{self, Auth, Msg, Server as _, Session}; use russh::{client, Channel}; -use ssh_key::PrivateKey; use tokio::io::{AsyncReadExt, AsyncWriteExt}; pub const WINDOW_SIZE: u32 = 8 * 2048; @@ -31,7 +30,7 @@ async fn test_reader_and_writer() -> Result<(), anyhow::Error> { async fn stream(addr: SocketAddr, data: &[u8]) -> Result<(), anyhow::Error> { let config = Arc::new(client::Config::default()); - let key = Arc::new(PrivateKey::random(&mut OsRng, ssh_key::Algorithm::Ed25519).unwrap()); + let key = Arc::new(russh_keys::key::KeyPair::generate_ed25519()); let mut session = russh::client::connect(config, addr, Client).await?; let mut channel = match session.authenticate_publickey("user", key).await { @@ -85,7 +84,7 @@ struct Server; impl Server { async fn run(addr: SocketAddr) { let config = Arc::new(server::Config { - keys: vec![PrivateKey::random(&mut OsRng, ssh_key::Algorithm::Ed25519).unwrap()], + keys: vec![russh_keys::key::KeyPair::generate_ed25519()], window_size: WINDOW_SIZE, ..Default::default() }); @@ -107,11 +106,7 @@ impl russh::server::Server for Server { impl russh::server::Handler for Server { type Error = anyhow::Error; - async fn auth_publickey( - &mut self, - _: &str, - _: &ssh_key::PublicKey, - ) -> Result { + async fn auth_publickey(&mut self, _: &str, _: &key::PublicKey) -> Result { Ok(Auth::Accept) } @@ -141,7 +136,7 @@ struct Client; impl russh::client::Handler for Client { type Error = anyhow::Error; - async fn check_server_key(&mut self, _: &ssh_key::PublicKey) -> Result { + async fn check_server_key(&mut self, _: &key::PublicKey) -> Result { Ok(true) } }