diff --git a/Cargo.toml b/Cargo.toml index 6d40fb53..93e9216e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,4 @@ russh-cryptovec = { path = "cryptovec" } russh-config = { path = "russh-config" } [workspace.dependencies] -ssh-key = { version = "0.6.6", features = ["ed25519", "rsa"] } \ No newline at end of file +ssh-key = { version = "0.6", features = ["ed25519", "rsa", "encryption"] } diff --git a/russh-keys/Cargo.toml b/russh-keys/Cargo.toml index 91b9af2c..6fbc21df 100644 --- a/russh-keys/Cargo.toml +++ b/russh-keys/Cargo.toml @@ -67,6 +67,7 @@ serde = { version = "1.0", features = ["derive"] } sha1 = { version = "0.10", features = ["oid"] } sha2 = { version = "0.10", features = ["oid"] } spki = "0.7" +ssh-encoding = "0.2" ssh-key = { workspace = true } thiserror = "1.0" tokio = { version = "1.17.0", features = ["io-util", "rt-multi-thread", "time", "net"] } diff --git a/russh-keys/src/agent/client.rs b/russh-keys/src/agent/client.rs index f247e4b3..c44035a9 100644 --- a/russh-keys/src/agent/client.rs +++ b/russh-keys/src/agent/client.rs @@ -1,7 +1,7 @@ use std::convert::TryFrom; use byteorder::{BigEndian, ByteOrder}; -use log::{debug, info}; +use log::debug; use russh_cryptovec::CryptoVec; use tokio; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; @@ -249,34 +249,12 @@ impl AgentClient { let mut r = self.buf.reader(1); let n = r.read_u32()?; for _ in 0..n { - let key = r.read_string()?; - let _ = r.read_string()?; - let mut r = key.reader(0); - let t = r.read_string()?; - debug!("t = {:?}", std::str::from_utf8(t)); - match t { - b"ssh-rsa" => keys.push(key::PublicKey::new_rsa_with_hash( - &r.read_ssh()?, - SignatureHash::SHA2_512, - )?), - b"ssh-ed25519" => keys.push(PublicKey::Ed25519( - ed25519_dalek::VerifyingKey::try_from(r.read_string()?)?, - )), - crate::KEYTYPE_ECDSA_SHA2_NISTP256 - | crate::KEYTYPE_ECDSA_SHA2_NISTP384 - | crate::KEYTYPE_ECDSA_SHA2_NISTP521 => { - let curve = r.read_string()?; - let sec1_bytes = r.read_string()?; - let key = crate::ec::PublicKey::from_sec1_bytes(t, sec1_bytes)?; - if curve != key.ident().as_bytes() { - return Err(Error::CouldNotReadKey); - } - keys.push(PublicKey::EC { key }) - } - t => { - info!("Unsupported key type: {:?}", std::str::from_utf8(t)) - } - } + let key_blob = r.read_string()?; + let _comment = r.read_string()?; + keys.push(key::parse_public_key( + key_blob, + Some(SignatureHash::SHA2_512), + )?); } } diff --git a/russh-keys/src/agent/server.rs b/russh-keys/src/agent/server.rs index b9eba687..be89509a 100644 --- a/russh-keys/src/agent/server.rs +++ b/russh-keys/src/agent/server.rs @@ -15,8 +15,7 @@ use {std, tokio}; use super::{msg, Constraint}; use crate::encoding::{Encoding, Position, Reader}; -use crate::key::SignatureHash; -use crate::{key, protocol, Error}; +use crate::{key, Error}; #[derive(Clone)] #[allow(clippy::type_complexity)] @@ -251,44 +250,22 @@ impl Result { - let pos0 = r.position; - let t = r.read_string()?; - let (blob, key) = match t { - b"ssh-ed25519" => { - let _public = r.read_string()?; - let pos1 = r.position; - let concat = r.read_string()?; - let _comment = r.read_string()?; - #[allow(clippy::indexing_slicing)] // length checked before - let secret = ed25519_dalek::SigningKey::try_from( - concat.get(..32).ok_or(Error::KeyIsCorrupt)?, - ) - .map_err(|_| Error::KeyIsCorrupt)?; + let (blob, key_pair) = { + use ssh_encoding::{Decode, Encode}; - writebuf.push(msg::SUCCESS); + 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)?; - #[allow(clippy::indexing_slicing)] // positions checked before - (self.buf[pos0..pos1].to_vec(), key::KeyPair::Ed25519(secret)) - } - b"ssh-rsa" => { - let key = - key::KeyPair::new_rsa_with_hash(&r.read_ssh()?, None, SignatureHash::SHA2_256)?; - - let mut blob = Vec::new(); - if let key::KeyPair::RSA { ref key, .. } = key { - let public = protocol::RsaPublicKey::from(key); - blob.extend_ssh_string(b"ssh-rsa"); - blob.extend_ssh(&public); - } else { - return Err(Error::KeyIsCorrupt); - }; + let mut blob = Vec::new(); + private_key.public_key().key_data().encode(&mut blob)?; - writebuf.push(msg::SUCCESS); - - (blob, key) - } - _ => return Ok(false), + (blob, key_pair) }; + writebuf.push(msg::SUCCESS); let mut w = self.keys.0.write().or(Err(Error::AgentFailure))?; let now = SystemTime::now(); if constrained { @@ -318,9 +295,9 @@ impl: Sized + 'a { /// Read the value from a position. fn read_ssh(pos: &mut Position<'a>) -> Result; } + +impl<'a> ssh_encoding::Reader for Position<'a> { + fn read<'o>(&mut self, out: &'o mut [u8]) -> ssh_encoding::Result<&'o [u8]> { + out.copy_from_slice( + self.s + .get(self.position..(self.position + out.len())) + .ok_or(ssh_encoding::Error::Length)?, + ); + self.position += out.len(); + Ok(out) + } + + fn remaining_len(&self) -> usize { + self.s.len() - self.position + } +} diff --git a/russh-keys/src/format/openssh.rs b/russh-keys/src/format/openssh.rs index 91ccd063..c27a1bc3 100644 --- a/russh-keys/src/format/openssh.rs +++ b/russh-keys/src/format/openssh.rs @@ -1,163 +1,124 @@ use std::convert::TryFrom; -use aes::cipher::block_padding::NoPadding; -use aes::cipher::{BlockDecryptMut, KeyIvInit, StreamCipher}; -use bcrypt_pbkdf; -use ctr::Ctr64BE; +use crate::{ + ec, + key::{KeyPair, PublicKey, SignatureHash}, + protocol, Error, +}; -use crate::encoding::Reader; -use crate::{ec, key, Error, KEYTYPE_ED25519, KEYTYPE_RSA}; +use ssh_key::{ + private::{EcdsaKeypair, Ed25519Keypair, KeypairData, PrivateKey, RsaKeypair, RsaPrivateKey}, + public::{Ed25519PublicKey, KeyData, RsaPublicKey}, + Algorithm, HashAlg, +}; /// 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 { - if matches!(secret.get(0..15), Some(b"openssh-key-v1\0")) { - let mut position = secret.reader(15); - - let ciphername = position.read_string()?; - let kdfname = position.read_string()?; - let kdfoptions = position.read_string()?; - - let nkeys = position.read_u32()?; +pub fn decode_openssh(secret: &[u8], password: Option<&str>) -> Result { + let pk = PrivateKey::from_bytes(secret)?; + KeyPair::try_from(&match password { + Some(password) => pk.decrypt(password)?, + None => pk, + }) +} - // Read all public keys - for _ in 0..nkeys { - position.read_string()?; - } +impl TryFrom<&PrivateKey> for KeyPair { + type Error = Error; - // Read all secret keys - let secret_ = position.read_string()?; - let secret = decrypt_secret_key(ciphername, kdfname, kdfoptions, password, secret_)?; - let mut position = secret.reader(0); - let _check0 = position.read_u32()?; - let _check1 = position.read_u32()?; - #[allow(clippy::never_loop)] - for _ in 0..nkeys { - // TODO check: never really loops beyond the first key - let key_type = position.read_string()?; - if key_type == KEYTYPE_ED25519 { - let pubkey = position.read_string()?; - let seckey = position.read_string()?; - let _comment = position.read_string()?; - if Some(pubkey) != seckey.get(32..) { + 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); } - let secret = ed25519_dalek::SigningKey::try_from( - seckey.get(..32).ok_or(Error::KeyIsCorrupt)?, - )?; - return Ok(key::KeyPair::Ed25519(secret)); - } else if key_type == KEYTYPE_RSA { - return key::KeyPair::new_rsa_with_hash( - &position.read_ssh()?, - None, - key::SignatureHash::SHA2_512, - ); - } else if key_type == crate::KEYTYPE_ECDSA_SHA2_NISTP256 - || key_type == crate::KEYTYPE_ECDSA_SHA2_NISTP384 - || key_type == crate::KEYTYPE_ECDSA_SHA2_NISTP521 - { - let ident = position.read_string()?; - let pubkey = position.read_string()?; - let seckey = position.read_mpint()?; - let _comment = position.read_string()?; - - let key = ec::PrivateKey::new_from_secret_scalar(key_type, seckey)?; - - if ident != key.ident().as_bytes() { - return Err(Error::CouldNotReadKey); - } - - let pubkey = ec::PublicKey::from_sec1_bytes(key_type, pubkey)?; - if pubkey != key.to_public_key() { - return Err(Error::CouldNotReadKey); + 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); } - - return Ok(key::KeyPair::EC { key }); - } else { - return Err(Error::UnsupportedKeyType { - key_type_string: String::from_utf8(key_type.to_vec()) - .unwrap_or_else(|_| format!("{key_type:?}")), - key_type_raw: key_type.to_vec(), - }); + 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(), + }), } - Err(Error::CouldNotReadKey) - } else { - Err(Error::CouldNotReadKey) } } -use aes::*; +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 decrypt_secret_key( - ciphername: &[u8], - kdfname: &[u8], - kdfoptions: &[u8], - password: Option<&str>, - secret_key: &[u8], -) -> Result, Error> { - if kdfname == b"none" { - if password.is_none() { - Ok(secret_key.to_vec()) - } else { - Err(Error::CouldNotReadKey) + 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(), + }), } - } else if let Some(password) = password { - let mut key = [0; 48]; - let n = match ciphername { - b"aes128-cbc" | b"aes128-ctr" => 32, - b"aes256-cbc" | b"aes256-ctr" => 48, - _ => return Err(Error::CouldNotReadKey), - }; - match kdfname { - b"bcrypt" => { - let mut kdfopts = kdfoptions.reader(0); - let salt = kdfopts.read_string()?; - let rounds = kdfopts.read_u32()?; - #[allow(clippy::unwrap_used)] // parameters are static - #[allow(clippy::indexing_slicing)] // output length is static - match bcrypt_pbkdf::bcrypt_pbkdf(password, salt, rounds, &mut key[..n]) { - Err(bcrypt_pbkdf::Error::InvalidParamLen) => return Err(Error::KeyIsEncrypted), - e => e.unwrap(), - } - } - _kdfname => { - return Err(Error::CouldNotReadKey); - } - }; - let (key, iv) = key.split_at(n - 16); + } +} - let mut dec = secret_key.to_vec(); - dec.resize(dec.len() + 32, 0u8); - match ciphername { - b"aes128-cbc" => { - #[allow(clippy::unwrap_used)] // parameters are static - let cipher = cbc::Decryptor::::new_from_slices(key, iv).unwrap(); - let n = cipher.decrypt_padded_mut::(&mut dec)?.len(); - dec.truncate(n) - } - b"aes256-cbc" => { - #[allow(clippy::unwrap_used)] // parameters are static - let cipher = cbc::Decryptor::::new_from_slices(key, iv).unwrap(); - let n = cipher.decrypt_padded_mut::(&mut dec)?.len(); - dec.truncate(n) - } - b"aes128-ctr" => { - #[allow(clippy::unwrap_used)] // parameters are static - let mut cipher = Ctr64BE::::new_from_slices(key, iv).unwrap(); - cipher.apply_keystream(&mut dec); - dec.truncate(secret_key.len()) - } - b"aes256-ctr" => { - #[allow(clippy::unwrap_used)] // parameters are static - let mut cipher = Ctr64BE::::new_from_slices(key, iv).unwrap(); - cipher.apply_keystream(&mut dec); - dec.truncate(secret_key.len()) - } - _ => {} +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(dec) - } else { - Err(Error::KeyIsEncrypted) } } diff --git a/russh-keys/src/key.rs b/russh-keys/src/key.rs index eae00283..83d28ff5 100644 --- a/russh-keys/src/key.rs +++ b/russh-keys/src/key.rs @@ -97,12 +97,11 @@ impl SignatureHash { } pub fn from_rsa_hostkey_algo(algo: &[u8]) -> Option { - if algo == b"rsa-sha2-256" { - Some(Self::SHA2_256) - } else if algo == b"rsa-sha2-512" { - Some(Self::SHA2_512) - } else { - Some(Self::SHA1) + 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, } } } @@ -135,59 +134,12 @@ impl PartialEq for PublicKey { impl PublicKey { /// Parse a public key in SSH format. pub fn parse(algo: &[u8], pubkey: &[u8]) -> Result { - match algo { - b"ssh-ed25519" => { - let mut p = pubkey.reader(0); - let key_algo = p.read_string()?; - let key_bytes = p.read_string()?; - if key_algo != b"ssh-ed25519" { - return Err(Error::CouldNotReadKey); - } - let Ok(key_bytes) = <&[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>::try_from(key_bytes) - else { - return Err(Error::CouldNotReadKey); - }; - ed25519_dalek::VerifyingKey::from_bytes(key_bytes) - .map(PublicKey::Ed25519) - .map_err(Error::from) - } - b"ssh-rsa" | b"rsa-sha2-256" | b"rsa-sha2-512" => { - use log::debug; - let mut p = pubkey.reader(0); - let key_algo = p.read_string()?; - debug!("{:?}", std::str::from_utf8(key_algo)); - if key_algo != b"ssh-rsa" - && key_algo != b"rsa-sha2-256" - && key_algo != b"rsa-sha2-512" - { - return Err(Error::CouldNotReadKey); - } - Ok(PublicKey::new_rsa_with_hash( - &p.read_ssh()?, - SignatureHash::from_rsa_hostkey_algo(algo).unwrap_or(SignatureHash::SHA1), - )?) - } - crate::KEYTYPE_ECDSA_SHA2_NISTP256 - | crate::KEYTYPE_ECDSA_SHA2_NISTP384 - | crate::KEYTYPE_ECDSA_SHA2_NISTP521 => { - let mut p = pubkey.reader(0); - let key_algo = p.read_string()?; - let curve = p.read_string()?; - let sec1_bytes = p.read_string()?; - - if key_algo != algo { - return Err(Error::CouldNotReadKey); - } - - let key = ec::PublicKey::from_sec1_bytes(key_algo, sec1_bytes)?; - if curve != key.ident().as_bytes() { - return Err(Error::CouldNotReadKey); - } - - Ok(PublicKey::EC { key }) - } - _ => Err(Error::CouldNotReadKey), + use ssh_encoding::Decode; + let key_data = &ssh_key::public::KeyData::decode(&mut pubkey.reader(0))?; + if key_data.algorithm().as_str().as_bytes() != algo { + return Err(Error::KeyIsCorrupt); } + Self::try_from(key_data) } pub fn new_rsa_with_hash( @@ -234,15 +186,9 @@ impl PublicKey { data_encoding::BASE64_NOPAD.encode(&hasher.finalize()) } - pub fn set_algorithm(&mut self, algorithm: &[u8]) { + pub fn set_algorithm(&mut self, algorithm: SignatureHash) { if let PublicKey::RSA { ref mut hash, .. } = self { - if algorithm == b"rsa-sha2-512" { - *hash = SignatureHash::SHA2_512 - } else if algorithm == b"rsa-sha2-256" { - *hash = SignatureHash::SHA2_256 - } else if algorithm == b"ssh-rsa" { - *hash = SignatureHash::SHA1 - } + *hash = algorithm; } } } @@ -484,36 +430,10 @@ fn ec_verify(key: &ec::PublicKey, b: &[u8], sig: &[u8]) -> Result<(), Error> { /// Parse a public key from a byte slice. pub fn parse_public_key(p: &[u8], prefer_hash: Option) -> Result { - let mut pos = p.reader(0); - let t = pos.read_string()?; - if t == b"ssh-ed25519" { - if let Ok(pubkey) = pos.read_string() { - let Ok(pubkey) = <&[u8; ed25519_dalek::PUBLIC_KEY_LENGTH]>::try_from(pubkey) else { - return Err(Error::CouldNotReadKey); - }; - let p = ed25519_dalek::VerifyingKey::from_bytes(pubkey).map_err(Error::from)?; - return Ok(PublicKey::Ed25519(p)); - } - } - if t == b"ssh-rsa" { - return PublicKey::new_rsa_with_hash( - &pos.read_ssh()?, - prefer_hash.unwrap_or(SignatureHash::SHA2_256), - ); - } - if t == crate::KEYTYPE_ECDSA_SHA2_NISTP256 - || t == crate::KEYTYPE_ECDSA_SHA2_NISTP384 - || t == crate::KEYTYPE_ECDSA_SHA2_NISTP521 - { - let ident = pos.read_string()?; - let sec1_bytes = pos.read_string()?; - let key = ec::PublicKey::from_sec1_bytes(t, sec1_bytes)?; - if ident != key.ident().as_bytes() { - return Err(Error::CouldNotReadKey); - } - return Ok(PublicKey::EC { key }); - } - Err(Error::CouldNotReadKey) + use ssh_encoding::Decode; + 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. diff --git a/russh-keys/src/lib.rs b/russh-keys/src/lib.rs index ba384720..8d11228b 100644 --- a/russh-keys/src/lib.rs +++ b/russh-keys/src/lib.rs @@ -174,6 +174,11 @@ pub enum Error { #[error("Sec1: {0}")] Sec1(#[from] sec1::Error), + #[error("SshKey: {0}")] + SshKey(#[from] ssh_key::Error), + #[error("SshEncoding: {0}")] + SshEncoding(#[from] ssh_encoding::Error), + #[error("Environment variable `{0}` not found")] EnvVar(&'static str), #[error( @@ -183,8 +188,6 @@ pub enum Error { BadAuthSock, } -const KEYTYPE_ED25519: &[u8] = b"ssh-ed25519"; -const KEYTYPE_RSA: &[u8] = b"ssh-rsa"; 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(); diff --git a/russh/src/server/encrypted.rs b/russh/src/server/encrypted.rs index f36e5d54..af8d64cc 100644 --- a/russh/src/server/encrypted.rs +++ b/russh/src/server/encrypted.rs @@ -426,7 +426,8 @@ impl Encrypted { debug!("signature = {:?}", signature); let mut s = signature.reader(0); let algo_ = s.read_string().map_err(crate::Error::from)?; - pubkey.set_algorithm(algo_); + key::SignatureHash::from_rsa_hostkey_algo(algo_) + .inspect(|hash| pubkey.set_algorithm(*hash)); debug!("algo_: {:?}", algo_); let sig = s.read_string().map_err(crate::Error::from)?; #[allow(clippy::indexing_slicing)] // length checked