diff --git a/Cargo.lock b/Cargo.lock index 8b3a013..ec55b49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -310,6 +310,7 @@ dependencies = [ "clap", "cluelessh-agent-client", "cluelessh-format", + "cluelessh-keys", "cluelessh-transport", "eyre", "hex", @@ -350,6 +351,7 @@ dependencies = [ name = "cluelessh-faked" version = "0.1.0" dependencies = [ + "cluelessh-keys", "cluelessh-protocol", "cluelessh-tokio", "eyre", @@ -389,12 +391,13 @@ dependencies = [ "base64", "bcrypt-pbkdf", "cluelessh-format", - "cluelessh-transport", "ctr", "ed25519-dalek", + "p256", "pem", "rand", "thiserror", + "tracing", ] [[package]] @@ -430,6 +433,7 @@ dependencies = [ "base64", "chacha20", "cluelessh-format", + "cluelessh-keys", "crypto-bigint", "ctr", "ed25519-dalek", diff --git a/bin/cluelessh-agentctl/Cargo.toml b/bin/cluelessh-agentctl/Cargo.toml index e64e0b6..9d6eff8 100644 --- a/bin/cluelessh-agentctl/Cargo.toml +++ b/bin/cluelessh-agentctl/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] cluelessh-agent-client = { path = "../../lib/cluelessh-agent-client" } cluelessh-transport = { path = "../../lib/cluelessh-transport" } +cluelessh-keys = { path = "../../lib/cluelessh-keys" } clap = { version = "4.5.16", features = ["derive"] } eyre.workspace = true diff --git a/bin/cluelessh-agentctl/src/main.rs b/bin/cluelessh-agentctl/src/main.rs index 6e98d71..c340af3 100644 --- a/bin/cluelessh-agentctl/src/main.rs +++ b/bin/cluelessh-agentctl/src/main.rs @@ -3,7 +3,7 @@ use std::{io::Write, path::PathBuf}; use clap::Parser; use cluelessh_agent_client::{IdentityAnswer, SocketAgentConnection}; use cluelessh_format::Writer; -use cluelessh_transport::key::PublicKey; +use cluelessh_keys::public::PublicKey; use eyre::{bail, Context}; use sha2::Digest; diff --git a/bin/cluelessh-faked/Cargo.toml b/bin/cluelessh-faked/Cargo.toml index fa2cf85..7ca0747 100644 --- a/bin/cluelessh-faked/Cargo.toml +++ b/bin/cluelessh-faked/Cargo.toml @@ -9,7 +9,7 @@ hex-literal = "0.4.1" rand = "0.8.5" cluelessh-protocol = { path = "../../lib/cluelessh-protocol" } cluelessh-tokio = { path = "../../lib/cluelessh-tokio" } - +cluelessh-keys = { path = "../../lib/cluelessh-keys" } tokio = { version = "1.39.2", features = ["full"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] } diff --git a/bin/cluelessh-faked/src/main.rs b/bin/cluelessh-faked/src/main.rs index dafb33b..bf40b42 100644 --- a/bin/cluelessh-faked/src/main.rs +++ b/bin/cluelessh-faked/src/main.rs @@ -60,7 +60,18 @@ async fn main() -> eyre::Result<()> { ), }; - let mut listener = cluelessh_tokio::server::ServerListener::new(listener, auth_verify); + let transport_config = cluelessh_protocol::transport::server::ServerConfig { + host_keys: vec![ + cluelessh_keys::EncryptedPrivateKeys::parse(ED25519_PRIVKEY.as_bytes()) + .unwrap() + .decrypt(None) + .unwrap() + .remove(0), + ], + }; + + let mut listener = + cluelessh_tokio::server::ServerListener::new(listener, auth_verify, transport_config); loop { let next = listener.accept().await?; @@ -327,3 +338,18 @@ fn execute_command(command: &[u8]) -> ProcessOutput { }, } } + +const ED25519_PRIVKEY: &str = "\ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDpOc36b8DXNzM7U06RPdMyyNUXn+AMMEVXUhciSxm49gAAAJDpgLSk6YC0 +pAAAAAtzc2gtZWQyNTUxOQAAACDpOc36b8DXNzM7U06RPdMyyNUXn+AMMEVXUhciSxm49g +AAAECSeskxuEtJrr9L7ZkbpogXC5pKRNVHx1ueMX2h1XUnmek5zfpvwNc3MztTTpE90zLI +1Ref4AwwRVdSFyJLGbj2AAAAB3Rlc3RrZXkBAgMEBQY= +-----END OPENSSH PRIVATE KEY----- +"; + +pub(crate) const ECDSA_P256_PRIVKEY_BYTES: &[u8; 32] = &[ + 0x89, 0xdd, 0x0c, 0x96, 0x22, 0x85, 0x10, 0xec, 0x3c, 0xa4, 0xa1, 0xb8, 0xac, 0x2a, 0x77, 0xa8, + 0xd4, 0x4d, 0xcb, 0x9d, 0x90, 0x25, 0xc6, 0xd8, 0x3a, 0x02, 0x74, 0x4f, 0x9e, 0x44, 0xcd, 0xa3, +]; diff --git a/bin/cluelessh-key/src/main.rs b/bin/cluelessh-key/src/main.rs index 86c2d6b..306d64f 100644 --- a/bin/cluelessh-key/src/main.rs +++ b/bin/cluelessh-key/src/main.rs @@ -6,7 +6,7 @@ use std::{ use base64::Engine; use clap::Parser; -use cluelessh_keys::{KeyEncryptionParams, PrivateKeyType}; +use cluelessh_keys::{KeyEncryptionParams, PrivateKey}; use eyre::{bail, Context}; #[derive(clap::Parser)] @@ -125,17 +125,23 @@ fn info(id_file: &Path, decrypt: bool, show_private: bool) -> eyre::Result<()> { None }; - let keys = keys.parse_private(passphrase.as_deref())?; + let keys = keys.decrypt(passphrase.as_deref())?; for key in keys { println!("{} {}", key.private_key.public_key(), key.comment); if show_private { match key.private_key { - PrivateKeyType::Ed25519 { private_key, .. } => { + PrivateKey::Ed25519 { private_key, .. } => { println!( " private key: {}", base64::prelude::BASE64_STANDARD_NO_PAD.encode(private_key) ) } + PrivateKey::EcdsaSha2NistP256 { private_key, .. } => { + println!( + " private key: {}", + base64::prelude::BASE64_STANDARD_NO_PAD.encode(private_key.to_bytes()) + ) + } } } } diff --git a/bin/cluelessh/src/main.rs b/bin/cluelessh/src/main.rs index 7aa8208..c0fc8b5 100644 --- a/bin/cluelessh/src/main.rs +++ b/bin/cluelessh/src/main.rs @@ -2,9 +2,9 @@ use std::{collections::HashSet, sync::Arc}; use clap::Parser; +use cluelessh_keys::public::PublicKey; use cluelessh_tokio::client::SignatureResult; use cluelessh_tokio::PendingChannel; -use cluelessh_transport::key::PublicKey; use eyre::{bail, Context, ContextCompat, OptionExt, Result}; use tokio::net::TcpStream; use tracing::{debug, error}; diff --git a/bin/cluelesshd/src/auth.rs b/bin/cluelesshd/src/auth.rs index a24c80b..3ffac6a 100644 --- a/bin/cluelesshd/src/auth.rs +++ b/bin/cluelesshd/src/auth.rs @@ -3,10 +3,8 @@ use std::io; use cluelessh_keys::{ - authorized_keys::{self, AuthorizedKeys}, - PublicKeyWithComment, + authorized_keys::{self, AuthorizedKeys}, public::PublicKey, PublicKeyWithComment }; -use cluelessh_transport::key::PublicKey; use users::os::unix::UserExt; /// A known-authorized public key for a user. diff --git a/bin/cluelesshd/src/main.rs b/bin/cluelesshd/src/main.rs index 9a9c097..cc4710e 100644 --- a/bin/cluelesshd/src/main.rs +++ b/bin/cluelesshd/src/main.rs @@ -4,8 +4,8 @@ mod pty; use std::{io, net::SocketAddr, process::ExitStatus, sync::Arc}; use auth::AuthError; +use cluelessh_keys::public::PublicKey; use cluelessh_tokio::{server::ServerAuthVerify, Channel}; -use cluelessh_transport::key::PublicKey; use eyre::{bail, eyre, Context, OptionExt, Result}; use pty::Pty; use rustix::termios::Winsize; @@ -109,7 +109,9 @@ async fn main() -> eyre::Result<()> { auth_banner: Some("welcome to my server!!!\r\ni hope you enjoy your stay.\r\n".to_owned()), }; - let mut listener = cluelessh_tokio::server::ServerListener::new(listener, auth_verify); + let config = todo!(); + + let mut listener = cluelessh_tokio::server::ServerListener::new(listener, auth_verify, config); loop { let next = listener.accept().await?; diff --git a/lib/cluelessh-format/Cargo.toml b/lib/cluelessh-format/Cargo.toml index c345069..845b8ca 100644 --- a/lib/cluelessh-format/Cargo.toml +++ b/lib/cluelessh-format/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -crypto-bigint = "0.5.5" +crypto-bigint = { version = "0.5.5", features = ["generic-array"] } [lints] workspace = true diff --git a/lib/cluelessh-keys/Cargo.toml b/lib/cluelessh-keys/Cargo.toml index c10297b..2f32c5c 100644 --- a/lib/cluelessh-keys/Cargo.toml +++ b/lib/cluelessh-keys/Cargo.toml @@ -10,10 +10,11 @@ ctr = "0.9.2" ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } pem = "3.0.4" rand = "0.8.5" -cluelessh-transport = { path = "../cluelessh-transport" } thiserror = "1.0.63" base64 = "0.22.1" cluelessh-format = { version = "0.1.0", path = "../cluelessh-format" } +tracing.workspace = true +p256 = "0.13.2" [lints] workspace = true diff --git a/lib/cluelessh-keys/src/authorized_keys.rs b/lib/cluelessh-keys/src/authorized_keys.rs index b19728e..6a0937c 100644 --- a/lib/cluelessh-keys/src/authorized_keys.rs +++ b/lib/cluelessh-keys/src/authorized_keys.rs @@ -1,7 +1,6 @@ use base64::Engine; -use cluelessh_transport::key::PublicKey; -use crate::PublicKeyWithComment; +use crate::{public::PublicKey, PublicKeyWithComment}; pub struct AuthorizedKeys { pub keys: Vec, @@ -56,9 +55,7 @@ impl AuthorizedKeys { #[cfg(test)] mod tests { - use cluelessh_transport::key::PublicKey; - - use crate::PublicKeyWithComment; + use crate::{public::PublicKey, PublicKeyWithComment}; use super::AuthorizedKeys; @@ -70,10 +67,16 @@ mod tests { keys.keys.as_slice(), [PublicKeyWithComment { key: PublicKey::Ed25519 { - public_key: [ - 109, 39, 214, 41, 20, 27, 218, 216, 170, 134, 225, 237, 106, 64, 201, 122, - 234, 102, 172, 80, 161, 13, 179, 52, 154, 197, 62, 61, 118, 129, 58, 79, - ], + public_key: ed25519_dalek::VerifyingKey::from_bytes( + &[ + 109, 39, 214, 41, 20, 27, 218, 216, 170, 134, 225, 237, 106, 64, 201, + 122, 234, 102, 172, 80, 161, 13, 179, 52, 154, 197, 62, 61, 118, 129, + 58, 79, + ] + .try_into() + .unwrap() + ) + .unwrap(), }, comment: "nora".into(), }] @@ -86,17 +89,27 @@ mod tests { let keys = AuthorizedKeys::parse(keys).unwrap(); let provided = PublicKey::Ed25519 { - public_key: [ - 109, 39, 214, 41, 20, 27, 218, 216, 170, 134, 225, 237, 106, 64, 201, 122, 234, - 102, 172, 80, 161, 13, 179, 52, 154, 197, 62, 61, 118, 129, 58, 79, - ], + public_key: ed25519_dalek::VerifyingKey::from_bytes( + &[ + 109, 39, 214, 41, 20, 27, 218, 216, 170, 134, 225, 237, 106, 64, 201, 122, 234, + 102, 172, 80, 161, 13, 179, 52, 154, 197, 62, 61, 118, 129, 58, 79, + ] + .try_into() + .unwrap(), + ) + .unwrap(), }; let flipped = PublicKey::Ed25519 { - public_key: [ - 0, 39, 214, 41, 20, 27, 218, 216, 170, 134, 225, 237, 106, 64, 201, 122, 234, 102, - 172, 80, 161, 13, 179, 52, 154, 197, 62, 61, 118, 129, 58, 79, - ], + public_key: ed25519_dalek::VerifyingKey::from_bytes( + &[ + 1, 39, 214, 41, 20, 27, 218, 216, 170, 134, 225, 237, 106, 64, 201, 122, 234, + 102, 172, 80, 161, 13, 179, 52, 154, 197, 62, 61, 118, 129, 58, 79, + ] + .try_into() + .unwrap(), + ) + .unwrap(), }; assert!(keys.contains(&provided).is_some()); @@ -119,10 +132,16 @@ mod tests { keys.keys.as_slice(), [PublicKeyWithComment { key: PublicKey::Ed25519 { - public_key: [ - 109, 39, 214, 41, 20, 27, 218, 216, 170, 134, 225, 237, 106, 64, 201, 122, - 234, 102, 172, 80, 161, 13, 179, 52, 154, 197, 62, 61, 118, 129, 58, 79, - ], + public_key: ed25519_dalek::VerifyingKey::from_bytes( + &[ + 109, 39, 214, 41, 20, 27, 218, 216, 170, 134, 225, 237, 106, 64, 201, + 122, 234, 102, 172, 80, 161, 13, 179, 52, 154, 197, 62, 61, 118, 129, + 58, 79, + ] + .try_into() + .unwrap() + ) + .unwrap(), }, comment: "".into(), }] diff --git a/lib/cluelessh-keys/src/crypto.rs b/lib/cluelessh-keys/src/crypto.rs index a2f8491..da231e2 100644 --- a/lib/cluelessh-keys/src/crypto.rs +++ b/lib/cluelessh-keys/src/crypto.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use aes::cipher::{KeySizeUser, StreamCipher}; use cluelessh_format::{Reader, Writer}; -use crate::PrivateKeyType; +use crate::PrivateKey; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Cipher { @@ -141,13 +141,13 @@ pub struct KeyGenerationParams { pub key_type: KeyType, } -pub(crate) fn generate_private_key(params: KeyGenerationParams) -> PrivateKeyType { +pub(crate) fn generate_private_key(params: KeyGenerationParams) -> PrivateKey { match params.key_type { KeyType::Ed25519 => { let private_key = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng); - PrivateKeyType::Ed25519 { - public_key: private_key.verifying_key().to_bytes(), + PrivateKey::Ed25519 { + public_key: private_key.verifying_key(), private_key: private_key.to_bytes(), } } diff --git a/lib/cluelessh-keys/src/lib.rs b/lib/cluelessh-keys/src/lib.rs index 64fa69e..36dae0b 100644 --- a/lib/cluelessh-keys/src/lib.rs +++ b/lib/cluelessh-keys/src/lib.rs @@ -1,11 +1,15 @@ pub mod authorized_keys; mod crypto; +pub mod public; pub mod signature; +use std::fmt::Debug; + use cluelessh_format::{Reader, Writer}; -use cluelessh_transport::key::PublicKey; use crypto::{Cipher, Kdf}; +use crate::public::PublicKey; + // TODO: good typed error messages so the user knows what's going on pub use crypto::{KeyGenerationParams, KeyType}; @@ -23,16 +27,34 @@ pub struct EncryptedPrivateKeys { pub encrypted_private_keys: Vec, } +#[derive(Clone)] pub struct PlaintextPrivateKey { - pub private_key: PrivateKeyType, + pub private_key: PrivateKey, pub comment: String, checkint: u32, } -pub enum PrivateKeyType { +impl Debug for PlaintextPrivateKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PlaintextPrivateKey") + .field( + "public_key", + &format_args!("{}", self.private_key.public_key()), + ) + .field("comment", &self.comment) + .finish() + } +} + +#[derive(Clone)] +pub enum PrivateKey { Ed25519 { - public_key: [u8; 32], - private_key: [u8; 32], + public_key: ed25519_dalek::VerifyingKey, + private_key: [u8; 32], // TODO: store a signing key! + }, + EcdsaSha2NistP256 { + public_key: p256::ecdsa::VerifyingKey, + private_key: p256::ecdsa::SigningKey, }, } @@ -139,7 +161,7 @@ impl EncryptedPrivateKeys { Ok(data) } - pub fn parse_private( + pub fn decrypt( &self, passphrase: Option<&str>, ) -> cluelessh_format::Result> { @@ -159,15 +181,17 @@ impl EncryptedPrivateKeys { for pubkey in &self.public_keys { let keytype = match pubkey { PublicKey::Ed25519 { public_key } => { - // + // let alg = p.utf8_string()?; - if alg != "ssh-ed25519" { + if alg != pubkey.algorithm_name() { return Err(cluelessh_format::ParseError(format!( - "algorithm mismatch. pubkey: ssh-ed25519, privkey: {alg}" + "algorithm mismatch. pubkey: {}, privkey: {alg}", + pubkey.algorithm_name() ))); } + let enc_a = p.string()?; // ENC(A) - if enc_a != public_key { + if enc_a != public_key.as_bytes() { return Err(cluelessh_format::ParseError(format!("public key mismatch"))); } let k_enc_a = p.string()?; // k || ENC(A) @@ -178,16 +202,42 @@ impl EncryptedPrivateKeys { ))); } let (k, enc_a) = k_enc_a.split_at(32); - if enc_a != public_key { + if enc_a != public_key.as_bytes() { // Yes, ed25519 SSH keys seriously store the public key THREE TIMES. return Err(cluelessh_format::ParseError(format!("public key mismatch"))); } let private_key = k.try_into().unwrap(); - PrivateKeyType::Ed25519 { + PrivateKey::Ed25519 { public_key: *public_key, private_key, } } + PublicKey::EcdsaSha2NistP256 { public_key } => { + // + let alg = p.utf8_string()?; + if alg != pubkey.algorithm_name() { + return Err(cluelessh_format::ParseError(format!( + "algorithm mismatch. pubkey: {}, privkey: {alg}", + pubkey.algorithm_name() + ))); + } + + let curve_name = p.utf8_string()?; + if curve_name != "nistp256" { + return Err(cluelessh_format::ParseError(format!( + "curve name mismatch. expected: nistp256, found: {curve_name}", + ))); + } + + let q = p.string()?; + if q != public_key.to_encoded_point(false).as_bytes() { + return Err(cluelessh_format::ParseError(format!("public key mismatch"))); + } + + let _d = p.mpint()?; + + todo!() + } }; let comment = p.utf8_string()?; @@ -262,22 +312,33 @@ impl PlaintextPrivateKey { enc.u32(self.checkint); enc.u32(self.checkint); - match self.private_key { - PrivateKeyType::Ed25519 { + match &self.private_key { + PrivateKey::Ed25519 { public_key, private_key, } => { - // + // enc.string(b"ssh-ed25519"); enc.string(public_key); - let combined = private_key.len() + public_key.len(); + let combined = private_key.len() + public_key.as_bytes().len(); enc.u32(combined as u32); - enc.raw(&private_key); - enc.raw(&public_key); - enc.string(self.comment.as_bytes()); + enc.raw(private_key); + enc.raw(public_key.as_bytes()); + } + PrivateKey::EcdsaSha2NistP256 { + public_key, + private_key, + } => { + // + enc.string(self.private_key.algorithm_name()); + enc.string("nistp256"); + enc.string(public_key.to_encoded_point(false)); + enc.mpint(p256::U256::from(private_key.as_nonzero_scalar().as_ref())); } } + enc.string(self.comment.as_bytes()); + // uh..., i don't really now how much i need to pad so YOLO this here // TODO: pad properly. enc.u8(1); @@ -310,17 +371,24 @@ impl PlaintextPrivateKey { } } -impl PrivateKeyType { +impl PrivateKey { pub fn public_key(&self) -> PublicKey { match *self { Self::Ed25519 { public_key, .. } => PublicKey::Ed25519 { public_key }, + Self::EcdsaSha2NistP256 { public_key, .. } => { + PublicKey::EcdsaSha2NistP256 { public_key } + } } } + + pub fn algorithm_name(&self) -> &'static str { + self.public_key().algorithm_name() + } } #[cfg(test)] mod tests { - use crate::{Cipher, EncryptedPrivateKeys, Kdf, KeyEncryptionParams, PrivateKeyType}; + use crate::{Cipher, EncryptedPrivateKeys, Kdf, KeyEncryptionParams, PrivateKey}; // ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHPaiIO6MePXM/QCJWVge1k4dsiefPr4taP9VJbCtXdx uwu // Password: 'test' @@ -345,6 +413,17 @@ zukcSwhnKrg+wzw7/JZQAAAAA3V3dQEC -----END OPENSSH PRIVATE KEY----- "; + // ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHZTdlJoLNb701EWnahywBv032Aby+Piza7TzKW1H6Z//Hni/rBcUgnMmG+Kc4XWp6zgny3FMFpviuL01eJbpY8= uwu + // no password + const TEST_ECDSA_SHA2_NISTP256_NONE: &[u8] = b"-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQR2U3ZSaCzW+9NRFp2ocsAb9N9gG8vj +4s2u08yltR+mf/x54v6wXFIJzJhvinOF1qes4J8txTBab4ri9NXiW6WPAAAAoKQV4mmkFe +JpAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHZTdlJoLNb701EW +nahywBv032Aby+Piza7TzKW1H6Z//Hni/rBcUgnMmG+Kc4XWp6zgny3FMFpviuL01eJbpY +8AAAAgVF0Z9J3CtkKpNt2IGTJZtBLK+QQKu/bUkp12gstIonUAAAADdXd1AQIDBAU= +-----END OPENSSH PRIVATE KEY-----"; + #[test] fn ed25519_none() { let keys = EncryptedPrivateKeys::parse(TEST_ED25519_NONE).unwrap(); @@ -352,17 +431,44 @@ zukcSwhnKrg+wzw7/JZQAAAAA3V3dQEC assert_eq!(keys.cipher, Cipher::None); assert_eq!(keys.kdf, Kdf::None); - let decrypted = keys.parse_private(None).unwrap(); + let decrypted = keys.decrypt(None).unwrap(); assert_eq!(decrypted.len(), 1); let key = decrypted.first().unwrap(); assert_eq!(key.comment, "uwu"); - assert!(matches!(key.private_key, PrivateKeyType::Ed25519 { .. })); + assert!(matches!(key.private_key, PrivateKey::Ed25519 { .. })); } #[test] fn roundtrip_ed25519_none() { let keys = EncryptedPrivateKeys::parse(TEST_ED25519_NONE).unwrap(); - let decrypted = keys.parse_private(None).unwrap(); + let decrypted = keys.decrypt(None).unwrap(); + + let encrypted = decrypted[0] + .encrypt(KeyEncryptionParams::secure_or_none("".to_owned())) + .unwrap(); + + let bytes = encrypted.to_bytes(); + assert_eq!(pem::parse(TEST_ED25519_NONE).unwrap().contents(), bytes); + } + + #[test] + fn ecdsa_sha2_nistp256_none() { + let keys = EncryptedPrivateKeys::parse(TEST_ECDSA_SHA2_NISTP256_NONE).unwrap(); + assert_eq!(keys.public_keys.len(), 1); + assert_eq!(keys.cipher, Cipher::None); + assert_eq!(keys.kdf, Kdf::None); + + let decrypted = keys.decrypt(None).unwrap(); + assert_eq!(decrypted.len(), 1); + let key = decrypted.first().unwrap(); + assert_eq!(key.comment, "uwu"); + assert!(matches!(key.private_key, PrivateKey::Ed25519 { .. })); + } + + #[test] + fn roundtrip_ecdsa_sha2_nistp256_none() { + let keys = EncryptedPrivateKeys::parse(TEST_ECDSA_SHA2_NISTP256_NONE).unwrap(); + let decrypted = keys.decrypt(None).unwrap(); let encrypted = decrypted[0] .encrypt(KeyEncryptionParams::secure_or_none("".to_owned())) @@ -379,17 +485,17 @@ zukcSwhnKrg+wzw7/JZQAAAAA3V3dQEC assert_eq!(keys.cipher, Cipher::Aes256Ctr); assert!(matches!(keys.kdf, Kdf::BCrypt { .. })); - let decrypted = keys.parse_private(Some("test")).unwrap(); + let decrypted = keys.decrypt(Some("test")).unwrap(); assert_eq!(decrypted.len(), 1); let key = decrypted.first().unwrap(); assert_eq!(key.comment, "uwu"); - assert!(matches!(key.private_key, PrivateKeyType::Ed25519 { .. })); + assert!(matches!(key.private_key, PrivateKey::Ed25519 { .. })); } #[test] - fn roundtrip_aes256ctr() { + fn roundtrip_ed25519_aes256ctr() { let keys = EncryptedPrivateKeys::parse(TEST_ED25519_NONE).unwrap(); - let decrypted = keys.parse_private(None).unwrap(); + let decrypted = keys.decrypt(None).unwrap(); let encrypted = decrypted[0] .encrypt(KeyEncryptionParams::secure_or_none("".to_owned())) diff --git a/lib/cluelessh-transport/src/key.rs b/lib/cluelessh-keys/src/public.rs similarity index 62% rename from lib/cluelessh-transport/src/key.rs rename to lib/cluelessh-keys/src/public.rs index f4c3530..6f5892c 100644 --- a/lib/cluelessh-transport/src/key.rs +++ b/lib/cluelessh-keys/src/public.rs @@ -5,13 +5,19 @@ use std::fmt::Display; use base64::Engine; +use ed25519_dalek::VerifyingKey; use tracing::debug; use cluelessh_format::{ParseError, Reader, Writer}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum PublicKey { - Ed25519 { public_key: [u8; 32] }, + Ed25519 { + public_key: ed25519_dalek::VerifyingKey, + }, + EcdsaSha2NistP256 { + public_key: p256::ecdsa::VerifyingKey, + }, } impl PublicKey { @@ -28,8 +34,13 @@ impl PublicKey { return Err(ParseError(format!("incorrect ed25519 len: {len}"))); } let public_key = p.array::<32>()?; + let public_key = VerifyingKey::from_bytes(&public_key) + .map_err(|_| ParseError(format!("invalid ed25519 public key")))?; Self::Ed25519 { public_key } } + "ecdsa-sha2-nistp256" => { + todo!() + } _ => return Err(ParseError(format!("unsupported key type: {alg}"))), }; Ok(k) @@ -37,10 +48,17 @@ impl PublicKey { pub fn to_wire_encoding(&self) -> Vec { let mut p = Writer::new(); + p.string(self.algorithm_name()); match self { Self::Ed25519 { public_key } => { - p.string(b"ssh-ed25519"); - p.string(public_key); + p.string(public_key.as_bytes()); + } + Self::EcdsaSha2NistP256 { public_key } => { + // + p.string(b"nistp256"); + // > point compression MAY be used. + // But OpenSSH does not appear to support that, so let's NOT use it. + p.string(public_key.to_encoded_point(false).as_bytes()); } } p.finish() @@ -49,6 +67,7 @@ impl PublicKey { pub fn algorithm_name(&self) -> &'static str { match self { Self::Ed25519 { .. } => "ssh-ed25519", + Self::EcdsaSha2NistP256 { .. } => "ecdsa-sha2-nistp256", } } @@ -70,12 +89,11 @@ impl PublicKey { debug!("Invalid signature length"); return false; }; - let Ok(verifying_key) = ed25519_dalek::VerifyingKey::from_bytes(public_key) else { - debug!("Invalid public key"); - return false; - }; - verifying_key.verify_strict(data, &signature).is_ok() + public_key.verify_strict(data, &signature).is_ok() + } + PublicKey::EcdsaSha2NistP256 { .. } => { + todo!("ecdsa-sha2-nistp256 signature verification") } } } @@ -86,8 +104,12 @@ impl Display for PublicKey { match self { Self::Ed25519 { .. } => { let encoded_pubkey = b64encode(&self.to_wire_encoding()); - write!(f, "ssh-ed25519 {encoded_pubkey}") + write!(f, "{} {encoded_pubkey}", self.algorithm_name()) } + Self::EcdsaSha2NistP256 { .. } => { + let encoded_pubkey = b64encode(&self.to_wire_encoding()); + write!(f, "{} {encoded_pubkey}", self.algorithm_name()) + }, } } } diff --git a/lib/cluelessh-keys/src/signature.rs b/lib/cluelessh-keys/src/signature.rs index f662fad..9b72d75 100644 --- a/lib/cluelessh-keys/src/signature.rs +++ b/lib/cluelessh-keys/src/signature.rs @@ -1,5 +1,6 @@ use cluelessh_format::Writer; -use cluelessh_transport::key::PublicKey; + +use crate::public::PublicKey; // TODO SessionId newtype pub fn signature_data(session_id: [u8; 32], username: &str, pubkey: &PublicKey) -> Vec { diff --git a/lib/cluelessh-protocol/src/lib.rs b/lib/cluelessh-protocol/src/lib.rs index 4a1437e..6fc35ba 100644 --- a/lib/cluelessh-protocol/src/lib.rs +++ b/lib/cluelessh-protocol/src/lib.rs @@ -3,12 +3,14 @@ use std::collections::HashSet; use std::mem; use auth::AuthOption; -pub use cluelessh_connection as connection; use cluelessh_connection::ChannelOperation; +use tracing::debug; + +// Re-exports +pub use cluelessh_connection as connection; pub use cluelessh_connection::{ChannelUpdate, ChannelUpdateKind}; pub use cluelessh_transport as transport; pub use cluelessh_transport::{Result, SshStatus}; -use tracing::debug; pub struct ThreadRngRand; impl transport::SshRng for ThreadRngRand { diff --git a/lib/cluelessh-tokio/src/server.rs b/lib/cluelessh-tokio/src/server.rs index 8eb8b65..553b308 100644 --- a/lib/cluelessh-tokio/src/server.rs +++ b/lib/cluelessh-tokio/src/server.rs @@ -24,6 +24,7 @@ use crate::{Channel, ChannelState, PendingChannel}; pub struct ServerListener { listener: TcpListener, auth_verify: ServerAuthVerify, + transport_config: cluelessh_transport::server::ServerConfig // TODO ratelimits etc } @@ -79,10 +80,11 @@ impl From for Error { } impl ServerListener { - pub fn new(listener: TcpListener, auth_verify: ServerAuthVerify) -> Self { + pub fn new(listener: TcpListener, auth_verify: ServerAuthVerify, transport_config: cluelessh_transport::server::ServerConfig) -> Self { Self { listener, auth_verify, + transport_config, } } @@ -93,12 +95,13 @@ impl ServerListener { conn, peer_addr, self.auth_verify.clone(), + self.transport_config.clone(), )) } } impl ServerConnection { - pub fn new(stream: S, peer_addr: SocketAddr, auth_verify: ServerAuthVerify) -> Self { + pub fn new(stream: S, peer_addr: SocketAddr, auth_verify: ServerAuthVerify, transport_config: cluelessh_transport::server::ServerConfig) -> Self { let (operations_send, operations_recv) = tokio::sync::mpsc::channel(15); let (channel_ops_send, channel_ops_recv) = tokio::sync::mpsc::channel(15); @@ -131,6 +134,7 @@ impl ServerConnection { proto: cluelessh_protocol::ServerConnection::new( cluelessh_transport::server::ServerConnection::new( cluelessh_protocol::ThreadRngRand, + transport_config, ), options, auth_verify.auth_banner.clone(), diff --git a/lib/cluelessh-transport/Cargo.toml b/lib/cluelessh-transport/Cargo.toml index 302e32d..abfc524 100644 --- a/lib/cluelessh-transport/Cargo.toml +++ b/lib/cluelessh-transport/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] cluelessh-format = { path = "../cluelessh-format" } +cluelessh-keys = { path = "../cluelessh-keys" } aes = "0.8.4" aes-gcm = "0.10.3" chacha20 = "0.9.1" diff --git a/lib/cluelessh-transport/src/client.rs b/lib/cluelessh-transport/src/client.rs index cb9458d..68e3afc 100644 --- a/lib/cluelessh-transport/src/client.rs +++ b/lib/cluelessh-transport/src/client.rs @@ -155,7 +155,7 @@ impl ClientConnection { )); } - let sup_algs = SupportedAlgorithms::secure(); + let sup_algs = SupportedAlgorithms::secure(&[]); let _cookie = kexinit.array::<16>()?; diff --git a/lib/cluelessh-transport/src/crypto.rs b/lib/cluelessh-transport/src/crypto.rs index 0816474..3c16f39 100644 --- a/lib/cluelessh-transport/src/crypto.rs +++ b/lib/cluelessh-transport/src/crypto.rs @@ -1,6 +1,7 @@ pub mod encrypt; use cluelessh_format::{Reader, Writer}; +use cluelessh_keys::{public::PublicKey, PlaintextPrivateKey, PrivateKey}; use p256::ecdsa::signature::Signer; use sha2::Digest; @@ -103,14 +104,12 @@ impl AlgorithmName for EncryptionAlgorithm { self.name } } - -pub struct EncodedSshPublicHostKey(pub Vec); pub struct EncodedSshSignature(pub Vec); pub struct HostKeySigningAlgorithm { name: &'static str, hostkey_private: Vec, - public_key: fn(private_key: &[u8]) -> EncodedSshPublicHostKey, + public_key: fn(private_key: &[u8]) -> PublicKey, sign: fn(private_key: &[u8], data: &[u8]) -> EncodedSshSignature, pub verify: fn(public_key: &[u8], message: &[u8], signature: &EncodedSshSignature) -> Result<()>, @@ -126,7 +125,7 @@ impl HostKeySigningAlgorithm { pub fn sign(&self, data: &[u8]) -> EncodedSshSignature { (self.sign)(&self.hostkey_private, data) } - pub fn public_key(&self) -> EncodedSshPublicHostKey { + pub fn public_key(&self) -> PublicKey { (self.public_key)(&self.hostkey_private) } } @@ -139,11 +138,7 @@ pub fn hostkey_ed25519(hostkey_private: Vec) -> HostKeySigningAlgorithm { let key = ed25519_dalek::SigningKey::from_bytes(key.try_into().unwrap()); let public_key = key.verifying_key(); - // - let mut data = Writer::new(); - data.string(b"ssh-ed25519"); - data.string(public_key.as_bytes()); - EncodedSshPublicHostKey(data.finish()) + PublicKey::Ed25519 { public_key } }, sign: |key, data| { let key = ed25519_dalek::SigningKey::from_bytes(key.try_into().unwrap()); @@ -203,7 +198,7 @@ pub fn hostkey_ecdsa_sha2_p256(hostkey_private: Vec) -> HostKeySigningAlgori // > point compression MAY be used. // But OpenSSH does not appear to support that, so let's NOT use it. data.string(public_key.to_encoded_point(false).as_bytes()); - EncodedSshPublicHostKey(data.finish()) + todo!() }, sign: |key, data| { let key = p256::ecdsa::SigningKey::from_slice(key).unwrap(); @@ -239,8 +234,15 @@ impl AlgorithmNegotiation { } } + let we_support = self + .supported + .iter() + .map(|alg| alg.name()) + .collect::>() + .join(","); + Err(peer_error!( - "peer does not support any matching algorithm: peer supports: {peer_supports:?}" + "peer does not support any matching algorithm: we support: {we_support:?}, peer supports: {peer_supports:?}" )) } } @@ -258,16 +260,23 @@ pub struct SupportedAlgorithms { impl SupportedAlgorithms { /// A secure default using elliptic curves and AEAD. - pub fn secure() -> Self { + pub fn secure(host_keys: &[PlaintextPrivateKey]) -> Self { + let supported_host_keys = host_keys + .iter() + .map(|key| match &key.private_key { + PrivateKey::Ed25519 { private_key, .. } => hostkey_ed25519(private_key.to_vec()), + PrivateKey::EcdsaSha2NistP256 { private_key, .. } => { + hostkey_ecdsa_sha2_p256(private_key.to_bytes().to_vec()) + } + }) + .collect(); + Self { key_exchange: AlgorithmNegotiation { supported: vec![KEX_CURVE_25519_SHA256, KEX_ECDH_SHA2_NISTP256], }, hostkey: AlgorithmNegotiation { - supported: vec![ - hostkey_ed25519(crate::server::ED25519_PRIVKEY_BYTES.to_vec()), - hostkey_ecdsa_sha2_p256(crate::server::ECDSA_P256_PRIVKEY_BYTES.to_vec()), - ], + supported: supported_host_keys, }, encryption_to_peer: AlgorithmNegotiation { supported: vec![encrypt::CHACHA20POLY1305, encrypt::AES256_GCM], diff --git a/lib/cluelessh-transport/src/lib.rs b/lib/cluelessh-transport/src/lib.rs index 21c4257..e73cca8 100644 --- a/lib/cluelessh-transport/src/lib.rs +++ b/lib/cluelessh-transport/src/lib.rs @@ -1,6 +1,5 @@ pub mod client; mod crypto; -pub mod key; pub mod packet; pub mod server; diff --git a/lib/cluelessh-transport/src/server.rs b/lib/cluelessh-transport/src/server.rs index 67caca3..0b3476b 100644 --- a/lib/cluelessh-transport/src/server.rs +++ b/lib/cluelessh-transport/src/server.rs @@ -20,9 +20,16 @@ pub struct ServerConnection { packet_transport: PacketTransport, rng: Box, + config: ServerConfig, + plaintext_packets: VecDeque, } +#[derive(Debug, Clone, Default)] +pub struct ServerConfig { + pub host_keys: Vec, +} + enum ServerState { ProtoExchange { ident_parser: ProtocolIdentParser, @@ -54,14 +61,14 @@ enum ServerState { } impl ServerConnection { - pub fn new(rng: impl SshRng + Send + Sync + 'static) -> Self { + pub fn new(rng: impl SshRng + Send + Sync + 'static, config: ServerConfig) -> Self { Self { state: ServerState::ProtoExchange { ident_parser: ProtocolIdentParser::new(), }, packet_transport: PacketTransport::new(), rng: Box::new(rng), - + config, plaintext_packets: VecDeque::new(), } } @@ -133,7 +140,7 @@ impl ServerConnection { } => { let kex = KeyExchangeInitPacket::parse(&packet.payload)?; - let sup_algs = SupportedAlgorithms::secure(); + let sup_algs = SupportedAlgorithms::secure(&self.config.host_keys); let kex_algorithm = sup_algs.key_exchange.find(kex.kex_algorithms.0)?; debug!(name = %kex_algorithm.name(), "Using KEX algorithm"); @@ -245,7 +252,7 @@ impl ServerConnection { SERVER_IDENTIFICATION, client_kexinit, server_kexinit, - &pub_hostkey.0, + &pub_hostkey.to_wire_encoding(), client_public_key, &server_public_key, &shared_secret, @@ -259,7 +266,7 @@ impl ServerConnection { // eprintln!("hash: {:x?}", hash); let packet = Packet::new_msg_kex_ecdh_reply( - &pub_hostkey.0, + &pub_hostkey.to_wire_encoding(), &server_public_key, &signature.0, ); @@ -348,35 +355,12 @@ impl ServerConnection { } } -/// Manually extracted from the key using , probably wrong -/// ```text -/// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOk5zfpvwNc3MztTTpE90zLI1Ref4AwwRVdSFyJLGbj2 testkey -/// ``` -/// ```text -/// -----BEGIN OPENSSH PRIVATE KEY----- -/// b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW -/// QyNTUxOQAAACDpOc36b8DXNzM7U06RPdMyyNUXn+AMMEVXUhciSxm49gAAAJDpgLSk6YC0 -/// pAAAAAtzc2gtZWQyNTUxOQAAACDpOc36b8DXNzM7U06RPdMyyNUXn+AMMEVXUhciSxm49g -/// AAAECSeskxuEtJrr9L7ZkbpogXC5pKRNVHx1ueMX2h1XUnmek5zfpvwNc3MztTTpE90zLI -/// 1Ref4AwwRVdSFyJLGbj2AAAAB3Rlc3RrZXkBAgMEBQY= -/// -----END OPENSSH PRIVATE KEY----- -/// ``` -// todo: remove this lol, lmao -pub(crate) const ED25519_PRIVKEY_BYTES: &[u8; 32] = &[ - 0x92, 0x7a, 0xc9, 0x31, 0xb8, 0x4b, 0x49, 0xae, 0xbf, 0x4b, 0xed, 0x99, 0x1b, 0xa6, 0x88, 0x17, - 0x0b, 0x9a, 0x4a, 0x44, 0xd5, 0x47, 0xc7, 0x5b, 0x9e, 0x31, 0x7d, 0xa1, 0xd5, 0x75, 0x27, 0x99, -]; - -pub(crate) const ECDSA_P256_PRIVKEY_BYTES: &[u8; 32] = &[ - 0x89, 0xdd, 0x0c, 0x96, 0x22, 0x85, 0x10, 0xec, 0x3c, 0xa4, 0xa1, 0xb8, 0xac, 0x2a, 0x77, 0xa8, - 0xd4, 0x4d, 0xcb, 0x9d, 0x90, 0x25, 0xc6, 0xd8, 0x3a, 0x02, 0x74, 0x4f, 0x9e, 0x44, 0xcd, 0xa3, -]; #[cfg(test)] mod tests { use hex_literal::hex; - use crate::{packet::MsgKind, server::ServerConnection, SshRng}; + use crate::{packet::MsgKind, server::{ServerConfig, ServerConnection}, SshRng}; struct NoRng; impl SshRng for NoRng { @@ -395,7 +379,7 @@ mod tests { #[test] fn protocol_exchange() { - let mut con = ServerConnection::new(NoRng); + let mut con = ServerConnection::new(NoRng, ServerConfig::default()); con.recv_bytes(b"SSH-2.0-OpenSSH_9.7\r\n").unwrap(); let msg = con.next_msg_to_send().unwrap(); assert!(matches!(msg.0, MsgKind::ServerProtocolInfo(_))); @@ -403,7 +387,7 @@ mod tests { #[test] fn protocol_exchange_slow_client() { - let mut con = ServerConnection::new(NoRng); + let mut con = ServerConnection::new(NoRng, ServerConfig::default()); con.recv_bytes(b"SSH-2.0-").unwrap(); con.recv_bytes(b"OpenSSH_9.7\r\n").unwrap(); let msg = con.next_msg_to_send().unwrap(); @@ -463,7 +447,7 @@ mod tests { }, ]; - let mut con = ServerConnection::new(HardcodedRng(rng)); + let mut con = ServerConnection::new(HardcodedRng(rng), ServerConfig::default()); for part in conversation { con.recv_bytes(&part.client).unwrap(); eprintln!("client: {:x?}", part.client);