the clueless rename

This commit is contained in:
nora 2024-08-24 00:51:47 +02:00
parent ea28daca0c
commit 9ce60280b1
46 changed files with 264 additions and 262 deletions

View file

@ -0,0 +1,24 @@
[package]
name = "cluelessh-transport"
version = "0.1.0"
edition = "2021"
[dependencies]
aes = "0.8.4"
aes-gcm = "0.10.3"
chacha20 = "0.9.1"
crypto-bigint = "0.5.5"
ctr = "0.9.2"
ed25519-dalek = "2.1.1"
p256 = { version = "0.13.2", features = ["ecdh", "ecdsa"] }
poly1305 = "0.8.0"
rand_core = "0.6.4"
sha2 = "0.10.8"
subtle = "2.6.1"
x25519-dalek = "2.0.1"
tracing.workspace = true
base64 = "0.22.1"
[dev-dependencies]
hex-literal = "0.4.1"

View file

@ -0,0 +1,15 @@
# cluelessh-transport
Transport layer of SSH.
Based on [RFC 4253 The Secure Shell (SSH) Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc4253)
and [RFC 4251 The Secure Shell (SSH) Protocol Architecture](https://datatracker.ietf.org/doc/html/rfc4251)
and [RFC 4250 The Secure Shell (SSH) Protocol Assigned Numbers](https://datatracker.ietf.org/doc/html/rfc4250).
Other relevant RFCs:
- [RFC 4344 The Secure Shell (SSH) Transport Layer Encryption Modes](https://datatracker.ietf.org/doc/html/rfc4344)
- [RFC 5649 AES Galois Counter Mode for the Secure Shell Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc5647)
- [RFC 5656 Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer](https://datatracker.ietf.org/doc/html/rfc5656)
- [RFC 6668 SHA-2 Data Integrity Verification for the Secure Shell (SSH) Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc6668)
- [RFC 8709 Ed25519 and Ed448 Public Key Algorithms for the Secure Shell (SSH) Protocol](https://datatracker.ietf.org/doc/html/rfc8709)
- [RFC 8731 Secure Shell (SSH) Key Exchange Method Using Curve25519 and Curve448](https://datatracker.ietf.org/doc/html/rfc8731)

View file

@ -0,0 +1,393 @@
use std::{collections::VecDeque, mem};
use tracing::{debug, info, trace};
use crate::{
crypto::{
self, AlgorithmName, EncodedSshSignature, EncryptionAlgorithm, HostKeySigningAlgorithm,
KeyExchangeSecret, SupportedAlgorithms,
},
numbers,
packet::{Packet, PacketTransport, ProtocolIdentParser},
parse::{NameList, Parser, Writer},
peer_error, Msg, Result, SshRng, SshStatus,
};
pub struct ClientConnection {
state: ClientState,
packet_transport: PacketTransport,
rng: Box<dyn SshRng + Send + Sync>,
plaintext_packets: VecDeque<Packet>,
pub abort_for_dos: bool,
}
enum ClientState {
ProtoExchange {
client_ident: Vec<u8>,
ident_parser: ProtocolIdentParser,
},
KexInit {
client_ident: Vec<u8>,
server_ident: Vec<u8>,
client_kexinit: Vec<u8>,
},
DhKeyInit {
client_ident: Vec<u8>,
server_ident: Vec<u8>,
kex_secret: Option<KeyExchangeSecret>,
server_hostkey_algorithm: HostKeySigningAlgorithm,
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
client_kexinit: Vec<u8>,
server_kexinit: Vec<u8>,
},
NewKeys {
h: [u8; 32],
k: Vec<u8>,
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
},
ServiceRequest {
session_identifier: [u8; 32],
},
Open {
session_identifier: [u8; 32],
},
}
impl ClientConnection {
pub fn new(rng: impl SshRng + Send + Sync + 'static) -> Self {
let client_ident = b"SSH-2.0-FakeSSH\r\n".to_vec();
let mut packet_transport = PacketTransport::new();
packet_transport.queue_send_protocol_info(client_ident.clone());
Self {
state: ClientState::ProtoExchange {
ident_parser: ProtocolIdentParser::new(),
client_ident,
},
packet_transport,
rng: Box::new(rng),
plaintext_packets: VecDeque::new(),
abort_for_dos: false,
}
}
pub fn recv_bytes(&mut self, bytes: &[u8]) -> Result<()> {
if let ClientState::ProtoExchange {
ident_parser,
client_ident,
} = &mut self.state
{
ident_parser.recv_bytes(bytes);
if let Some(server_ident) = ident_parser.get_peer_ident() {
let client_ident = mem::take(client_ident);
// This moves to the next state.
self.send_kexinit(client_ident, server_ident);
return Ok(());
}
return Ok(());
}
self.packet_transport.recv_bytes(bytes)?;
while let Some(packet) = self.packet_transport.recv_next_packet() {
let packet_type = packet.payload.first().unwrap_or(&0xFF);
let packet_type_string = numbers::packet_type_to_string(*packet_type);
trace!(%packet_type, %packet_type_string, packet_len = %packet.payload.len(), "Received packet");
// TODO: deduplicate with server
// Handle some packets ignoring the state.
match packet.payload.first().copied() {
Some(numbers::SSH_MSG_DISCONNECT) => {
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.1>
let mut p = Parser::new(&packet.payload[1..]);
let reason = p.u32()?;
let description = p.utf8_string()?;
let _language_tag = p.utf8_string()?;
let reason_string =
numbers::disconnect_reason_to_string(reason).unwrap_or("<unknown>");
info!(%reason, %reason_string, %description, "Server disconnecting");
return Err(SshStatus::Disconnect);
}
Some(numbers::SSH_MSG_IGNORE) => {
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.2>
let mut p = Parser::new(&packet.payload[1..]);
let _ = p.string()?;
continue;
}
Some(numbers::SSH_MSG_DEBUG) => {
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.3>
let mut p = Parser::new(&packet.payload[1..]);
let always_display = p.bool()?;
let msg = p.utf8_string()?;
let _language_tag = p.utf8_string()?;
if always_display {
info!(%msg, "Received debug message (SSH_MSG_DEBUG)");
} else {
debug!(%msg, "Received debug message (SSH_MSG_DEBUG)")
}
continue;
}
_ => {}
}
match &mut self.state {
ClientState::ProtoExchange { .. } => unreachable!("handled above"),
ClientState::KexInit {
client_ident,
server_ident,
client_kexinit,
} => {
let mut kexinit = packet.payload_parser();
let packet_type = kexinit.u8()?;
if packet_type != numbers::SSH_MSG_KEXINIT {
return Err(peer_error!(
"expected SSH_MSG_KEXINIT, found {}",
numbers::packet_type_to_string(packet_type)
));
}
let sup_algs = SupportedAlgorithms::secure();
let _cookie = kexinit.array::<16>()?;
let kex_algorithm = kexinit.name_list()?;
let kex_algorithm = sup_algs.key_exchange.find(kex_algorithm.0)?;
debug!(name = %kex_algorithm.name(), "Using KEX algorithm");
let server_hostkey_algorithm = kexinit.name_list()?;
let server_hostkey_algorithm =
sup_algs.hostkey.find(server_hostkey_algorithm.0)?;
debug!(name = %server_hostkey_algorithm.name(), "Using host key algorithm");
let encryption_algorithms_client_to_server = kexinit.name_list()?;
let encryption_client_to_server = sup_algs
.encryption_to_peer
.find(encryption_algorithms_client_to_server.0)?;
debug!(name = %encryption_client_to_server.name(), "Using encryption algorithm C->S");
let encryption_algorithms_server_to_client = kexinit.name_list()?;
let encryption_server_to_client = sup_algs
.encryption_from_peer
.find(encryption_algorithms_server_to_client.0)?;
debug!(name = %encryption_server_to_client.name(), "Using encryption algorithm S->C");
let mac_algorithms_client_to_server = kexinit.name_list()?;
let _mac_client_to_server = sup_algs
.mac_to_peer
.find(mac_algorithms_client_to_server.0)?;
let mac_algorithms_server_to_client = kexinit.name_list()?;
let _mac_server_to_client = sup_algs
.mac_from_peer
.find(mac_algorithms_server_to_client.0)?;
let compression_algorithms_client_to_server = kexinit.name_list()?;
let _compression_client_to_server = sup_algs
.compression_to_peer
.find(compression_algorithms_client_to_server.0)?;
let compression_algorithms_server_to_client = kexinit.name_list()?;
let _compression_server_to_client = sup_algs
.compression_from_peer
.find(compression_algorithms_server_to_client.0)?;
let _languages_client_to_server = kexinit.name_list()?;
let _languages_server_to_client = kexinit.name_list()?;
let first_kex_packet_follows = kexinit.bool()?;
if first_kex_packet_follows {
return Err(peer_error!("does not support guessed kex init packages"));
}
let kex_secret = (kex_algorithm.generate_secret)(&mut *self.rng);
self.packet_transport
.queue_packet(Packet::new_msg_kex_ecdh_init(&kex_secret.pubkey));
self.state = ClientState::DhKeyInit {
client_ident: mem::take(client_ident),
server_ident: mem::take(server_ident),
kex_secret: Some(kex_secret),
server_hostkey_algorithm,
encryption_client_to_server,
encryption_server_to_client,
client_kexinit: mem::take(client_kexinit),
server_kexinit: packet.payload,
};
}
ClientState::DhKeyInit {
client_ident,
server_ident,
kex_secret,
server_hostkey_algorithm,
encryption_client_to_server,
encryption_server_to_client,
client_kexinit,
server_kexinit,
} => {
let mut dh = packet.payload_parser();
let packet_type = dh.u8()?;
if packet_type != numbers::SSH_MSG_KEX_ECDH_REPLY {
return Err(peer_error!(
"expected SSH_MSG_KEX_ECDH_REPLY, found {}",
numbers::packet_type_to_string(packet_type)
));
}
if self.abort_for_dos {
return Err(peer_error!("early abort"));
}
let server_hostkey = dh.string()?;
let server_ephermal_key = dh.string()?;
let signature = dh.string()?;
let kex_secret = mem::take(kex_secret).unwrap();
let shared_secret = (kex_secret.exchange)(server_ephermal_key)?;
// The exchange hash serves as the session identifier.
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!("Requesting ssh-userauth service");
self.packet_transport
.queue_packet(Packet::new_msg_service_request(b"ssh-userauth"));
self.state = ClientState::ServiceRequest {
session_identifier: *h,
};
}
ClientState::ServiceRequest { session_identifier } => {
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 {
session_identifier: *session_identifier,
};
}
ClientState::Open { .. } => {
self.plaintext_packets.push_back(packet);
}
}
}
Ok(())
}
pub fn next_msg_to_send(&mut self) -> Option<Msg> {
self.packet_transport.next_msg_to_send()
}
pub fn next_plaintext_packet(&mut self) -> Option<Packet> {
self.plaintext_packets.pop_front()
}
pub fn send_plaintext_packet(&mut self, packet: Packet) {
self.packet_transport.queue_packet(packet);
}
pub fn is_open(&self) -> Option<[u8; 32]> {
match self.state {
ClientState::Open { session_identifier } => Some(session_identifier),
_ => None,
}
}
fn send_kexinit(&mut self, client_ident: Vec<u8>, server_ident: Vec<u8>) {
let mut cookie = [0; 16];
self.rng.fill_bytes(&mut cookie);
let mut kexinit = Writer::new();
kexinit.u8(numbers::SSH_MSG_KEXINIT);
kexinit.array(cookie);
kexinit.name_list(NameList::multi("curve25519-sha256,ecdh-sha2-nistp256")); // kex_algorithms
kexinit.name_list(NameList::multi("ssh-ed25519,ecdsa-sha2-nistp256")); // server_host_key_algorithms
kexinit.name_list(NameList::multi(
"chacha20-poly1305@openssh.com,aes256-gcm@openssh.com",
)); // encryption_algorithms_client_to_server
kexinit.name_list(NameList::multi(
"chacha20-poly1305@openssh.com,aes256-gcm@openssh.com",
)); // encryption_algorithms_server_to_client
kexinit.name_list(NameList::one("hmac-sha2-256")); // mac_algorithms_client_to_server
kexinit.name_list(NameList::one("hmac-sha2-256")); // mac_algorithms_server_to_client
kexinit.name_list(NameList::one("none")); // compression_algorithms_client_to_server
kexinit.name_list(NameList::one("none")); // compression_algorithms_server_to_client
kexinit.name_list(NameList::none()); // languages_client_to_server
kexinit.name_list(NameList::none()); // languages_server_to_client
kexinit.bool(false); // first_kex_packet_follows
kexinit.u32(0); // reserved
let kexinit = kexinit.finish();
self.packet_transport.queue_packet(Packet {
payload: kexinit.clone(),
});
self.state = ClientState::KexInit {
client_ident,
server_ident,
client_kexinit: kexinit,
};
}
}

View file

@ -0,0 +1,523 @@
pub mod encrypt;
use p256::ecdsa::signature::Signer;
use sha2::Digest;
use crate::{
packet::{EncryptedPacket, MsgKind, Packet, RawPacket},
parse::{self, Parser, Writer},
peer_error, Msg, Result, SshRng,
};
pub trait AlgorithmName {
fn name(&self) -> &'static str;
}
// Dummy algorithm.
impl AlgorithmName for &'static str {
fn name(&self) -> &'static str {
self
}
}
#[derive(Clone, Copy)]
pub struct KexAlgorithm {
name: &'static str,
/// Generate an ephemeral key for the exchange.
pub generate_secret: fn(random: &mut (dyn SshRng + Send + Sync)) -> KeyExchangeSecret,
}
impl AlgorithmName for KexAlgorithm {
fn name(&self) -> &'static str {
self.name
}
}
pub struct KeyExchangeSecret {
/// Q_x
pub pubkey: Vec<u8>,
/// Does the exchange, returning the shared secret K.
pub exchange: Box<dyn FnOnce(&[u8]) -> Result<Vec<u8>> + Send + Sync>,
}
/// <https://datatracker.ietf.org/doc/html/rfc8731>
pub const KEX_CURVE_25519_SHA256: KexAlgorithm = KexAlgorithm {
name: "curve25519-sha256",
generate_secret: |rng| {
let secret = x25519_dalek::EphemeralSecret::random_from_rng(crate::SshRngRandAdapter(rng));
let my_public_key = x25519_dalek::PublicKey::from(&secret);
KeyExchangeSecret {
pubkey: my_public_key.as_bytes().to_vec(),
exchange: Box::new(move |peer_public_key| {
let Ok(peer_public_key) = <[u8; 32]>::try_from(peer_public_key) else {
return Err(crate::peer_error!(
"invalid x25519 public key length, should be 32, was: {}",
peer_public_key.len()
));
};
let peer_public_key = x25519_dalek::PublicKey::from(peer_public_key);
let shared_secret = secret.diffie_hellman(&peer_public_key); // K
Ok(shared_secret.as_bytes().to_vec())
}),
}
},
};
/// <https://datatracker.ietf.org/doc/html/rfc5656>
pub const KEX_ECDH_SHA2_NISTP256: KexAlgorithm = KexAlgorithm {
name: "ecdh-sha2-nistp256",
generate_secret: |rng| {
let secret = p256::ecdh::EphemeralSecret::random(&mut crate::SshRngRandAdapter(rng));
let my_public_key = p256::EncodedPoint::from(secret.public_key());
KeyExchangeSecret {
pubkey: my_public_key.as_bytes().to_vec(),
exchange: Box::new(move |peer_public_key| {
let peer_public_key =
p256::PublicKey::from_sec1_bytes(peer_public_key).map_err(|_| {
crate::peer_error!(
"invalid p256 public key length: {}",
peer_public_key.len()
)
})?;
let shared_secret = secret.diffie_hellman(&peer_public_key); // K
Ok(shared_secret.raw_secret_bytes().to_vec())
}),
}
},
};
#[derive(Clone, Copy)]
pub struct EncryptionAlgorithm {
name: &'static str,
iv_size: usize,
key_size: usize,
decrypt_len: fn(state: &mut [u8], bytes: &mut [u8], packet_number: u64),
decrypt_packet: fn(state: &mut [u8], bytes: RawPacket, packet_number: u64) -> Result<Packet>,
encrypt_packet: fn(state: &mut [u8], packet: Packet, packet_number: u64) -> EncryptedPacket,
}
impl AlgorithmName for EncryptionAlgorithm {
fn name(&self) -> &'static str {
self.name
}
}
pub struct EncodedSshPublicHostKey(pub Vec<u8>);
pub struct EncodedSshSignature(pub Vec<u8>);
pub struct HostKeySigningAlgorithm {
name: &'static str,
hostkey_private: Vec<u8>,
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 {
fn name(&self) -> &'static str {
self.name
}
}
impl HostKeySigningAlgorithm {
pub fn sign(&self, data: &[u8]) -> EncodedSshSignature {
(self.sign)(&self.hostkey_private, data)
}
pub fn public_key(&self) -> EncodedSshPublicHostKey {
(self.public_key)(&self.hostkey_private)
}
}
pub fn hostkey_ed25519(hostkey_private: Vec<u8>) -> HostKeySigningAlgorithm {
HostKeySigningAlgorithm {
name: "ssh-ed25519",
hostkey_private,
public_key: |key| {
let key = ed25519_dalek::SigningKey::from_bytes(key.try_into().unwrap());
let public_key = key.verifying_key();
// <https://datatracker.ietf.org/doc/html/rfc8709#section-4>
let mut data = Writer::new();
data.string(b"ssh-ed25519");
data.string(public_key.as_bytes());
EncodedSshPublicHostKey(data.finish())
},
sign: |key, data| {
let key = ed25519_dalek::SigningKey::from_bytes(key.try_into().unwrap());
let signature = key.sign(data);
// <https://datatracker.ietf.org/doc/html/rfc8709#section-6>
let mut data = Writer::new();
data.string(b"ssh-ed25519");
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<u8>) -> HostKeySigningAlgorithm {
HostKeySigningAlgorithm {
name: "ecdsa-sha2-nistp256",
hostkey_private,
public_key: |key| {
let key = p256::ecdsa::SigningKey::from_slice(key).unwrap();
let public_key = key.verifying_key();
let mut data = Writer::new();
// <https://datatracker.ietf.org/doc/html/rfc5656#section-3.1>
data.string(b"ecdsa-sha2-nistp256");
data.string(b"nistp256");
// > 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())
},
sign: |key, data| {
let key = p256::ecdsa::SigningKey::from_slice(key).unwrap();
let signature: p256::ecdsa::Signature = key.sign(data);
let (r, s) = signature.split_scalars();
// <https://datatracker.ietf.org/doc/html/rfc5656#section-3.1.2>
let mut data = Writer::new();
data.string(b"ecdsa-sha2-nistp256");
let mut signature_blob = Writer::new();
signature_blob.mpint(p256::U256::from(r.as_ref()));
signature_blob.mpint(p256::U256::from(s.as_ref()));
data.string(&signature_blob.finish());
EncodedSshSignature(data.finish())
},
verify: |_public_key, _message, _signature| todo!("ecdsa p256 verification"),
}
}
pub struct AlgorithmNegotiation<T> {
pub supported: Vec<T>,
}
impl<T: AlgorithmName> AlgorithmNegotiation<T> {
pub fn find(mut self, peer_supports: &str) -> Result<T> {
for client_alg in peer_supports.split(',') {
if let Some(alg) = self
.supported
.iter()
.position(|alg| alg.name() == client_alg)
{
return Ok(self.supported.remove(alg));
}
}
Err(peer_error!(
"peer does not support any matching algorithm: peer supports: {peer_supports:?}"
))
}
}
pub struct SupportedAlgorithms {
pub key_exchange: AlgorithmNegotiation<KexAlgorithm>,
pub hostkey: AlgorithmNegotiation<HostKeySigningAlgorithm>,
pub encryption_to_peer: AlgorithmNegotiation<EncryptionAlgorithm>,
pub encryption_from_peer: AlgorithmNegotiation<EncryptionAlgorithm>,
pub mac_to_peer: AlgorithmNegotiation<&'static str>,
pub mac_from_peer: AlgorithmNegotiation<&'static str>,
pub compression_to_peer: AlgorithmNegotiation<&'static str>,
pub compression_from_peer: AlgorithmNegotiation<&'static str>,
}
impl SupportedAlgorithms {
/// A secure default using elliptic curves and AEAD.
pub fn secure() -> Self {
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()),
],
},
encryption_to_peer: AlgorithmNegotiation {
supported: vec![encrypt::CHACHA20POLY1305, encrypt::AES256_GCM],
},
encryption_from_peer: AlgorithmNegotiation {
supported: vec![encrypt::CHACHA20POLY1305, encrypt::AES256_GCM],
},
mac_to_peer: AlgorithmNegotiation {
supported: vec!["hmac-sha2-256", "hmac-sha2-256-etm@openssh.com"],
},
mac_from_peer: AlgorithmNegotiation {
supported: vec!["hmac-sha2-256", "hmac-sha2-256-etm@openssh.com"],
},
compression_to_peer: AlgorithmNegotiation {
supported: vec!["none"],
},
compression_from_peer: AlgorithmNegotiation {
supported: vec!["none"],
},
}
}
}
pub(crate) struct Session {
session_id: [u8; 32],
from_peer: Tunnel,
to_peer: Tunnel,
}
struct Tunnel {
/// `key || IV`
state: Vec<u8>,
algorithm: EncryptionAlgorithm,
}
pub(crate) trait Keys: Send + Sync + 'static {
fn decrypt_len(&mut self, bytes: &mut [u8; 4], packet_number: u64);
fn decrypt_packet(&mut self, raw_packet: RawPacket, packet_number: u64) -> Result<Packet>;
fn encrypt_packet_to_msg(&mut self, packet: Packet, packet_number: u64) -> Msg;
fn additional_mac_len(&self) -> usize;
fn rekey(
&mut self,
h: [u8; 32],
k: &[u8],
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
is_server: bool,
) -> Result<(), ()>;
}
pub(crate) struct Plaintext;
impl Keys for Plaintext {
fn decrypt_len(&mut self, _: &mut [u8; 4], _: u64) {}
fn decrypt_packet(&mut self, raw: RawPacket, _: u64) -> Result<Packet> {
Packet::from_full(raw.rest())
}
fn encrypt_packet_to_msg(&mut self, packet: Packet, _: u64) -> Msg {
Msg(MsgKind::PlaintextPacket(packet))
}
fn additional_mac_len(&self) -> usize {
0
}
fn rekey(
&mut self,
_: [u8; 32],
_: &[u8],
_: EncryptionAlgorithm,
_: EncryptionAlgorithm,
_: bool,
) -> Result<(), ()> {
Err(())
}
}
impl Session {
pub(crate) fn new(
h: [u8; 32],
k: &[u8],
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
is_server: bool,
) -> Self {
Self::from_keys(
h,
h,
k,
encryption_client_to_server,
encryption_server_to_client,
is_server,
)
}
/// <https://datatracker.ietf.org/doc/html/rfc4253#section-7.2>
fn from_keys(
session_id: [u8; 32],
h: [u8; 32],
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,
from_peer,
to_peer,
// integrity_key_client_to_server: derive("E").into(),
// integrity_key_server_to_client: derive("F").into(),
}
}
}
impl Keys for Session {
fn decrypt_len(&mut self, bytes: &mut [u8; 4], packet_number: u64) {
(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<Packet> {
(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.to_peer.algorithm.encrypt_packet)(&mut self.to_peer.state, packet, packet_number);
Msg(MsgKind::EncryptedPacket(packet))
}
fn additional_mac_len(&self) -> usize {
poly1305::BLOCK_SIZE
}
fn rekey(
&mut self,
h: [u8; 32],
k: &[u8],
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
is_server: bool,
) -> Result<(), ()> {
*self = Self::from_keys(
self.session_id,
h,
k,
encryption_client_to_server,
encryption_server_to_client,
is_server,
);
Ok(())
}
}
/// Derive a key from the shared secret K and exchange hash H.
/// <https://datatracker.ietf.org/doc/html/rfc4253#section-7.2>
fn derive_key(
k: &[u8],
h: [u8; 32],
letter: &str,
session_id: [u8; 32],
key_size: usize,
) -> Vec<u8> {
let sha2len = sha2::Sha256::output_size();
let padded_key_size = key_size.next_multiple_of(sha2len);
let mut output = vec![0; padded_key_size];
for i in 0..(padded_key_size / sha2len) {
let mut hash = <sha2::Sha256 as sha2::Digest>::new();
encode_mpint_for_hash(k, |data| hash.update(data));
hash.update(h);
if i == 0 {
hash.update(letter.as_bytes());
hash.update(session_id);
} else {
hash.update(&output[..(i * sha2len)]);
}
output[(i * sha2len)..][..sha2len].copy_from_slice(&hash.finalize())
}
output.truncate(key_size);
output
}
pub(crate) fn encode_mpint_for_hash(key: &[u8], mut add_to_hash: impl FnMut(&[u8])) {
let (key, pad_zero) = parse::fixup_mpint(key);
add_to_hash(&u32::to_be_bytes((key.len() + (pad_zero as usize)) as u32));
if pad_zero {
add_to_hash(&[0]);
}
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.
// <https://datatracker.ietf.org/doc/html/rfc5656#section-4>
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()
}

View file

@ -0,0 +1,253 @@
use crate::Result;
use aes_gcm::{aead::AeadMutInPlace, KeyInit};
use chacha20::cipher::{StreamCipher, StreamCipherSeek};
use subtle::ConstantTimeEq;
use crate::packet::{EncryptedPacket, Packet, RawPacket};
use super::EncryptionAlgorithm;
pub const CHACHA20POLY1305: EncryptionAlgorithm = EncryptionAlgorithm {
name: "chacha20-poly1305@openssh.com",
iv_size: 0,
key_size: 64, // 32 for header, 32 for main
decrypt_len: |state, bytes, packet_number| {
let alg = ChaCha20Poly1305OpenSsh::from_state(state);
alg.decrypt_len(bytes, packet_number)
},
decrypt_packet: |state, bytes, packet_number| {
let alg = ChaCha20Poly1305OpenSsh::from_state(state);
alg.decrypt_packet(bytes, packet_number)
},
encrypt_packet: |state, packet, packet_number| {
let alg = ChaCha20Poly1305OpenSsh::from_state(state);
alg.encrypt_packet(packet, packet_number)
},
};
pub const AES256_GCM: EncryptionAlgorithm = EncryptionAlgorithm {
name: "aes256-gcm@openssh.com",
iv_size: 12,
key_size: 32,
decrypt_len: |state, bytes, packet_number| {
let mut alg = Aes256GcmOpenSsh::from_state(state);
alg.decrypt_len(bytes, packet_number)
},
decrypt_packet: |state, bytes, packet_number| {
let mut alg = Aes256GcmOpenSsh::from_state(state);
alg.decrypt_packet(bytes, packet_number)
},
encrypt_packet: |state, packet, packet_number| {
let mut alg = Aes256GcmOpenSsh::from_state(state);
alg.encrypt_packet(packet, packet_number)
},
};
/// RFC 4344 AES128 in counter mode.
/// <https://datatracker.ietf.org/doc/html/rfc4344#section-4>
pub const ENC_AES128_CTR: EncryptionAlgorithm = EncryptionAlgorithm {
name: "aes128-ctr",
iv_size: 12,
key_size: 32,
decrypt_len: |state, bytes, packet_number| {
let mut alg = Aes128Ctr::from_state(state);
alg.decrypt_len(bytes, packet_number)
},
decrypt_packet: |state, bytes, packet_number| {
let mut state = Aes128Ctr::from_state(state);
state.decrypt_packet(bytes, packet_number)
},
encrypt_packet: |state, packet, packet_number| {
let mut state = Aes128Ctr::from_state(state);
state.encrypt_packet(packet, packet_number)
},
};
/// `chacha20-poly1305@openssh.com` uses a 64-bit nonce, not the 96-bit one in the IETF version.
type SshChaCha20 = chacha20::ChaCha20Legacy;
/// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
struct ChaCha20Poly1305OpenSsh {
header_key: chacha20::Key,
main_key: chacha20::Key,
}
impl ChaCha20Poly1305OpenSsh {
fn from_state(keys: &[u8]) -> Self {
assert_eq!(keys.len(), 64);
Self {
main_key: <[u8; 32]>::try_from(&keys[..32]).unwrap().into(),
header_key: <[u8; 32]>::try_from(&keys[32..]).unwrap().into(),
}
}
fn decrypt_len(&self, bytes: &mut [u8], packet_number: u64) {
// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
let mut cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
&self.header_key,
&packet_number.to_be_bytes().into(),
);
cipher.apply_keystream(bytes);
}
fn decrypt_packet(&self, mut bytes: RawPacket, packet_number: u64) -> Result<Packet> {
// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
let mut cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
&self.main_key,
&packet_number.to_be_bytes().into(),
);
let tag_offset = bytes.full_packet().len() - 16;
let authenticated = &bytes.full_packet()[..tag_offset];
let mac = {
let mut poly1305_key = [0; poly1305::KEY_SIZE];
cipher.apply_keystream(&mut poly1305_key);
poly1305::Poly1305::new(&poly1305_key.into()).compute_unpadded(authenticated)
};
let read_tag = poly1305::Tag::from_slice(&bytes.full_packet()[tag_offset..]);
if !bool::from(mac.ct_eq(read_tag)) {
return Err(crate::peer_error!(
"failed to decrypt: invalid poly1305 MAC"
));
}
// Advance ChaCha's block counter to 1
cipher
.seek(<chacha20::ChaCha20LegacyCore as chacha20::cipher::BlockSizeUser>::block_size());
let encrypted_packet_content = bytes.content_mut();
cipher.apply_keystream(encrypted_packet_content);
Packet::from_full(encrypted_packet_content)
}
fn encrypt_packet(&self, packet: Packet, packet_number: u64) -> EncryptedPacket {
let mut bytes = packet.to_bytes(false, Packet::DEFAULT_BLOCK_SIZE);
// Prepare the main cipher.
let mut main_cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
&self.main_key,
&packet_number.to_be_bytes().into(),
);
// Get the poly1305 key first, but don't use it yet!
// We encrypt-then-mac.
let mut poly1305_key = [0; poly1305::KEY_SIZE];
main_cipher.apply_keystream(&mut poly1305_key);
// As the first act of encryption, encrypt the length.
let mut len_cipher = <SshChaCha20 as chacha20::cipher::KeyIvInit>::new(
&self.header_key,
&packet_number.to_be_bytes().into(),
);
len_cipher.apply_keystream(&mut bytes[..4]);
// Advance ChaCha's block counter to 1
main_cipher
.seek(<chacha20::ChaCha20LegacyCore as chacha20::cipher::BlockSizeUser>::block_size());
// Encrypt the content of the packet, excluding the length and the MAC, which is not pushed yet.
main_cipher.apply_keystream(&mut bytes[4..]);
// Now, MAC the length || content, and push that to the end.
let mac = poly1305::Poly1305::new(&poly1305_key.into()).compute_unpadded(&bytes);
bytes.extend_from_slice(mac.as_slice());
EncryptedPacket::from_encrypted_full_bytes(bytes)
}
}
/// <https://datatracker.ietf.org/doc/html/rfc5647>
/// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL#L188C49-L188C64>
struct Aes256GcmOpenSsh<'a> {
key: aes_gcm::Key<aes_gcm::Aes256Gcm>,
nonce: &'a mut [u8; 12],
}
impl<'a> Aes256GcmOpenSsh<'a> {
fn from_state(keys: &'a mut [u8]) -> Self {
assert_eq!(keys.len(), 44);
Self {
key: <[u8; 32]>::try_from(&keys[..32]).unwrap().into(),
nonce: <&mut [u8; 12]>::try_from(&mut keys[32..]).unwrap(),
}
}
fn decrypt_len(&mut self, _: &mut [u8], _: u64) {
// AES-GCM does not encrypt the length.
// <https://datatracker.ietf.org/doc/html/rfc5647#section-7.3>
}
fn decrypt_packet(&mut self, mut bytes: RawPacket, _packet_number: u64) -> Result<Packet> {
let mut cipher = aes_gcm::Aes256Gcm::new(&self.key);
let mut len = [0; 4];
len.copy_from_slice(&bytes.full_packet()[..4]);
let tag_offset = bytes.full_packet().len() - 16;
let mut tag = [0; 16];
tag.copy_from_slice(&bytes.full_packet()[tag_offset..]);
let encrypted_packet_content = bytes.content_mut();
cipher
.decrypt_in_place_detached(
(&*self.nonce).into(),
&len,
encrypted_packet_content,
(&tag).into(),
)
.map_err(|_| crate::peer_error!("failed to decrypt: invalid GCM MAC"))?;
self.inc_nonce();
Packet::from_full(encrypted_packet_content)
}
fn encrypt_packet(&mut self, packet: Packet, _packet_number: u64) -> EncryptedPacket {
let mut bytes = packet.to_bytes(
false,
<aes_gcm::aes::Aes256 as aes_gcm::aes::cipher::BlockSizeUser>::block_size() as u8,
);
let mut cipher = aes_gcm::Aes256Gcm::new(&self.key);
let (aad, plaintext) = bytes.split_at_mut(4);
let tag = cipher
.encrypt_in_place_detached((&*self.nonce).into(), aad, plaintext)
.unwrap();
bytes.extend_from_slice(&tag);
self.inc_nonce();
EncryptedPacket::from_encrypted_full_bytes(bytes)
}
fn inc_nonce(&mut self) {
let mut carry = 1;
for i in (0..self.nonce.len()).rev() {
let n = self.nonce[i] as u16 + carry;
self.nonce[i] = n as u8;
carry = n >> 8;
}
}
}
struct Aes128Ctr {
_key: ctr::Ctr128BE<aes::Aes128>,
}
impl Aes128Ctr {
fn from_state(_keys: &mut [u8]) -> Self {
todo!()
}
fn decrypt_len(&mut self, _: &mut [u8], _: u64) {}
fn decrypt_packet(&mut self, _bytes: RawPacket, _packet_number: u64) -> Result<Packet> {
todo!()
}
fn encrypt_packet(&mut self, _packet: Packet, _packet_number: u64) -> EncryptedPacket {
todo!()
}
}

View file

@ -0,0 +1,68 @@
//! Operations on SSH keys.
// <https://datatracker.ietf.org/doc/html/rfc4716> exists but is kinda weird
use std::fmt::Display;
use base64::Engine;
use crate::parse::{self, ParseError, Parser, Writer};
#[derive(Debug, Clone)]
pub enum PublicKey {
Ed25519 { public_key: [u8; 32] },
}
impl PublicKey {
/// Parses an SSH public key from its wire encoding as specified in
/// RFC4253, RFC5656, and RFC8709.
pub fn from_wire_encoding(bytes: &[u8]) -> parse::Result<Self> {
let mut p = Parser::new(bytes);
let alg = p.utf8_string()?;
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>()?;
Self::Ed25519 { public_key }
}
_ => return Err(ParseError(format!("unsupported key type: {alg}"))),
};
Ok(k)
}
pub fn to_wire_encoding(&self) -> Vec<u8> {
let mut p = Writer::new();
match self {
Self::Ed25519 { public_key } => {
p.string(b"ssh-ed25519");
p.string(public_key);
}
}
p.finish()
}
pub fn algorithm_name(&self) -> &'static str {
match self {
Self::Ed25519 { .. } => "ssh-ed25519",
}
}
}
impl Display for PublicKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ed25519 { .. } => {
let encoded_pubkey = b64encode(&self.to_wire_encoding());
write!(f, "ssh-ed25519 {encoded_pubkey}")
}
}
}
}
fn b64encode(bytes: &[u8]) -> String {
base64::prelude::BASE64_STANDARD_NO_PAD.encode(bytes)
}

View file

@ -0,0 +1,53 @@
pub mod client;
mod crypto;
pub mod key;
pub mod numbers;
pub mod packet;
pub mod parse;
pub mod server;
pub use packet::Msg;
#[derive(Debug)]
pub enum SshStatus {
/// The client has sent a disconnect request, close the connection.
/// This is not an error.
Disconnect,
/// The peer did something wrong.
/// The connection should be closed and a notice may be logged,
/// but this does not require operator intervention.
PeerError(String),
}
pub type Result<T, E = SshStatus> = std::result::Result<T, E>;
pub trait SshRng {
fn fill_bytes(&mut self, dest: &mut [u8]);
}
struct SshRngRandAdapter<'a>(&'a mut dyn SshRng);
impl rand_core::CryptoRng for SshRngRandAdapter<'_> {}
impl rand_core::RngCore for SshRngRandAdapter<'_> {
fn next_u32(&mut self) -> u32 {
self.next_u64() as u32
}
fn next_u64(&mut self) -> u64 {
rand_core::impls::next_u64_via_fill(self)
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest);
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> std::result::Result<(), rand_core::Error> {
self.fill_bytes(dest);
Ok(())
}
}
#[macro_export]
macro_rules! peer_error {
($($tt:tt)*) => {
$crate::SshStatus::PeerError(::std::format!($($tt)*))
};
}

View file

@ -0,0 +1,143 @@
//! Constants for SSH.
//! <https://datatracker.ietf.org/doc/html/rfc4250>
// -----
// Transport layer protocol:
// 1 to 19 Transport layer generic (e.g., disconnect, ignore, debug, etc.)
pub const SSH_MSG_DISCONNECT: u8 = 1;
pub const SSH_MSG_IGNORE: u8 = 2;
pub const SSH_MSG_UNIMPLEMENTED: u8 = 3;
pub const SSH_MSG_DEBUG: u8 = 4;
pub const SSH_MSG_SERVICE_REQUEST: u8 = 5;
pub const SSH_MSG_SERVICE_ACCEPT: u8 = 6;
// 20 to 29 Algorithm negotiation
pub const SSH_MSG_KEXINIT: u8 = 20;
pub const SSH_MSG_NEWKEYS: u8 = 21;
// 30 to 49 Key exchange method specific (numbers can be reused for different authentication methods)
pub const SSH_MSG_KEXDH_INIT: u8 = 30;
pub const SSH_MSG_KEX_ECDH_INIT: u8 = 30; // Same number
pub const SSH_MSG_KEXDH_REPLY: u8 = 31;
pub const SSH_MSG_KEX_ECDH_REPLY: u8 = 31;
// -----
// User authentication protocol:
// 50 to 59 User authentication generic
pub const SSH_MSG_USERAUTH_REQUEST: u8 = 50;
pub const SSH_MSG_USERAUTH_FAILURE: u8 = 51;
pub const SSH_MSG_USERAUTH_SUCCESS: u8 = 52;
pub const SSH_MSG_USERAUTH_BANNER: u8 = 53;
// 60 to 79 User authentication method specific (numbers can be reused for different authentication methods)
// -----
// Connection protocol:
// 80 to 89 Connection protocol generic
pub const SSH_MSG_GLOBAL_REQUEST: u8 = 80;
pub const SSH_MSG_REQUEST_SUCCESS: u8 = 81;
pub const SSH_MSG_REQUEST_FAILURE: u8 = 82;
// 90 to 127 Channel related messages
pub const SSH_MSG_CHANNEL_OPEN: u8 = 90;
pub const SSH_MSG_CHANNEL_OPEN_CONFIRMATION: u8 = 91;
pub const SSH_MSG_CHANNEL_OPEN_FAILURE: u8 = 92;
pub const SSH_MSG_CHANNEL_WINDOW_ADJUST: u8 = 93;
pub const SSH_MSG_CHANNEL_DATA: u8 = 94;
pub const SSH_MSG_CHANNEL_EXTENDED_DATA: u8 = 95;
pub const SSH_MSG_CHANNEL_EOF: u8 = 96;
pub const SSH_MSG_CHANNEL_CLOSE: u8 = 97;
pub const SSH_MSG_CHANNEL_REQUEST: u8 = 98;
pub const SSH_MSG_CHANNEL_SUCCESS: u8 = 99;
pub const SSH_MSG_CHANNEL_FAILURE: u8 = 100;
pub fn packet_type_to_string(packet_type: u8) -> &'static str {
match packet_type {
1 => "SSH_MSG_DISCONNECT",
2 => "SSH_MSG_IGNORE",
3 => "SSH_MSG_UNIMPLEMENTED",
4 => "SSH_MSG_DEBUG",
5 => "SSH_MSG_SERVICE_REQUEST",
6 => "SSH_MSG_SERVICE_ACCEPT",
20 => "SSH_MSG_KEXINIT",
21 => "SSH_MSG_NEWKEYS",
30 => "SSH_MSG_KEX_ECDH_INIT",
31 => "SSH_MSG_KEX_ECDH_REPLY",
50 => "SSH_MSG_USERAUTH_REQUEST",
51 => "SSH_MSG_USERAUTH_FAILURE",
52 => "SSH_MSG_USERAUTH_SUCCESS",
53 => "SSH_MSG_USERAUTH_BANNER",
80 => "SSH_MSG_GLOBAL_REQUEST",
81 => "SSH_MSG_REQUEST_SUCCESS",
82 => "SSH_MSG_REQUEST_FAILURE",
90 => "SSH_MSG_CHANNEL_OPEN",
91 => "SSH_MSG_CHANNEL_OPEN_CONFIRMATION",
92 => "SSH_MSG_CHANNEL_OPEN_FAILURE",
93 => "SSH_MSG_CHANNEL_WINDOW_ADJUST",
94 => "SSH_MSG_CHANNEL_DATA",
95 => "SSH_MSG_CHANNEL_EXTENDED_DATA",
96 => "SSH_MSG_CHANNEL_EOF",
97 => "SSH_MSG_CHANNEL_CLOSE",
98 => "SSH_MSG_CHANNEL_REQUEST",
99 => "SSH_MSG_CHANNEL_SUCCESS",
100 => "SSH_MSG_CHANNEL_FAILURE",
_ => "<unknown>",
}
}
pub const SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT: u32 = 1;
pub const SSH_DISCONNECT_PROTOCOL_ERROR: u32 = 2;
pub const SSH_DISCONNECT_KEY_EXCHANGE_FAILED: u32 = 3;
pub const SSH_DISCONNECT_RESERVED: u32 = 4;
pub const SSH_DISCONNECT_MAC_ERROR: u32 = 5;
pub const SSH_DISCONNECT_COMPRESSION_ERROR: u32 = 6;
pub const SSH_DISCONNECT_SERVICE_NOT_AVAILABLE: u32 = 7;
pub const SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED: u32 = 8;
pub const SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE: u32 = 9;
pub const SSH_DISCONNECT_CONNECTION_LOST: u32 = 10;
pub const SSH_DISCONNECT_BY_APPLICATION: u32 = 11;
pub const SSH_DISCONNECT_TOO_MANY_CONNECTIONS: u32 = 12;
pub const SSH_DISCONNECT_AUTH_CANCELLED_BY_USER: u32 = 13;
pub const SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE: u32 = 14;
pub const SSH_DISCONNECT_ILLEGAL_USER_NAME: u32 = 15;
pub fn disconnect_reason_to_string(reason: u32) -> Option<&'static str> {
Some(match reason {
1 => "SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT",
2 => "SSH_DISCONNECT_PROTOCOL_ERROR",
3 => "SSH_DISCONNECT_KEY_EXCHANGE_FAILED",
4 => "SSH_DISCONNECT_RESERVED",
5 => "SSH_DISCONNECT_MAC_ERROR",
6 => "SSH_DISCONNECT_COMPRESSION_ERROR",
7 => "SSH_DISCONNECT_SERVICE_NOT_AVAILABLE",
8 => "SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED",
9 => "SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE",
10 => "SSH_DISCONNECT_CONNECTION_LOST",
11 => "SSH_DISCONNECT_BY_APPLICATION",
12 => "SSH_DISCONNECT_TOO_MANY_CONNECTIONS",
13 => "SSH_DISCONNECT_AUTH_CANCELLED_BY_USER",
14 => "SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE",
15 => "SSH_DISCONNECT_ILLEGAL_USER_NAME",
_ => return None,
})
}
pub const SSH_OPEN_ADMINISTRATIVELY_PROHIBITED: u32 = 1;
pub const SSH_OPEN_CONNECT_FAILED: u32 = 2;
pub const SSH_OPEN_UNKNOWN_CHANNEL_TYPE: u32 = 3;
pub const SSH_OPEN_RESOURCE_SHORTAGE: u32 = 4;
pub fn channel_connection_failure_to_string(reason: u32) -> Option<&'static str> {
Some(match reason {
1 => "SSH_OPEN_ADMINISTRATIVELY_PROHIBITED",
2 => "SSH_OPEN_CONNECT_FAILED",
3 => "SSH_OPEN_UNKNOWN_CHANNEL_TYPE",
4 => "SSH_OPEN_RESOURCE_SHORTAGE",
_ => return None,
})
}
pub const SSH_EXTENDED_DATA_STDERR: u32 = 1;

View file

@ -0,0 +1,540 @@
mod ctors;
use std::collections::VecDeque;
use std::mem;
use tracing::{debug, trace};
use crate::crypto::{self, EncryptionAlgorithm, Keys, Plaintext, Session};
use crate::parse::{NameList, Parser, Writer};
use crate::Result;
use crate::{numbers, peer_error};
/// Frames the byte stream into packets.
pub(crate) struct PacketTransport {
keys: Box<dyn Keys>,
recv_next_packet: PacketParser,
recv_packets: VecDeque<Packet>,
recv_next_seq_nr: u64,
msgs_to_send: VecDeque<Msg>,
send_next_seq_nr: u64,
}
#[derive(Debug)]
pub struct Msg(pub(crate) MsgKind);
#[derive(Debug, PartialEq)]
pub(crate) enum MsgKind {
ServerProtocolInfo(Vec<u8>),
PlaintextPacket(Packet),
EncryptedPacket(EncryptedPacket),
}
impl Msg {
pub fn to_bytes(self) -> Vec<u8> {
match self.0 {
MsgKind::ServerProtocolInfo(v) => v,
MsgKind::PlaintextPacket(v) => v.to_bytes(true, Packet::DEFAULT_BLOCK_SIZE),
MsgKind::EncryptedPacket(v) => v.into_bytes(),
}
}
}
impl PacketTransport {
pub(crate) fn new() -> Self {
PacketTransport {
keys: Box::new(Plaintext),
recv_next_packet: PacketParser::new(),
recv_packets: VecDeque::new(),
recv_next_seq_nr: 0,
msgs_to_send: VecDeque::new(),
send_next_seq_nr: 0,
}
}
pub(crate) fn recv_bytes(&mut self, mut bytes: &[u8]) -> Result<()> {
while let Some(consumed) = self.recv_bytes_step(bytes)? {
bytes = &bytes[consumed..];
if bytes.is_empty() {
break;
}
}
Ok(())
}
fn recv_bytes_step(&mut self, bytes: &[u8]) -> Result<Option<usize>> {
// TODO: This might not work if we buffer two packets where one changes keys in between?
let result =
self.recv_next_packet
.recv_bytes(bytes, &mut *self.keys, self.recv_next_seq_nr)?;
if let Some((consumed, result)) = result {
self.recv_packets.push_back(result);
self.recv_next_seq_nr = self.recv_next_seq_nr.wrapping_add(1);
self.recv_next_packet = PacketParser::new();
return Ok(Some(consumed));
}
Ok(None)
}
pub(crate) fn queue_packet(&mut self, packet: Packet) {
let packet_type = packet.packet_type();
let packet_type_string = numbers::packet_type_to_string(packet_type);
trace!(%packet_type, %packet_type_string, packet_len = %packet.payload.len(), "Sending packet");
let seq_nr = self.send_next_seq_nr;
self.send_next_seq_nr = self.send_next_seq_nr.wrapping_add(1);
let msg = self.keys.encrypt_packet_to_msg(packet, seq_nr);
self.queue_send_msg(msg);
}
pub(crate) fn queue_send_protocol_info(&mut self, identification: Vec<u8>) {
self.queue_send_msg(Msg(MsgKind::ServerProtocolInfo(identification)));
}
pub(crate) fn recv_next_packet(&mut self) -> Option<Packet> {
self.recv_packets.pop_front()
}
// Private: Make sure all sending goes through variant-specific functions here.
fn queue_send_msg(&mut self, msg: Msg) {
self.msgs_to_send.push_back(msg);
}
pub(crate) fn next_msg_to_send(&mut self) -> Option<Msg> {
self.msgs_to_send.pop_front()
}
pub(crate) fn set_key(
&mut self,
h: [u8; 32],
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,
));
}
}
}
/*
packet teminology used throughout this crate:
length | padding_length | payload | random padding | MAC
-------------------------------------------------------- "full"
----------------------------------------------- "rest"
------- "payload"
----------------------------------------- "content"
-------------------------------------------------- "authenticated"
^^^^^^ encrypted using K1
^^^^ plaintext
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ encrypted using K2
*/
/// A plaintext SSH packet payload.
#[derive(Debug, PartialEq)]
pub struct Packet {
pub payload: Vec<u8>,
}
impl Packet {
pub const DEFAULT_BLOCK_SIZE: u8 = 8;
pub fn packet_type(&self) -> u8 {
self.payload[0]
}
pub(crate) fn from_full(bytes: &[u8]) -> Result<Self> {
let Some(padding_length) = bytes.first() else {
return Err(peer_error!("empty packet"));
};
let Some(payload_len) = (bytes.len() - 1).checked_sub(*padding_length as usize) else {
return Err(peer_error!("packet padding longer than packet"));
};
let payload = &bytes[1..][..payload_len];
// TODO: handle the annoying decryption special case differnt where its +0 instead of +4
// also TODO: this depends on the cipher!
//if (bytes.len() + 4) % 8 != 0 {
// return Err(peer_error!("full packet length must be multiple of 8: {}", bytes.len()));
//}
Ok(Self {
payload: payload.to_vec(),
})
}
pub(crate) fn to_bytes(&self, respect_len_for_padding: bool, block_size: u8) -> Vec<u8> {
assert!(block_size.is_power_of_two());
let let_bytes = if respect_len_for_padding { 4 } else { 0 };
// <https://datatracker.ietf.org/doc/html/rfc4253#section-6>
let min_full_length = self.payload.len() + let_bytes + 1;
// The padding must give a factor of block_size.
let min_padding_len =
(min_full_length.next_multiple_of(block_size as usize) - min_full_length) as u8;
// > There MUST be at least four bytes of padding.
let padding_len = if min_padding_len < 4 {
min_padding_len + block_size
} else {
min_padding_len
};
let packet_len = self.payload.len() + (padding_len as usize) + 1;
let mut new = Vec::new();
new.extend_from_slice(&u32::to_be_bytes(packet_len as u32));
new.extend_from_slice(&[padding_len]);
new.extend_from_slice(&self.payload);
new.extend(std::iter::repeat(0).take(padding_len as usize));
assert!((let_bytes + 1 + self.payload.len() + (padding_len as usize)) % 8 == 0);
new
}
pub fn payload_parser(&self) -> Parser<'_> {
Parser::new(&self.payload)
}
}
#[derive(Debug, PartialEq)]
pub(crate) struct EncryptedPacket {
data: Vec<u8>,
}
impl EncryptedPacket {
pub(crate) fn into_bytes(self) -> Vec<u8> {
self.data
}
pub(crate) fn from_encrypted_full_bytes(data: Vec<u8>) -> Self {
Self { data }
}
}
#[derive(Debug)]
pub(crate) struct KeyExchangeInitPacket<'a> {
pub(crate) cookie: [u8; 16],
pub(crate) kex_algorithms: NameList<'a>,
pub(crate) server_host_key_algorithms: NameList<'a>,
pub(crate) encryption_algorithms_client_to_server: NameList<'a>,
pub(crate) encryption_algorithms_server_to_client: NameList<'a>,
pub(crate) mac_algorithms_client_to_server: NameList<'a>,
pub(crate) mac_algorithms_server_to_client: NameList<'a>,
pub(crate) compression_algorithms_client_to_server: NameList<'a>,
pub(crate) compression_algorithms_server_to_client: NameList<'a>,
pub(crate) languages_client_to_server: NameList<'a>,
pub(crate) languages_server_to_client: NameList<'a>,
pub(crate) first_kex_packet_follows: bool,
}
impl<'a> KeyExchangeInitPacket<'a> {
pub(crate) fn parse(payload: &'a [u8]) -> Result<KeyExchangeInitPacket<'_>> {
let mut c = Parser::new(payload);
let kind = c.u8()?;
if kind != numbers::SSH_MSG_KEXINIT {
return Err(peer_error!("expected SSH_MSG_KEXINIT packet, found {kind}"));
}
let cookie = c.array::<16>()?;
let kex_algorithms = c.name_list()?;
let server_host_key_algorithms = c.name_list()?;
let encryption_algorithms_client_to_server = c.name_list()?;
let encryption_algorithms_server_to_client = c.name_list()?;
let mac_algorithms_client_to_server = c.name_list()?;
let mac_algorithms_server_to_client = c.name_list()?;
let compression_algorithms_client_to_server = c.name_list()?;
let compression_algorithms_server_to_client = c.name_list()?;
let languages_client_to_server = c.name_list()?;
let languages_server_to_client = c.name_list()?;
let first_kex_packet_follows = c.bool()?;
let _ = c.u32()?; // Reserved.
Ok(Self {
cookie,
kex_algorithms,
server_host_key_algorithms,
encryption_algorithms_client_to_server,
encryption_algorithms_server_to_client,
mac_algorithms_client_to_server,
mac_algorithms_server_to_client,
compression_algorithms_client_to_server,
compression_algorithms_server_to_client,
languages_client_to_server,
languages_server_to_client,
first_kex_packet_follows,
})
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let mut data = Writer::new();
data.u8(numbers::SSH_MSG_KEXINIT);
data.array(self.cookie);
data.name_list(self.kex_algorithms);
data.name_list(self.server_host_key_algorithms);
data.name_list(self.encryption_algorithms_client_to_server);
data.name_list(self.encryption_algorithms_server_to_client);
data.name_list(self.mac_algorithms_client_to_server);
data.name_list(self.mac_algorithms_server_to_client);
data.name_list(self.compression_algorithms_client_to_server);
data.name_list(self.compression_algorithms_server_to_client);
data.name_list(self.languages_client_to_server);
data.name_list(self.languages_server_to_client);
data.u8(self.first_kex_packet_follows as u8);
data.u32(0); // Reserved.
data.finish()
}
}
#[derive(Debug)]
pub(crate) struct KeyExchangeEcDhInitPacket<'a> {
pub(crate) qc: &'a [u8],
}
impl<'a> KeyExchangeEcDhInitPacket<'a> {
pub(crate) fn parse(payload: &'a [u8]) -> Result<KeyExchangeEcDhInitPacket<'_>> {
let mut c = Parser::new(payload);
let kind = c.u8()?;
if kind != numbers::SSH_MSG_KEX_ECDH_INIT {
return Err(peer_error!(
"expected SSH_MSG_KEXDH_INIT packet, found {kind}"
));
}
let qc = c.string()?;
Ok(Self { qc })
}
}
pub(crate) struct RawPacket {
pub mac_len: usize,
pub raw: Vec<u8>,
}
impl RawPacket {
pub(crate) fn rest(&self) -> &[u8] {
&self.raw[4..]
}
pub(crate) fn full_packet(&self) -> &[u8] {
&self.raw
}
pub(crate) fn content_mut(&mut self) -> &mut [u8] {
let mac_start = self.raw.len() - self.mac_len;
&mut self.raw[4..mac_start]
}
}
pub struct PacketParser {
// The length of the packet.
packet_length: Option<usize>,
// The raw data *encrypted*, including the length.
raw_data: Vec<u8>,
done: bool,
}
impl PacketParser {
pub fn new() -> Self {
Self {
packet_length: None,
raw_data: Vec::new(),
done: false,
}
}
/// Parse a raw packet body out of a plaintext stream of bytes.
/// # Returns
/// - `Err()` - if the packet was invalid
/// - `Ok(None)` - if the packet is incomplete and needs more data
/// - `Ok(Some(consumed, all_data))` if a packet has been parsed.
/// `consumed` is the amount of bytes from `bytes` that were actually consumed,
/// `all_data` is the entire packet including the length.
pub fn recv_plaintext_bytes(&mut self, bytes: &[u8]) -> Result<Option<(usize, Vec<u8>)>> {
let Some((consumed, data)) = self.recv_bytes_inner(bytes, &mut crypto::Plaintext, 0)?
else {
return Ok(None);
};
self.done = true;
Ok(Some((consumed, data.raw)))
}
fn recv_bytes(
&mut self,
bytes: &[u8],
decrytor: &mut dyn Keys,
next_seq_nr: u64,
) -> Result<Option<(usize, Packet)>> {
let Some((consumed, data)) = self.recv_bytes_inner(bytes, decrytor, next_seq_nr)? else {
return Ok(None);
};
let packet = decrytor.decrypt_packet(data, next_seq_nr)?;
Ok(Some((consumed, packet)))
}
fn recv_bytes_inner(
&mut self,
mut bytes: &[u8],
keys: &mut dyn Keys,
next_seq_nr: u64,
) -> Result<Option<(usize, RawPacket)>> {
assert!(
!self.done,
"Passed bytes to packet parser even after it was completed"
);
let mut consumed = 0;
let packet_length = match self.packet_length {
Some(packet_length) => {
assert!(self.raw_data.len() >= 4);
packet_length
}
None => {
let remaining_len = std::cmp::min(bytes.len(), 4 - self.raw_data.len());
// Try to read the bytes of the length.
self.raw_data.extend_from_slice(&bytes[..remaining_len]);
if self.raw_data.len() < 4 {
// Not enough data yet :(.
return Ok(None);
}
let mut len_to_decrypt = [0_u8; 4];
len_to_decrypt.copy_from_slice(self.raw_data.as_slice());
keys.decrypt_len(&mut len_to_decrypt, next_seq_nr);
let packet_length = u32::from_be_bytes(len_to_decrypt);
let packet_length: usize = packet_length.try_into().unwrap();
let packet_length = packet_length + keys.additional_mac_len();
self.packet_length = Some(packet_length);
// We have the data.
bytes = &bytes[remaining_len..];
consumed += remaining_len;
packet_length
}
};
// <https://datatracker.ietf.org/doc/html/rfc4253#section-6.1>
// All implementations MUST be able to process packets with an
// uncompressed payload length of 32768 bytes or less and a total packet
// size of 35000 bytes or less (including 'packet_length',
// '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}"));
}
let remaining_len = std::cmp::min(bytes.len(), packet_length - (self.raw_data.len() - 4));
self.raw_data.extend_from_slice(&bytes[..remaining_len]);
consumed += remaining_len;
if (self.raw_data.len() - 4) == packet_length {
// We have the full data.
Ok(Some((
consumed,
RawPacket {
raw: std::mem::take(&mut self.raw_data),
mac_len: keys.additional_mac_len(),
},
)))
} else {
Ok(None)
}
}
#[cfg(test)]
fn test_recv_bytes(&mut self, bytes: &[u8]) -> Option<(usize, RawPacket)> {
self.recv_bytes_inner(bytes, &mut Plaintext, 0).unwrap()
}
}
pub(crate) struct ProtocolIdentParser(Vec<u8>);
impl ProtocolIdentParser {
pub(crate) fn new() -> Self {
Self(Vec::new())
}
pub(crate) fn recv_bytes(&mut self, bytes: &[u8]) {
self.0.extend_from_slice(bytes);
}
pub(crate) fn get_peer_ident(&mut self) -> Option<Vec<u8>> {
if self.0.windows(2).any(|win| win == b"\r\n") {
// TODO: care that its SSH 2.0 instead of anythin anything else
// The peer will not send any more information than this until we respond, so discord the rest of the bytes.
let peer_ident = mem::take(&mut self.0);
let peer_ident_string = String::from_utf8_lossy(&peer_ident);
debug!(identification = %peer_ident_string.trim(), "Peer identifier");
Some(peer_ident)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use crate::packet::PacketParser;
trait OptionExt {
fn unwrap_none(self);
}
impl<T> OptionExt for Option<T> {
#[track_caller]
fn unwrap_none(self) {
assert!(self.is_none());
}
}
#[test]
fn packet_parser() {
let mut p = PacketParser::new();
p.test_recv_bytes(&2_u32.to_be_bytes()).unwrap_none();
p.test_recv_bytes(&[1]).unwrap_none();
let (consumed, data) = p.test_recv_bytes(&[2]).unwrap();
assert_eq!(consumed, 1);
assert_eq!(data.rest(), &[1, 2]);
}
#[test]
fn packet_parser_split_len() {
let mut p = PacketParser::new();
let len = &2_u32.to_be_bytes();
p.test_recv_bytes(&len[0..2]).unwrap_none();
p.test_recv_bytes(&len[2..4]).unwrap_none();
p.test_recv_bytes(&[1]).unwrap_none();
let (consumed, data) = p.test_recv_bytes(&[2]).unwrap();
assert_eq!(consumed, 1);
assert_eq!(data.rest(), &[1, 2]);
}
#[test]
fn packet_parser_all() {
let mut p = PacketParser::new();
let (consumed, data) = p.test_recv_bytes(&[0, 0, 0, 2, 1, 2]).unwrap();
assert_eq!(consumed, 6);
assert_eq!(data.rest(), &[1, 2]);
}
}

View file

@ -0,0 +1,147 @@
use crate::packet::Packet;
use crate::parse::Writer;
#[allow(non_camel_case_types)]
mod ssh_type_to_rust {
pub(super) use {bool, u32, u8};
pub(super) type string<'a> = &'a [u8];
pub(super) type name_list<'a> = crate::parse::NameList<'a>;
}
macro_rules! ctors {
(
$(
fn $fn_name:ident(
$msg_type:ident;
$(
$name:ident: $ssh_type:ident
),*
$(,)?
);
)*
) => {
impl Packet {
$(
pub fn $fn_name(
$(
$name: ssh_type_to_rust::$ssh_type
),*
) -> Packet {
let mut w = Writer::new();
w.u8($crate::numbers::$msg_type);
$(
w.$ssh_type($name);
)*
Packet {
payload: w.finish(),
}
}
)*
}
};
}
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);
fn new_msg_kex_ecdh_reply(SSH_MSG_KEX_ECDH_REPLY;
server_public_host_key_ks: string,
server_ephemeral_public_key_qs: string,
signature: string,
);
// -----
// User authentication protocol:
// 50 to 59 User authentication generic
fn new_msg_userauth_request_none(SSH_MSG_USERAUTH_REQUEST;
username: string,
service_name: string,
method_name_none: string,
);
fn new_msg_userauth_request_password(SSH_MSG_USERAUTH_REQUEST;
username: string,
service_name: string,
method_name_password: string,
false_: bool,
password: string,
);
fn new_msg_userauth_request_publickey(SSH_MSG_USERAUTH_REQUEST;
username: string,
service_name: string,
method_name_pubkey: string,
true_: bool,
pubkey_alg_name: string,
pubkey: string,
signature: string,
);
fn new_msg_userauth_failure(SSH_MSG_USERAUTH_FAILURE;
auth_options: name_list,
partial_success: bool,
);
fn new_msg_userauth_success(SSH_MSG_USERAUTH_SUCCESS;);
fn new_msg_userauth_banner(SSH_MSG_USERAUTH_BANNER; msg: string, language_tag: string);
// 60 to 79 User authentication method specific (numbers can be reused for different authentication methods)
// -----
// Connection protocol:
// 80 to 89 Connection protocol generic
fn new_msg_request_failure(SSH_MSG_REQUEST_FAILURE;);
// 90 to 127 Channel related messages
fn new_msg_channel_open_session(SSH_MSG_CHANNEL_OPEN;
session: string,
sender_channel: u32,
initial_window_size: u32,
maximum_packet_size: u32,
);
fn new_msg_channel_open_confirmation(SSH_MSG_CHANNEL_OPEN_CONFIRMATION;
peer_channel: u32,
sender_channel: u32,
initial_window_size: u32,
max_packet_size: u32,
);
fn new_msg_channel_open_failure(SSH_MSG_CHANNEL_OPEN_FAILURE;
sender_channe: u32,
reason_code: u32,
description: string,
language_tag: string,
);
fn new_msg_channel_window_adjust(SSH_MSG_CHANNEL_WINDOW_ADJUST; recipient_channel: u32, bytes_to_add: u32);
fn new_msg_channel_data(SSH_MSG_CHANNEL_DATA; recipient_channel: u32, data: string);
fn new_msg_channel_eof(SSH_MSG_CHANNEL_EOF; recipient_channel: u32);
fn new_msg_channel_close(SSH_MSG_CHANNEL_CLOSE; recipient_channel: u32);
fn new_msg_channel_request_pty_req(SSH_MSG_CHANNEL_REQUEST;
recipient_channel: u32,
kind_pty_req: string,
want_reply: bool,
term: string,
term_width_char: u32,
term_height_rows: u32,
term_width_px: u32,
term_height_px: u32,
term_modes: string,
);
fn new_msg_channel_request_shell(SSH_MSG_CHANNEL_REQUEST;
recipient_channel: u32,
kind_shell: string,
want_reply: bool,
);
fn new_msg_channel_request_exit_status(SSH_MSG_CHANNEL_REQUEST; recipient_channel: u32, kind_exit_status: string, false_: bool, exit_status: u32);
fn new_msg_channel_success(SSH_MSG_CHANNEL_SUCCESS; recipient_channel: u32);
fn new_msg_channel_failure(SSH_MSG_CHANNEL_FAILURE; recipient_channel: u32);
}

View file

@ -0,0 +1,205 @@
use core::str;
use std::fmt::{Debug, Display};
use crate::SshStatus;
#[derive(Debug)]
pub struct ParseError(pub String);
impl Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for ParseError {}
impl From<ParseError> for SshStatus {
fn from(err: ParseError) -> Self {
Self::PeerError(err.0)
}
}
pub type Result<T, E = ParseError> = std::result::Result<T, E>;
/// A simplified `byteorder` clone that emits client errors when the data is too short.
pub struct Parser<'a>(&'a [u8]);
impl<'a> Parser<'a> {
pub fn new(data: &'a [u8]) -> Self {
Self(data)
}
pub fn remaining(&self) -> &[u8] {
&self.0
}
pub fn has_data(&self) -> bool {
!self.0.is_empty()
}
pub fn u8(&mut self) -> Result<u8> {
let arr = self.array::<1>()?;
Ok(arr[0])
}
pub fn u32(&mut self) -> Result<u32> {
let arr = self.array()?;
Ok(u32::from_be_bytes(arr))
}
pub fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
assert!(N < 100_000);
if self.0.len() < N {
return Err(ParseError(format!(
"packet too short, expected {N} but found {}",
self.0.len()
)));
}
let result = self.0[..N].try_into().unwrap();
self.0 = &self.0[N..];
Ok(result)
}
pub fn slice(&mut self, len: usize) -> Result<&'a [u8]> {
if self.0.len() < len {
return Err(ParseError(format!(
"packet too short, expected {len} but found {}",
self.0.len()
)));
}
if len > 100_000 {
return Err(ParseError(format!("bytes too long: {len}")));
}
let result = &self.0[..len];
self.0 = &self.0[len..];
Ok(result)
}
pub fn bool(&mut self) -> Result<bool> {
let b = self.u8()?;
match b {
0 => Ok(false),
1 => Ok(true),
_ => Err(ParseError(format!("invalid bool: {b}"))),
}
}
pub fn name_list(&mut self) -> Result<NameList<'a>> {
let list = self.utf8_string()?;
Ok(NameList(list))
}
pub fn mpint(&mut self) -> Result<MpInt<'a>> {
todo!("do correctly")
}
pub fn string(&mut self) -> Result<&'a [u8]> {
let len = self.u32()?;
let data = self.slice(len.try_into().unwrap())?;
Ok(data)
}
pub fn utf8_string(&mut self) -> Result<&'a str> {
let s = self.string()?;
let Ok(s) = str::from_utf8(s) else {
return Err(ParseError(format!("name-list is invalid UTF-8")));
};
Ok(s)
}
}
/// A writer for the SSH wire format.
pub struct Writer(Vec<u8>);
impl Writer {
pub fn new() -> Self {
Self(Vec::new())
}
pub fn u8(&mut self, v: u8) {
self.raw(&[v]);
}
pub fn u32(&mut self, v: u32) {
self.raw(&u32::to_be_bytes(v));
}
pub fn raw(&mut self, v: &[u8]) {
self.0.extend_from_slice(v);
}
pub fn array<const N: usize>(&mut self, arr: [u8; N]) {
self.raw(&arr);
}
pub fn name_list(&mut self, list: NameList<'_>) {
self.string(list.0.as_bytes());
}
pub fn mpint<const LIMBS: usize>(&mut self, uint: crypto_bigint::Uint<LIMBS>)
where
crypto_bigint::Uint<LIMBS>: crypto_bigint::ArrayEncoding,
{
let bytes = crypto_bigint::ArrayEncoding::to_be_byte_array(&uint);
let (bytes, pad_zero) = fixup_mpint(&bytes);
let len = bytes.len() + (pad_zero as usize);
self.u32(len as u32);
if pad_zero {
self.u8(0);
}
self.raw(bytes);
}
pub fn string(&mut self, data: impl AsRef<[u8]>) {
let data = data.as_ref();
self.u32(data.len() as u32);
self.raw(data);
}
pub fn bool(&mut self, v: bool) {
self.u8(v as u8);
}
pub fn finish(self) -> Vec<u8> {
self.0
}
}
/// Returns an array of significant bits for the mpint,
/// and whether a leading 0 needs to be added for padding.
pub fn fixup_mpint(mut int_encoded: &[u8]) -> (&[u8], bool) {
while int_encoded[0] == 0 {
int_encoded = &int_encoded[1..];
}
// If the first high bit is set, pad it with a zero.
(int_encoded, (int_encoded[0] & 0b10000000) > 1)
}
#[derive(Clone, Copy)]
pub struct NameList<'a>(pub &'a str);
impl<'a> NameList<'a> {
pub fn one(item: &'a str) -> Self {
if item.contains(',') {
panic!("tried creating name list with comma in item: {item}");
}
Self(item)
}
pub fn multi(items: &'a str) -> Self {
Self(items)
}
pub fn none() -> NameList<'static> {
NameList("")
}
pub fn iter(&self) -> std::str::Split<'a, char> {
self.0.split(',')
}
}
impl Debug for NameList<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self.0, f)
}
}
#[derive(Debug, Clone, Copy)]
pub struct MpInt<'a>(pub &'a [u8]);

