diff --git a/bin/cluelessh-faked/src/main.rs b/bin/cluelessh-faked/src/main.rs index bf40b42..9a2bc08 100644 --- a/bin/cluelessh-faked/src/main.rs +++ b/bin/cluelessh-faked/src/main.rs @@ -67,6 +67,7 @@ async fn main() -> eyre::Result<()> { .decrypt(None) .unwrap() .remove(0), + // TODO: add ECDSA support again!! ], }; @@ -348,8 +349,3 @@ 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 306d64f..5531ea3 100644 --- a/bin/cluelessh-key/src/main.rs +++ b/bin/cluelessh-key/src/main.rs @@ -133,13 +133,13 @@ fn info(id_file: &Path, decrypt: bool, show_private: bool) -> eyre::Result<()> { PrivateKey::Ed25519 { private_key, .. } => { println!( " private key: {}", - base64::prelude::BASE64_STANDARD_NO_PAD.encode(private_key) + base64::prelude::BASE64_STANDARD.encode(private_key) ) } PrivateKey::EcdsaSha2NistP256 { private_key, .. } => { println!( " private key: {}", - base64::prelude::BASE64_STANDARD_NO_PAD.encode(private_key.to_bytes()) + base64::prelude::BASE64_STANDARD.encode(private_key.to_bytes()) ) } } diff --git a/bin/cluelesshd/src/main.rs b/bin/cluelesshd/src/main.rs index cc4710e..14d07db 100644 --- a/bin/cluelesshd/src/main.rs +++ b/bin/cluelesshd/src/main.rs @@ -4,8 +4,9 @@ mod pty; use std::{io, net::SocketAddr, process::ExitStatus, sync::Arc}; use auth::AuthError; -use cluelessh_keys::public::PublicKey; +use cluelessh_keys::{public::PublicKey, EncryptedPrivateKeys}; use cluelessh_tokio::{server::ServerAuthVerify, Channel}; +use cluelessh_transport::server::ServerConfig; use eyre::{bail, eyre, Context, OptionExt, Result}; use pty::Pty; use rustix::termios::Winsize; @@ -31,7 +32,7 @@ async fn main() -> eyre::Result<()> { tracing_subscriber::fmt().with_env_filter(env_filter).init(); - let addr = "0.0.0.0:2222".to_owned(); + let addr = "0.0.0.0:2223".to_owned(); let addr = addr .parse::() @@ -109,7 +110,39 @@ async fn main() -> eyre::Result<()> { auth_banner: Some("welcome to my server!!!\r\ni hope you enjoy your stay.\r\n".to_owned()), }; - let config = todo!(); + let mut host_keys = Vec::new(); + + let host_key_locations = ["/etc/ssh/ssh_host_ed25519_key", "./test_ed25519_key"]; + + for host_key_location in host_key_locations { + match tokio::fs::read_to_string(host_key_location).await { + Ok(key) => { + let key = EncryptedPrivateKeys::parse(key.as_bytes()) + .wrap_err_with(|| format!("invalid {host_key_location}"))?; + if key.requires_passphrase() { + bail!("{host_key_location} must not require a passphrase"); + } + let mut key = key + .decrypt(None) + .wrap_err_with(|| format!("invalid {host_key_location}"))?; + if key.len() != 1 { + bail!("{host_key_location} must contain a single key"); + } + host_keys.push(key.remove(0)); + + info!(?host_key_location, "Loaded host key") + } + Err(err) => { + debug!(?err, ?host_key_location, "Failed to load host key") + } + } + } + + if host_keys.is_empty() { + bail!("no host keys found"); + } + + let config = ServerConfig { host_keys }; let mut listener = cluelessh_tokio::server::ServerListener::new(listener, auth_verify, config); diff --git a/lib/cluelessh-format/src/lib.rs b/lib/cluelessh-format/src/lib.rs index 3fcaade..639d89b 100644 --- a/lib/cluelessh-format/src/lib.rs +++ b/lib/cluelessh-format/src/lib.rs @@ -81,8 +81,15 @@ impl<'a> Reader<'a> { Ok(NameList(list)) } - pub fn mpint(&mut self) -> Result> { - todo!("do correctly") + pub fn mpint(&mut self) -> Result<&'a [u8]> { + let mut s = self.string()?; + + if s.first() == Some(&0) { + // Skip the leading zero byte in case the number is negative. + s = &s[1..]; + } + + Ok(s) } pub fn string(&mut self) -> Result<&'a [u8]> { @@ -152,6 +159,10 @@ impl Writer { self.u8(v as u8); } + pub fn current_length(&self) -> usize { + self.0.len() + } + pub fn finish(self) -> Vec { self.0 } diff --git a/lib/cluelessh-keys/src/authorized_keys.rs b/lib/cluelessh-keys/src/authorized_keys.rs index 6a0937c..23c0dd6 100644 --- a/lib/cluelessh-keys/src/authorized_keys.rs +++ b/lib/cluelessh-keys/src/authorized_keys.rs @@ -23,7 +23,7 @@ impl AuthorizedKeys { let key_blob = parts .next() .ok_or_else(|| Error("missing key on line".to_owned()))?; - let key_blob = base64::prelude::BASE64_STANDARD_NO_PAD + let key_blob = base64::prelude::BASE64_STANDARD .decode(key_blob) .map_err(|err| Error(format!("invalid base64 encoding for key: {err}")))?; let comment = parts.next().unwrap_or_default(); diff --git a/lib/cluelessh-keys/src/crypto.rs b/lib/cluelessh-keys/src/crypto.rs index da231e2..9a71532 100644 --- a/lib/cluelessh-keys/src/crypto.rs +++ b/lib/cluelessh-keys/src/crypto.rs @@ -55,6 +55,14 @@ impl Cipher { } } } + + pub(crate) fn block_size(&self) -> usize { + // this is the "minimum" block size in core SSH, so I assume it's here as well? + match self { + Self::None => 8, + Self::Aes256Ctr => 16, // looks like it takes the AES block size, even if AES-CTR isn't really a block cipher.. + } + } } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/lib/cluelessh-keys/src/lib.rs b/lib/cluelessh-keys/src/lib.rs index 36dae0b..4d8159e 100644 --- a/lib/cluelessh-keys/src/lib.rs +++ b/lib/cluelessh-keys/src/lib.rs @@ -179,7 +179,7 @@ impl EncryptedPrivateKeys { let mut result_keys = Vec::new(); for pubkey in &self.public_keys { - let keytype = match pubkey { + let keytype = match *pubkey { PublicKey::Ed25519 { public_key } => { // let alg = p.utf8_string()?; @@ -208,7 +208,7 @@ impl EncryptedPrivateKeys { } let private_key = k.try_into().unwrap(); PrivateKey::Ed25519 { - public_key: *public_key, + public_key, private_key, } } @@ -234,9 +234,16 @@ impl EncryptedPrivateKeys { return Err(cluelessh_format::ParseError(format!("public key mismatch"))); } - let _d = p.mpint()?; + let d = p.mpint()?; - todo!() + let private_key = p256::ecdsa::SigningKey::from_slice(d).map_err(|_| { + cluelessh_format::ParseError(format!("invalid private key bytes")) + })?; + + PrivateKey::EcdsaSha2NistP256 { + public_key, + private_key, + } } }; @@ -339,10 +346,13 @@ impl PlaintextPrivateKey { 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); - enc.u8(2); + let current_len = enc.current_length(); + let block_size = params.cipher.block_size(); + let pad_len = current_len.next_multiple_of(block_size) - current_len; + + for i in 1..=(pad_len as u8) { + enc.u8(i); + } let mut encrypted_private_keys = enc.finish(); @@ -462,7 +472,7 @@ nahywBv032Aby+Piza7TzKW1H6Z//Hni/rBcUgnMmG+Kc4XWp6zgny3FMFpviuL01eJbpY assert_eq!(decrypted.len(), 1); let key = decrypted.first().unwrap(); assert_eq!(key.comment, "uwu"); - assert!(matches!(key.private_key, PrivateKey::Ed25519 { .. })); + assert!(matches!(key.private_key, PrivateKey::EcdsaSha2NistP256 { .. })); } #[test] @@ -475,7 +485,22 @@ nahywBv032Aby+Piza7TzKW1H6Z//Hni/rBcUgnMmG+Kc4XWp6zgny3FMFpviuL01eJbpY .unwrap(); let bytes = encrypted.to_bytes(); - assert_eq!(pem::parse(TEST_ED25519_NONE).unwrap().contents(), bytes); + + std::fs::write( + "expected", + pem::parse(TEST_ECDSA_SHA2_NISTP256_NONE) + .unwrap() + .contents(), + ) + .unwrap(); + std::fs::write("found", &bytes).unwrap(); + + assert_eq!( + pem::parse(TEST_ECDSA_SHA2_NISTP256_NONE) + .unwrap() + .contents(), + bytes + ); } #[test] diff --git a/lib/cluelessh-keys/src/public.rs b/lib/cluelessh-keys/src/public.rs index 6f5892c..0d6f39e 100644 --- a/lib/cluelessh-keys/src/public.rs +++ b/lib/cluelessh-keys/src/public.rs @@ -5,7 +5,6 @@ use std::fmt::Display; use base64::Engine; -use ed25519_dalek::VerifyingKey; use tracing::debug; use cluelessh_format::{ParseError, Reader, Writer}; @@ -29,17 +28,27 @@ impl PublicKey { let k = match alg { "ssh-ed25519" => { + // let len = p.u32()?; if len != 32 { return Err(ParseError(format!("incorrect ed25519 len: {len}"))); } let public_key = p.array::<32>()?; - let public_key = VerifyingKey::from_bytes(&public_key) + let public_key = ed25519_dalek::VerifyingKey::from_bytes(&public_key) .map_err(|_| ParseError(format!("invalid ed25519 public key")))?; Self::Ed25519 { public_key } } "ecdsa-sha2-nistp256" => { - todo!() + // + let params = p.utf8_string()?; + if params != "nistp256" { + return Err(ParseError(format!("curve parameter mismatch: {params}"))); + } + let q = p.string()?; + let public_key = p256::ecdsa::VerifyingKey::from_sec1_bytes(q) + .map_err(|_| ParseError("invalid public key format".to_owned()))?; + + Self::EcdsaSha2NistP256 { public_key } } _ => return Err(ParseError(format!("unsupported key type: {alg}"))), }; @@ -51,6 +60,7 @@ impl PublicKey { p.string(self.algorithm_name()); match self { Self::Ed25519 { public_key } => { + // p.string(public_key.as_bytes()); } Self::EcdsaSha2NistP256 { public_key } => { @@ -109,11 +119,47 @@ impl Display for PublicKey { Self::EcdsaSha2NistP256 { .. } => { let encoded_pubkey = b64encode(&self.to_wire_encoding()); write!(f, "{} {encoded_pubkey}", self.algorithm_name()) - }, + } } } } fn b64encode(bytes: &[u8]) -> String { - base64::prelude::BASE64_STANDARD_NO_PAD.encode(bytes) + base64::prelude::BASE64_STANDARD.encode(bytes) +} + +#[cfg(test)] +mod tests { + use base64::Engine; + + use super::PublicKey; + + #[track_caller] + fn test_roundtrip(keys: &[&str]) { + for key_bytes in keys { + eprintln!("{key_bytes}"); + let key_bytes: Vec = base64::prelude::BASE64_STANDARD.decode(key_bytes).unwrap(); + + let key = PublicKey::from_wire_encoding(&key_bytes).unwrap(); + + assert_eq!(key.to_wire_encoding(), key_bytes); + } + } + + #[test] + fn ed25519() { + test_roundtrip(&[ + "AAAAC3NzaC1lZDI1NTE5AAAAIJJKT1n+xPwS4ECXXPVB5U5gWwMpqa+FMvVuyFwbfvEg", + "AAAAC3NzaC1lZDI1NTE5AAAAINZ1yLdDhI2Vou/9qrPIUP8RU8Sg0WxLI2njtP5hkdL7", + "AAAAC3NzaC1lZDI1NTE5AAAAIAIIWlDvWkMEX8XIu6lxvd4cFOxeFUpH4ZReKuyS3h9l", + ]); + } + + #[test] + fn ecdsa_sha2_nistp256() { + test_roundtrip(&[ + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHZTdlJoLNb701EWnahywBv032Aby+Piza7TzKW1H6Z//Hni/rBcUgnMmG+Kc4XWp6zgny3FMFpviuL01eJbpY8=", + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCv8bAwK5tZBEpOgFe6tmnog6GHKzeXnOK/qewbH4yiGb9fq4LkSY8oK3WhVZdIwtc1n8j9dNc4aGMURNlVBNKc=", + ]); + } }