diff --git a/Cargo.lock b/Cargo.lock index 5e84cb0..ef4dad6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1064,7 +1064,6 @@ dependencies = [ "crypto-bigint", "ctr", "ed25519-dalek", - "eyre", "hex-literal", "p256", "poly1305", diff --git a/fakesshd/src/main.rs b/fakesshd/src/main.rs index 9c40b9e..77db2a7 100644 --- a/fakesshd/src/main.rs +++ b/fakesshd/src/main.rs @@ -118,13 +118,10 @@ async fn handle_connection( if let Err(err) = state.recv_bytes(&buf[..read]) { match err { - SshStatus::ClientError(err) => { + SshStatus::PeerError(err) => { info!(?err, "disconnecting client after invalid operation"); return Ok(()); } - SshStatus::ServerError(err) => { - return Err(err); - } SshStatus::Disconnect => { info!("Received disconnect from client"); return Ok(()); diff --git a/ssh-transport/Cargo.toml b/ssh-transport/Cargo.toml index 66305a3..0edb481 100644 --- a/ssh-transport/Cargo.toml +++ b/ssh-transport/Cargo.toml @@ -10,7 +10,6 @@ chacha20 = "0.9.1" crypto-bigint = "0.5.5" ctr = "0.9.2" ed25519-dalek = "2.1.1" -eyre = "0.6.12" p256 = { version = "0.13.2", features = ["ecdh", "ecdsa"] } poly1305 = "0.8.0" rand_core = "0.6.4" diff --git a/ssh-transport/src/client.rs b/ssh-transport/src/client.rs index d350cef..6ebffd7 100644 --- a/ssh-transport/src/client.rs +++ b/ssh-transport/src/client.rs @@ -4,7 +4,7 @@ use tracing::{debug, info, trace}; use crate::{ crypto::{ - self, AlgorithmName, AlgorithmNegotiation, EncryptionAlgorithm, HostKeySigningAlgorithm, + self, AlgorithmName, EncodedSshSignature, EncryptionAlgorithm, HostKeySigningAlgorithm, KeyExchangeSecret, SupportedAlgorithms, }, numbers, @@ -29,6 +29,7 @@ enum ClientState { KexInit { client_ident: Vec, server_ident: Vec, + client_kexinit: Vec, }, DhKeyInit { client_ident: Vec, @@ -37,7 +38,17 @@ enum ClientState { server_hostkey_algorithm: HostKeySigningAlgorithm, encryption_client_to_server: EncryptionAlgorithm, encryption_server_to_client: EncryptionAlgorithm, + client_kexinit: Vec, + server_kexinit: Vec, }, + NewKeys { + h: [u8; 32], + k: Vec, + encryption_client_to_server: EncryptionAlgorithm, + encryption_server_to_client: EncryptionAlgorithm, + }, + ServiceRequest, + Open, } impl ClientConnection { @@ -107,6 +118,7 @@ impl ClientConnection { ClientState::KexInit { client_ident, server_ident, + client_kexinit, } => { let mut kexinit = packet.payload_parser(); let packet_type = kexinit.u8()?; @@ -119,7 +131,7 @@ impl ClientConnection { let sup_algs = SupportedAlgorithms::secure(); - let cookie = kexinit.array::<16>()?; + let _cookie = kexinit.array::<16>()?; let kex_algorithm = kexinit.name_list()?; let kex_algorithm = sup_algs.key_exchange.find(kex_algorithm.0)?; @@ -179,6 +191,8 @@ impl ClientConnection { server_hostkey_algorithm, encryption_client_to_server, encryption_server_to_client, + client_kexinit: mem::take(client_kexinit), + server_kexinit: packet.payload, }; } ClientState::DhKeyInit { @@ -188,6 +202,8 @@ impl ClientConnection { server_hostkey_algorithm, encryption_client_to_server, encryption_server_to_client, + client_kexinit, + server_kexinit, } => { let mut dh = packet.payload_parser(); @@ -199,12 +215,85 @@ impl ClientConnection { )); } - let sever_host_key = dh.string()?; + let server_hostkey = dh.string()?; let server_ephermal_key = dh.string()?; let signature = dh.string()?; - let shared_secret = - (mem::take(kex_secret).unwrap().exchange)(server_ephermal_key)?; + let kex_secret = mem::take(kex_secret).unwrap(); + let shared_secret = (kex_secret.exchange)(server_ephermal_key)?; + + let hash = crypto::key_exchange_hash( + client_ident, + server_ident, + client_kexinit, + server_kexinit, + server_hostkey, + &kex_secret.pubkey, + server_ephermal_key, + &shared_secret, + ); + + (server_hostkey_algorithm.verify)( + server_hostkey, + &hash, + &EncodedSshSignature(signature.to_vec()), + )?; + + // eprintln!("client_public_key: {:x?}", kex_secret.pubkey); + // eprintln!("server_public_key: {:x?}", server_ephermal_key); + // eprintln!("shared_secret: {:x?}", shared_secret); + // eprintln!("hash: {:x?}", hash); + + self.packet_transport.queue_packet(Packet { + payload: vec![numbers::SSH_MSG_NEWKEYS], + }); + self.state = ClientState::NewKeys { + h: hash, + k: shared_secret, + encryption_client_to_server: *encryption_client_to_server, + encryption_server_to_client: *encryption_server_to_client, + }; + } + ClientState::NewKeys { + h, + k, + encryption_client_to_server, + encryption_server_to_client, + } => { + if packet.payload != [numbers::SSH_MSG_NEWKEYS] { + return Err(peer_error!("did not send SSH_MSG_NEWKEYS")); + } + + self.packet_transport.set_key( + *h, + k, + *encryption_client_to_server, + *encryption_server_to_client, + false, + ); + + debug!("Requestin ssh-userauth service"); + self.packet_transport + .queue_packet(Packet::new_msg_service_request(b"ssh-userauth")); + + self.state = ClientState::ServiceRequest; + } + ClientState::ServiceRequest => { + let mut accept = packet.payload_parser(); + let packet_type = accept.u8()?; + if packet_type != numbers::SSH_MSG_SERVICE_ACCEPT { + return Err(peer_error!("did not accept service")); + } + let service = accept.utf8_string()?; + if service != "ssh-userauth" { + return Err(peer_error!("server accepted the wrong service: {service}")); + } + + debug!("Connection has been opened successfully"); + self.state = ClientState::Open; + } + ClientState::Open => { + self.plaintext_packets.push_back(packet); } } } @@ -240,11 +329,13 @@ impl ClientConnection { kexinit.u32(0); // reserved let kexinit = kexinit.finish(); - self.packet_transport - .queue_packet(Packet { payload: kexinit }); + self.packet_transport.queue_packet(Packet { + payload: kexinit.clone(), + }); self.state = ClientState::KexInit { client_ident, server_ident, + client_kexinit: kexinit, }; } } diff --git a/ssh-transport/src/crypto.rs b/ssh-transport/src/crypto.rs index 2c5790a..43bf4d8 100644 --- a/ssh-transport/src/crypto.rs +++ b/ssh-transport/src/crypto.rs @@ -5,7 +5,7 @@ use sha2::Digest; use crate::{ packet::{EncryptedPacket, MsgKind, Packet, RawPacket}, - parse::{self, Writer}, + parse::{self, Parser, Writer}, peer_error, Msg, Result, SshRng, }; @@ -112,6 +112,8 @@ pub struct HostKeySigningAlgorithm { hostkey_private: Vec, public_key: fn(private_key: &[u8]) -> EncodedSshPublicHostKey, sign: fn(private_key: &[u8], data: &[u8]) -> EncodedSshSignature, + pub verify: + fn(public_key: &[u8], message: &[u8], signature: &EncodedSshSignature) -> Result<()>, } impl AlgorithmName for HostKeySigningAlgorithm { @@ -153,6 +155,37 @@ pub fn hostkey_ed25519(hostkey_private: Vec) -> HostKeySigningAlgorithm { data.string(&signature.to_bytes()); EncodedSshSignature(data.finish()) }, + verify: |public_key, message, signature| { + // Parse out public key + let mut public_key = Parser::new(public_key); + let public_key_alg = public_key.string()?; + if public_key_alg != b"ssh-ed25519" { + return Err(peer_error!("incorrect algorithm public host key")); + } + let public_key = public_key.string()?; + let Ok(public_key) = public_key.try_into() else { + return Err(peer_error!("incorrect length for public host key")); + }; + let public_key = ed25519_dalek::VerifyingKey::from_bytes(public_key) + .map_err(|err| peer_error!("incorrect public host key: {err}"))?; + + // Parse out signature + let mut signature = Parser::new(&signature.0); + let alg = signature.string()?; + if alg != b"ssh-ed25519" { + return Err(peer_error!("incorrect algorithm for signature")); + } + let signature = signature.string()?; + let Ok(signature) = signature.try_into() else { + return Err(peer_error!("incorrect length for signature")); + }; + let signature = ed25519_dalek::Signature::from_bytes(signature); + + // Verify + public_key + .verify_strict(message, &signature) + .map_err(|err| peer_error!("incorrect signature: {err}")) + }, } } pub fn hostkey_ecdsa_sha2_p256(hostkey_private: Vec) -> HostKeySigningAlgorithm { @@ -186,6 +219,7 @@ pub fn hostkey_ecdsa_sha2_p256(hostkey_private: Vec) -> HostKeySigningAlgori data.string(&signature_blob.finish()); EncodedSshSignature(data.finish()) }, + verify: |_public_key, _message, _signature| todo!("ecdsa p256 verification"), } } @@ -259,8 +293,8 @@ impl SupportedAlgorithms { pub(crate) struct Session { session_id: [u8; 32], - client_to_server: Tunnel, - server_to_client: Tunnel, + from_peer: Tunnel, + to_peer: Tunnel, } struct Tunnel { @@ -282,6 +316,7 @@ pub(crate) trait Keys: Send + Sync + 'static { k: &[u8], encryption_client_to_server: EncryptionAlgorithm, encryption_server_to_client: EncryptionAlgorithm, + is_server: bool, ) -> Result<(), ()>; } @@ -303,6 +338,7 @@ impl Keys for Plaintext { _: &[u8], _: EncryptionAlgorithm, _: EncryptionAlgorithm, + _: bool, ) -> Result<(), ()> { Err(()) } @@ -314,6 +350,7 @@ impl Session { k: &[u8], encryption_client_to_server: EncryptionAlgorithm, encryption_server_to_client: EncryptionAlgorithm, + is_server: bool, ) -> Self { Self::from_keys( h, @@ -321,6 +358,7 @@ impl Session { k, encryption_client_to_server, encryption_server_to_client, + is_server, ) } @@ -331,26 +369,32 @@ impl Session { k: &[u8], alg_c2s: EncryptionAlgorithm, alg_s2c: EncryptionAlgorithm, + is_server: bool, ) -> Self { + let c2s = Tunnel { + algorithm: alg_c2s, + state: { + let mut state = derive_key(k, h, "C", session_id, alg_c2s.key_size); + let iv = derive_key(k, h, "A", session_id, alg_c2s.iv_size); + state.extend_from_slice(&iv); + state + }, + }; + let s2c = Tunnel { + algorithm: alg_s2c, + state: { + let mut state = derive_key(k, h, "D", session_id, alg_s2c.key_size); + state.extend_from_slice(&derive_key(k, h, "B", session_id, alg_s2c.iv_size)); + state + }, + }; + + let (from_peer, to_peer) = if is_server { (c2s, s2c) } else { (s2c, c2s) }; + Self { session_id, - client_to_server: Tunnel { - algorithm: alg_c2s, - state: { - let mut state = derive_key(k, h, "C", session_id, alg_c2s.key_size); - let iv = derive_key(k, h, "A", session_id, alg_c2s.iv_size); - state.extend_from_slice(&iv); - state - }, - }, - server_to_client: Tunnel { - algorithm: alg_s2c, - state: { - let mut state = derive_key(k, h, "D", session_id, alg_s2c.key_size); - state.extend_from_slice(&derive_key(k, h, "B", session_id, alg_s2c.iv_size)); - state - }, - }, + from_peer, + to_peer, // integrity_key_client_to_server: derive("E").into(), // integrity_key_server_to_client: derive("F").into(), } @@ -359,27 +403,16 @@ impl Session { impl Keys for Session { fn decrypt_len(&mut self, bytes: &mut [u8; 4], packet_number: u64) { - (self.client_to_server.algorithm.decrypt_len)( - &mut self.client_to_server.state, - bytes, - packet_number, - ); + (self.from_peer.algorithm.decrypt_len)(&mut self.from_peer.state, bytes, packet_number); } fn decrypt_packet(&mut self, bytes: RawPacket, packet_number: u64) -> Result { - (self.client_to_server.algorithm.decrypt_packet)( - &mut self.client_to_server.state, - bytes, - packet_number, - ) + (self.from_peer.algorithm.decrypt_packet)(&mut self.from_peer.state, bytes, packet_number) } fn encrypt_packet_to_msg(&mut self, packet: Packet, packet_number: u64) -> Msg { - let packet = (self.server_to_client.algorithm.encrypt_packet)( - &mut self.server_to_client.state, - packet, - packet_number, - ); + let packet = + (self.to_peer.algorithm.encrypt_packet)(&mut self.to_peer.state, packet, packet_number); Msg(MsgKind::EncryptedPacket(packet)) } @@ -393,6 +426,7 @@ impl Keys for Session { k: &[u8], encryption_client_to_server: EncryptionAlgorithm, encryption_server_to_client: EncryptionAlgorithm, + is_server: bool, ) -> Result<(), ()> { *self = Self::from_keys( self.session_id, @@ -400,6 +434,7 @@ impl Keys for Session { k, encryption_client_to_server, encryption_server_to_client, + is_server, ); Ok(()) } @@ -445,3 +480,44 @@ pub(crate) fn encode_mpint_for_hash(key: &[u8], mut add_to_hash: impl FnMut(&[u8 } add_to_hash(key); } + +pub fn key_exchange_hash( + client_ident: &[u8], + server_ident: &[u8], + client_kexinit: &[u8], + server_kexinit: &[u8], + server_hostkey: &[u8], + eph_client_public_key: &[u8], + eph_server_public_key: &[u8], + shared_secret: &[u8], +) -> [u8; 32] { + let mut hash = sha2::Sha256::new(); + let add_hash = |hash: &mut sha2::Sha256, bytes: &[u8]| { + hash.update(bytes); + }; + let hash_string = |hash: &mut sha2::Sha256, bytes: &[u8]| { + add_hash(hash, &u32::to_be_bytes(bytes.len() as u32)); + add_hash(hash, bytes); + }; + let hash_mpint = |hash: &mut sha2::Sha256, bytes: &[u8]| { + encode_mpint_for_hash(bytes, |data| add_hash(hash, data)); + }; + + // Strip the \r\n + hash_string(&mut hash, &client_ident[..(client_ident.len() - 2)]); // V_C + hash_string(&mut hash, &server_ident[..(server_ident.len() - 2)]); // V_S + + hash_string(&mut hash, client_kexinit); // I_C + hash_string(&mut hash, server_kexinit); // I_S + hash_string(&mut hash, server_hostkey); // K_S + + // For normal DH as in RFC4253, e and f are mpints. + // But for ECDH as defined in RFC5656, Q_C and Q_S are strings. + // + hash_string(&mut hash, eph_client_public_key); // Q_C + hash_string(&mut hash, eph_server_public_key); // Q_S + hash_mpint(&mut hash, shared_secret); // K + + let hash = hash.finalize(); + hash.into() +} diff --git a/ssh-transport/src/lib.rs b/ssh-transport/src/lib.rs index 23e9805..77ecaa7 100644 --- a/ssh-transport/src/lib.rs +++ b/ssh-transport/src/lib.rs @@ -12,23 +12,14 @@ pub enum SshStatus { /// The client has sent a disconnect request, close the connection. /// This is not an error. Disconnect, - /// The client did something wrong. + /// The peer did something wrong. /// The connection should be closed and a notice may be logged, /// but this does not require operator intervention. - ClientError(String), - /// Something went wrong on the server. - /// The connection should be closed and an error should be logged. - // TODO: does this ever happen? - ServerError(eyre::Report), + PeerError(String), } pub type Result = std::result::Result; -impl From for SshStatus { - fn from(value: eyre::Report) -> Self { - Self::ServerError(value) - } -} pub trait SshRng { fn fill_bytes(&mut self, dest: &mut [u8]); @@ -57,6 +48,6 @@ impl rand_core::RngCore for SshRngRandAdapter<'_> { #[macro_export] macro_rules! peer_error { ($($tt:tt)*) => { - $crate::SshStatus::ClientError(::std::format!($($tt)*)) + $crate::SshStatus::PeerError(::std::format!($($tt)*)) }; } diff --git a/ssh-transport/src/packet.rs b/ssh-transport/src/packet.rs index 4adbb30..8e6b527 100644 --- a/ssh-transport/src/packet.rs +++ b/ssh-transport/src/packet.rs @@ -8,7 +8,7 @@ use tracing::{debug, trace}; use crate::crypto::{EncryptionAlgorithm, Keys, Plaintext, Session}; use crate::parse::{NameList, Parser, Writer}; use crate::Result; -use crate::{peer_error, numbers}; +use crate::{numbers, peer_error}; /// Frames the byte stream into packets. pub(crate) struct PacketTransport { @@ -114,18 +114,21 @@ impl PacketTransport { k: &[u8], encryption_client_to_server: EncryptionAlgorithm, encryption_server_to_client: EncryptionAlgorithm, + is_server: bool, ) { if let Err(()) = self.keys.rekey( h, k, encryption_client_to_server, encryption_server_to_client, + is_server, ) { self.keys = Box::new(Session::new( h, k, encryption_client_to_server, encryption_server_to_client, + is_server, )); } } @@ -251,9 +254,7 @@ impl<'a> KeyExchangeInitPacket<'a> { let kind = c.u8()?; if kind != numbers::SSH_MSG_KEXINIT { - return Err(peer_error!( - "expected SSH_MSG_KEXINIT packet, found {kind}" - )); + return Err(peer_error!("expected SSH_MSG_KEXINIT packet, found {kind}")); } let cookie = c.array::<16>()?; let kex_algorithms = c.name_list()?; @@ -418,9 +419,7 @@ impl PacketParser { // 'padding_length', 'payload', 'random padding', and 'mac'). // Implementations SHOULD support longer packets, where they might be needed. if packet_length > 500_000 { - return Err(peer_error!( - "packet too large (>500_000): {packet_length}" - )); + return Err(peer_error!("packet too large (>500_000): {packet_length}")); } let remaining_len = std::cmp::min(bytes.len(), packet_length - (self.raw_data.len() - 4)); diff --git a/ssh-transport/src/packet/ctors.rs b/ssh-transport/src/packet/ctors.rs index 6bac0d6..217d7a6 100644 --- a/ssh-transport/src/packet/ctors.rs +++ b/ssh-transport/src/packet/ctors.rs @@ -49,6 +49,7 @@ ctors! { // Transport layer protocol: // 1 to 19 Transport layer generic (e.g., disconnect, ignore, debug, etc.) + fn new_msg_service_request(SSH_MSG_SERVICE_REQUEST; service_name: string); // 20 to 29 Algorithm negotiation // 30 to 49 Key exchange method specific (numbers can be reused for different authentication methods) fn new_msg_kex_ecdh_init(SSH_MSG_KEX_ECDH_INIT; client_ephemeral_public_key_qc: string); diff --git a/ssh-transport/src/server.rs b/ssh-transport/src/server.rs index e1a897c..574e977 100644 --- a/ssh-transport/src/server.rs +++ b/ssh-transport/src/server.rs @@ -9,7 +9,6 @@ use crate::packet::{ use crate::parse::{NameList, Parser, Writer}; use crate::{numbers, Result}; use crate::{peer_error, Msg, SshRng, SshStatus}; -use sha2::Digest; use tracing::{debug, info, trace}; // This is definitely who we are. @@ -215,47 +214,24 @@ impl ServerConnection { let server_secret = (kex_algorithm.generate_secret)(&mut *self.rng); let server_public_key = server_secret.pubkey; let shared_secret = (server_secret.exchange)(client_public_key)?; - let pub_hostkey = server_host_key_algorithm.public_key(); - let mut hash = sha2::Sha256::new(); - let add_hash = |hash: &mut sha2::Sha256, bytes: &[u8]| { - hash.update(bytes); - }; - let hash_string = |hash: &mut sha2::Sha256, bytes: &[u8]| { - add_hash(hash, &u32::to_be_bytes(bytes.len() as u32)); - add_hash(hash, bytes); - }; - let hash_mpint = |hash: &mut sha2::Sha256, bytes: &[u8]| { - crypto::encode_mpint_for_hash(bytes, |data| add_hash(hash, data)); - }; - - hash_string( - &mut hash, - &client_identification[..(client_identification.len() - 2)], - ); // V_C - hash_string( - &mut hash, - &SERVER_IDENTIFICATION[..(SERVER_IDENTIFICATION.len() - 2)], - ); // V_S - hash_string(&mut hash, client_kexinit); // I_C - hash_string(&mut hash, server_kexinit); // I_S - hash_string(&mut hash, &pub_hostkey.0); // K_S - - // For normal DH as in RFC4253, e and f are mpints. - // But for ECDH as defined in RFC5656, Q_C and Q_S are strings. - // - hash_string(&mut hash, client_public_key); // Q_C - hash_string(&mut hash, &server_public_key); // Q_S - hash_mpint(&mut hash, &shared_secret); // K - - let hash = hash.finalize(); + let hash = crypto::key_exchange_hash( + &client_identification, + SERVER_IDENTIFICATION, + client_kexinit, + server_kexinit, + &pub_hostkey.0, + client_public_key, + &server_public_key, + &shared_secret, + ); let signature = server_host_key_algorithm.sign(&hash); - // eprintln!("client_public_key: {:x?}", client_public_key.0); - // eprintln!("server_public_key: {:x?}", server_public_key.as_bytes()); - // eprintln!("shared_secret: {:x?}", shared_secret.as_bytes()); + // eprintln!("client_public_key: {:x?}", client_public_key); + // eprintln!("server_public_key: {:x?}", server_public_key); + // eprintln!("shared_secret: {:x?}", shared_secret); // eprintln!("hash: {:x?}", hash); let packet = Packet::new_msg_kex_ecdh_reply( @@ -266,7 +242,7 @@ impl ServerConnection { self.packet_transport.queue_packet(packet); self.state = ServerState::NewKeys { - h: hash.into(), + h: hash, k: shared_secret, encryption_client_to_server: *encryption_client_to_server, encryption_server_to_client: *encryption_server_to_client, @@ -285,11 +261,13 @@ impl ServerConnection { self.packet_transport.queue_packet(Packet { payload: vec![numbers::SSH_MSG_NEWKEYS], }); + self.packet_transport.set_key( *h, k, *encryption_client_to_server, *encryption_server_to_client, + true, ); self.state = ServerState::ServiceRequest {}; } diff --git a/ssh/src/main.rs b/ssh/src/main.rs index 7f5674d..40e7b25 100644 --- a/ssh/src/main.rs +++ b/ssh/src/main.rs @@ -6,7 +6,7 @@ use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream, }; -use tracing::{info, trace}; +use tracing::info; use ssh_protocol::{ transport::{self}, @@ -46,7 +46,6 @@ async fn main() -> eyre::Result<()> { loop { while let Some(msg) = state.next_msg_to_send() { - trace!("Writing packet {msg:?}"); conn.write_all(&msg.to_bytes()) .await .wrap_err("writing response")?; @@ -63,13 +62,10 @@ async fn main() -> eyre::Result<()> { if let Err(err) = state.recv_bytes(&buf[..read]) { match err { - SshStatus::ClientError(err) => { + SshStatus::PeerError(err) => { info!(?err, "disconnecting client after invalid operation"); return Ok(()); } - SshStatus::ServerError(err) => { - return Err(err); - } SshStatus::Disconnect => { info!("Received disconnect from client"); return Ok(());