View file

@ -0,0 +1,446 @@
use std::{collections::VecDeque, mem::take};
use crate::crypto::{
self, AlgorithmName, EncryptionAlgorithm, HostKeySigningAlgorithm, SupportedAlgorithms,
};
use crate::packet::{
KeyExchangeEcDhInitPacket, KeyExchangeInitPacket, Packet, PacketTransport, ProtocolIdentParser,
};
use crate::parse::{NameList, Parser, Writer};
use crate::{numbers, Result};
use crate::{peer_error, Msg, SshRng, SshStatus};
use tracing::{debug, info, trace};
// This is definitely who we are.
pub const SERVER_IDENTIFICATION: &[u8] = b"SSH-2.0-OpenSSH_9.7\r\n";
pub struct ServerConnection {
state: ServerState,
packet_transport: PacketTransport,
rng: Box<dyn SshRng + Send + Sync>,
plaintext_packets: VecDeque<Packet>,
}
enum ServerState {
ProtoExchange {
ident_parser: ProtocolIdentParser,
},
KeyExchangeInit {
client_identification: Vec<u8>,
},
DhKeyInit {
client_identification: Vec<u8>,
client_kexinit: Vec<u8>,
server_kexinit: Vec<u8>,
kex_algorithm: crypto::KexAlgorithm,
server_host_key_algorithm: HostKeySigningAlgorithm,
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
},
NewKeys {
h: [u8; 32],
k: Vec<u8>,
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
},
ServiceRequest,
Open,
}
impl ServerConnection {
pub fn new(rng: impl SshRng + Send + Sync + 'static) -> Self {
Self {
state: ServerState::ProtoExchange {
ident_parser: ProtocolIdentParser::new(),
},
packet_transport: PacketTransport::new(),
rng: Box::new(rng),
plaintext_packets: VecDeque::new(),
}
}
pub fn recv_bytes(&mut self, bytes: &[u8]) -> Result<()> {
if let ServerState::ProtoExchange { ident_parser } = &mut self.state {
ident_parser.recv_bytes(bytes);
if let Some(client_identification) = ident_parser.get_peer_ident() {
self.packet_transport
.queue_send_protocol_info(SERVER_IDENTIFICATION.to_vec());
self.state = ServerState::KeyExchangeInit {
client_identification,
};
}
// This means that we must be called at least twice, which is fine I think.
return Ok(());
}
self.packet_transport.recv_bytes(bytes)?;
while let Some(packet) = self.packet_transport.recv_next_packet() {
let packet_type = packet.payload.first().unwrap_or(&0xFF);
let packet_type_string = numbers::packet_type_to_string(*packet_type);
trace!(%packet_type, %packet_type_string, packet_len = %packet.payload.len(), "Received packet");
// Handle some packets ignoring the state.
match packet.payload.first().copied() {
Some(numbers::SSH_MSG_DISCONNECT) => {
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.1>
let mut disconnect = Parser::new(&packet.payload[1..]);
let reason = disconnect.u32()?;
let description = disconnect.utf8_string()?;
let _language_tag = disconnect.utf8_string()?;
let reason_string =
numbers::disconnect_reason_to_string(reason).unwrap_or("<unknown>");
info!(%reason, %reason_string, %description, "Client disconnecting");
return Err(SshStatus::Disconnect);
}
_ => {}
}
match &mut self.state {
ServerState::ProtoExchange { .. } => unreachable!("handled above"),
ServerState::KeyExchangeInit {
client_identification,
} => {
let kex = KeyExchangeInitPacket::parse(&packet.payload)?;
let sup_algs = SupportedAlgorithms::secure();
let kex_algorithm = sup_algs.key_exchange.find(kex.kex_algorithms.0)?;
debug!(name = %kex_algorithm.name(), "Using KEX algorithm");
let server_host_key_algorithm =
sup_algs.hostkey.find(kex.server_host_key_algorithms.0)?;
debug!(name = %server_host_key_algorithm.name(), "Using host key algorithm");
// TODO: Implement aes128-ctr
let _ = crypto::encrypt::ENC_AES128_CTR;
let encryption_client_to_server = sup_algs
.encryption_from_peer
.find(kex.encryption_algorithms_client_to_server.0)?;
debug!(name = %encryption_client_to_server.name(), "Using encryption algorithm C->S");
let encryption_server_to_client = sup_algs
.encryption_to_peer
.find(kex.encryption_algorithms_server_to_client.0)?;
debug!(name = %encryption_server_to_client.name(), "Using encryption algorithm S->C");
let mac_algorithm_client_to_server = sup_algs
.mac_from_peer
.find(kex.mac_algorithms_client_to_server.0)?;
let mac_algorithm_server_to_client = sup_algs
.mac_to_peer
.find(kex.mac_algorithms_server_to_client.0)?;
let compression_algorithm_client_to_server = sup_algs
.compression_from_peer
.find(kex.compression_algorithms_client_to_server.0)?;
let compression_algorithm_server_to_client = sup_algs
.compression_to_peer
.find(kex.compression_algorithms_server_to_client.0)?;
let _ = kex.languages_client_to_server;
let _ = kex.languages_server_to_client;
if kex.first_kex_packet_follows {
return Err(peer_error!(
"the client wants to send a guessed packet, that's annoying :("
));
}
let mut cookie = [0; 16];
self.rng.fill_bytes(&mut cookie);
let server_kexinit = KeyExchangeInitPacket {
cookie,
kex_algorithms: NameList::one(kex_algorithm.name()),
server_host_key_algorithms: NameList::one(server_host_key_algorithm.name()),
encryption_algorithms_client_to_server: NameList::one(
encryption_client_to_server.name(),
),
encryption_algorithms_server_to_client: NameList::one(
encryption_server_to_client.name(),
),
mac_algorithms_client_to_server: NameList::one(
mac_algorithm_client_to_server,
),
mac_algorithms_server_to_client: NameList::one(
mac_algorithm_server_to_client,
),
compression_algorithms_client_to_server: NameList::one(
compression_algorithm_client_to_server,
),
compression_algorithms_server_to_client: NameList::one(
compression_algorithm_server_to_client,
),
languages_client_to_server: NameList::none(),
languages_server_to_client: NameList::none(),
first_kex_packet_follows: false,
};
let client_identification = take(client_identification);
let server_kexinit_payload = server_kexinit.to_bytes();
self.packet_transport.queue_packet(Packet {
payload: server_kexinit_payload.clone(),
});
self.state = ServerState::DhKeyInit {
client_identification,
client_kexinit: packet.payload,
server_kexinit: server_kexinit_payload,
kex_algorithm,
server_host_key_algorithm,
encryption_client_to_server,
encryption_server_to_client,
};
}
ServerState::DhKeyInit {
client_identification,
client_kexinit,
server_kexinit,
kex_algorithm,
server_host_key_algorithm,
encryption_client_to_server,
encryption_server_to_client,
} => {
let dh = KeyExchangeEcDhInitPacket::parse(&packet.payload)?;
let client_public_key = dh.qc;
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 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);
// 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(
&pub_hostkey.0,
&server_public_key,
&signature.0,
);
self.packet_transport.queue_packet(packet);
self.state = ServerState::NewKeys {
h: hash,
k: shared_secret,
encryption_client_to_server: *encryption_client_to_server,
encryption_server_to_client: *encryption_server_to_client,
};
}
ServerState::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.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 {};
}
ServerState::ServiceRequest => {
// TODO: this should probably move out of here? unsure.
if packet.payload.first() != Some(&numbers::SSH_MSG_SERVICE_REQUEST) {
return Err(peer_error!("did not send SSH_MSG_SERVICE_REQUEST"));
}
let mut p = Parser::new(&packet.payload[1..]);
let service = p.utf8_string()?;
debug!(%service, "Client requesting service");
if service != "ssh-userauth" {
return Err(peer_error!("only supports ssh-userauth"));
}
self.packet_transport.queue_packet(Packet {
payload: {
let mut writer = Writer::new();
writer.u8(numbers::SSH_MSG_SERVICE_ACCEPT);
writer.string(service.as_bytes());
writer.finish()
},
});
self.state = ServerState::Open;
}
ServerState::Open => {
self.plaintext_packets.push_back(packet);
}
}
}
Ok(())
}
pub fn next_msg_to_send(&mut self) -> Option<Msg> {
self.packet_transport.next_msg_to_send()
}
pub fn next_plaintext_packet(&mut self) -> Option<Packet> {
self.plaintext_packets.pop_front()
}
pub fn send_plaintext_packet(&mut self, packet: Packet) {
self.packet_transport.queue_packet(packet);
}
}
/// Manually extracted from the key using <https://peterlyons.com/problog/2017/12/openssh-ed25519-private-key-file-format/>, 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};
struct NoRng;
impl SshRng for NoRng {
fn fill_bytes(&mut self, _: &mut [u8]) {
unreachable!()
}
}
struct HardcodedRng(Vec<u8>);
impl SshRng for HardcodedRng {
fn fill_bytes(&mut self, dest: &mut [u8]) {
dest.copy_from_slice(&self.0[..dest.len()]);
self.0.splice(0..dest.len(), []);
}
}
#[test]
fn protocol_exchange() {
let mut con = ServerConnection::new(NoRng);
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(_)));
}
#[test]
fn protocol_exchange_slow_client() {
let mut con = ServerConnection::new(NoRng);
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();
assert!(matches!(msg.0, MsgKind::ServerProtocolInfo(_)));
}
#[test]
#[ignore = "this is super annoying, use expect-test please"]
fn handshake() {
#[rustfmt::skip]
let rng = vec![
0x14, 0xa2, 0x04, 0xa5, 0x4b, 0x2f, 0x5f, 0xa7, 0xff, 0x53, 0x13, 0x67, 0x57, 0x67, 0xbc,
0x55, 0x3f, 0xc0, 0x6c, 0x0d, 0x07, 0x8f, 0xe2, 0x75, 0x95, 0x18, 0x4b, 0xd2, 0xcb, 0xd0,
0x64, 0x06, 0x14, 0xa2, 0x04, 0xa5, 0x4b, 0x2f, 0x5f, 0xa7, 0xff, 0x53, 0x13, 0x67, 0x57,
0x67, 0xbc, 0x55, 0x3f, 0xc0, 0x6c, 0x0d, 0x07, 0x8f, 0xe2, 0x75, 0x95, 0x18, 0x4b, 0xd2,
0xcb, 0xd0, 0x64, 0x06, 0x67, 0xbc, 0x55, 0x3f, 0xc0, 0x6c, 0x0d, 0x07, 0x8f, 0xe2, 0x75,
0x95, 0x18, 0x4b, 0xd2, 0xcb, 0xd0, 0x64, 0x06,
];
struct Part {
client: &'static [u8],
server: &'static [u8],
}
// Extracted from a real OpenSSH client using this server (with hardcoded creds) using Wireshark.
let conversation = [
Part {
client: &hex!("5353482d322e302d4f70656e5353485f392e370d0a"),
server: &hex!("5353482d322e302d4f70656e5353485f392e370d0a"),
},
// KEX Init
Part {
client: &hex!(
"000005fc0714fd3d911937c7294823f93c5ba691f77e00000131736e747275703736317832353531392d736861353132406f70656e7373682e636f6d2c637572766532353531392d7368613235362c637572766532353531392d736861323536406c69627373682e6f72672c656364682d736861322d6e697374703235362c656364682d736861322d6e697374703338342c656364682d736861322d6e697374703532312c6469666669652d68656c6c6d616e2d67726f75702d65786368616e67652d7368613235362c6469666669652d68656c6c6d616e2d67726f757031362d7368613531322c6469666669652d68656c6c6d616e2d67726f757031382d7368613531322c6469666669652d68656c6c6d616e2d67726f757031342d7368613235362c6578742d696e666f2d632c6b65782d7374726963742d632d763030406f70656e7373682e636f6d000001cf7373682d656432353531392d636572742d763031406f70656e7373682e636f6d2c65636473612d736861322d6e697374703235362d636572742d763031406f70656e7373682e636f6d2c65636473612d736861322d6e697374703338342d636572742d763031406f70656e7373682e636f6d2c65636473612d736861322d6e697374703532312d636572742d763031406f70656e7373682e636f6d2c736b2d7373682d656432353531392d636572742d763031406f70656e7373682e636f6d2c736b2d65636473612d736861322d6e697374703235362d636572742d763031406f70656e7373682e636f6d2c7273612d736861322d3531322d636572742d763031406f70656e7373682e636f6d2c7273612d736861322d3235362d636572742d763031406f70656e7373682e636f6d2c7373682d656432353531392c65636473612d736861322d6e697374703235362c65636473612d736861322d6e697374703338342c65636473612d736861322d6e697374703532312c736b2d7373682d65643235353139406f70656e7373682e636f6d2c736b2d65636473612d736861322d6e69737470323536406f70656e7373682e636f6d2c7273612d736861322d3531322c7273612d736861322d3235360000006c63686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733139322d6374722c6165733235362d6374722c6165733132382d67636d406f70656e7373682e636f6d2c6165733235362d67636d406f70656e7373682e636f6d0000006c63686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733139322d6374722c6165733235362d6374722c6165733132382d67636d406f70656e7373682e636f6d2c6165733235362d67636d406f70656e7373682e636f6d000000d5756d61632d36342d65746d406f70656e7373682e636f6d2c756d61632d3132382d65746d406f70656e7373682e636f6d2c686d61632d736861322d3235362d65746d406f70656e7373682e636f6d2c686d61632d736861322d3531322d65746d406f70656e7373682e636f6d2c686d61632d736861312d65746d406f70656e7373682e636f6d2c756d61632d3634406f70656e7373682e636f6d2c756d61632d313238406f70656e7373682e636f6d2c686d61632d736861322d3235362c686d61632d736861322d3531322c686d61632d73686131000000d5756d61632d36342d65746d406f70656e7373682e636f6d2c756d61632d3132382d65746d406f70656e7373682e636f6d2c686d61632d736861322d3235362d65746d406f70656e7373682e636f6d2c686d61632d736861322d3531322d65746d406f70656e7373682e636f6d2c686d61632d736861312d65746d406f70656e7373682e636f6d2c756d61632d3634406f70656e7373682e636f6d2c756d61632d313238406f70656e7373682e636f6d2c686d61632d736861322d3235362c686d61632d736861322d3531322c686d61632d736861310000001a6e6f6e652c7a6c6962406f70656e7373682e636f6d2c7a6c69620000001a6e6f6e652c7a6c6962406f70656e7373682e636f6d2c7a6c69620000000000000000000000000000000000000000"
),
server: &hex!(
"000000bc051414a204a54b2f5fa7ff5313675767bc5500000011637572766532353531392d7368613235360000000b7373682d656432353531390000001d63686163686132302d706f6c7931333035406f70656e7373682e636f6d0000001d63686163686132302d706f6c7931333035406f70656e7373682e636f6d0000000d686d61632d736861322d3235360000000d686d61632d736861322d323536000000046e6f6e65000000046e6f6e65000000000000000000000000000000000000"
),
},
// ECDH KEX Init
Part {
client: &hex!(
"0000002c061e000000204c646d1281abf23264d63db96e05c0223cfead668d9d38c62579b8856e67ae19000000000000"
),
server: &hex!(
"000000bc081f000000330000000b7373682d6564323535313900000020e939cdfa6fc0d737333b534e913dd332c8d5179fe00c3045575217224b19b8f6000000204260e2c5e5383f1a021c9631fa61f60f305b29183fd219d4c8207c664e063410000000530000000b7373682d65643235353139000000406504a045499f26aa4ee17606ea6bd9e3f288838591f25d8604a63f77a52f5b9e909c00d10f386553e585d86ab329bbde0fca5c64b1b1982d7adcac17cf7f06010000000000000000"
),
},
// New Keys
Part {
client: &hex!("0000000c0a1500000000000000000000"),
server: &hex!("0000000c0a1500000000000000000000"),
},
// Service Request (encrypted)
Part {
client: &hex!("09ca4db7baeb24836a1f7d22368055bf4c26981ed86738ac7a5c31d0730ad656f1967853781dff91ee1c4de8"),
server: &hex!("7b444c0d5faf740d350701a054ea469fab1c98e4b669e4872a454163edb42ec5e4fa95c404ab601f016bd259"),
},
];
let mut con = ServerConnection::new(HardcodedRng(rng));
for part in conversation {
con.recv_bytes(&part.client).unwrap();
eprintln!("client: {:x?}", part.client);
let bytes = con.next_msg_to_send().unwrap().to_bytes();
if part.server != bytes {
panic!(
"expected != found\nexpected: {:x?}\nfound: {:x?}",
part.server, bytes
);
}
}
}
}