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