mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
the clueless rename
This commit is contained in:
parent
ea28daca0c
commit
9ce60280b1
46 changed files with 264 additions and 262 deletions
24
lib/cluelessh-transport/Cargo.toml
Normal file
24
lib/cluelessh-transport/Cargo.toml
Normal 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"
|
||||
15
lib/cluelessh-transport/README.md
Normal file
15
lib/cluelessh-transport/README.md
Normal 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)
|
||||
393
lib/cluelessh-transport/src/client.rs
Normal file
393
lib/cluelessh-transport/src/client.rs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
523
lib/cluelessh-transport/src/crypto.rs
Normal file
523
lib/cluelessh-transport/src/crypto.rs
Normal 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()
|
||||
}
|
||||
253
lib/cluelessh-transport/src/crypto/encrypt.rs
Normal file
253
lib/cluelessh-transport/src/crypto/encrypt.rs
Normal 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!()
|
||||
}
|
||||
}
|
||||
68
lib/cluelessh-transport/src/key.rs
Normal file
68
lib/cluelessh-transport/src/key.rs
Normal 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)
|
||||
}
|
||||
53
lib/cluelessh-transport/src/lib.rs
Normal file
53
lib/cluelessh-transport/src/lib.rs
Normal 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)*))
|
||||
};
|
||||
}
|
||||
143
lib/cluelessh-transport/src/numbers.rs
Normal file
143
lib/cluelessh-transport/src/numbers.rs
Normal 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;
|
||||
540
lib/cluelessh-transport/src/packet.rs
Normal file
540
lib/cluelessh-transport/src/packet.rs
Normal 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]);
|
||||
}
|
||||
}
|
||||
147
lib/cluelessh-transport/src/packet/ctors.rs
Normal file
147
lib/cluelessh-transport/src/packet/ctors.rs
Normal 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);
|
||||
}
|
||||
205
lib/cluelessh-transport/src/parse.rs
Normal file
205
lib/cluelessh-transport/src/parse.rs
Normal 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]);
|
||||
446
lib/cluelessh-transport/src/server.rs
Normal file
446
lib/cluelessh-transport/src/server.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue