This commit is contained in:
nora 2024-08-10 02:49:39 +02:00
parent 0eb9001f08
commit adff1f593b
5 changed files with 195 additions and 21 deletions

View file

@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
chacha20poly1305 = "0.10.1"
crypto-bigint = "0.5.5"
ed25519-dalek = { version = "2.1.1" }
eyre = "0.6.12"

71
ssh-transport/src/keys.rs Normal file
View file

@ -0,0 +1,71 @@
use chacha20poly1305::{
aead::{Aead, AeadCore},
ChaCha20Poly1305, KeyInit,
};
use sha2::Digest;
use crate::Result;
pub(crate) struct Session {
session_id: [u8; 32],
client_to_server_iv: [u8; 32],
server_to_client_iv: [u8; 32],
encryption_key_client_to_server: ChaCha20Poly1305,
encryption_key_server_to_client: ChaCha20Poly1305,
integrity_key_server_to_client: [u8; 32],
integrity_key_client_to_server: [u8; 32],
}
impl Session {
pub(crate) fn new(h: [u8; 32], k: [u8; 32]) -> Self {
Self::from_keys(h, h, k)
}
pub(crate) fn rekey(&mut self, h: [u8; 32], k: [u8; 32]) {
*self = Self::from_keys(self.session_id, h, k);
}
/// <https://datatracker.ietf.org/doc/html/rfc4253#section-7.2>
fn from_keys(session_id: [u8; 32], h: [u8; 32], k: [u8; 32]) -> Self {
let derive = |letter: &str| {
let mut hash = sha2::Sha256::new();
encode_mpint_for_hash(&k, |data| hash.update(data));
hash.update(h);
hash.update(letter.as_bytes());
hash.update(session_id);
hash.finalize()
};
let encryption_key_client_to_server = ChaCha20Poly1305::new(&derive("C"));
let encryption_key_server_to_client = ChaCha20Poly1305::new(&derive("D"));
Self {
session_id,
client_to_server_iv: derive("A").into(),
server_to_client_iv: derive("B").into(),
encryption_key_client_to_server,
encryption_key_server_to_client,
integrity_key_client_to_server: derive("E").into(),
integrity_key_server_to_client: derive("F").into(),
}
}
pub(crate) fn decrypt_bytes(&mut self, bytes: &[u8]) -> Result<Vec<u8>> {
self.encryption_key_client_to_server
.decrypt(&[0; 12].into(), bytes)
.map_err(|_| crate::client_error!("failed to decrypt, invalid message"))
}
}
pub(crate) fn encode_mpint_for_hash(mut key: &[u8], mut add_to_hash: impl FnMut(&[u8])) {
while key[0] == 0 {
key = &key[1..];
}
// If the first high bit is set, pad it with a zero.
let pad_zero = (key[0] & 0b10000000) > 1;
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);
}

View file

@ -1,3 +1,4 @@
mod keys;
mod packet;
mod parse;
@ -55,7 +56,10 @@ enum ServerState {
client_kexinit: Vec<u8>,
server_kexinit: Vec<u8>,
},
NewKeys,
NewKeys {
h: [u8; 32],
k: [u8; 32],
},
ServiceRequest {},
}
@ -212,6 +216,7 @@ impl ServerConnection {
client_kexinit,
server_kexinit,
} => {
// TODO: move to keys.rs
let dh = DhKeyExchangeInitPacket::parse(&packet.payload)?;
let secret =
@ -236,20 +241,8 @@ impl ServerConnection {
add_hash(hash, &u32::to_be_bytes(bytes.len() as u32));
add_hash(hash, bytes);
};
let hash_mpint = |hash: &mut sha2::Sha256, mut bytes: &[u8]| {
while bytes[0] == 0 {
bytes = &bytes[1..];
}
// If the first high bit is set, pad it with a zero.
let pad_zero = (bytes[0] & 0b10000000) > 1;
add_hash(
hash,
&u32::to_be_bytes((bytes.len() + (pad_zero as usize)) as u32),
);
if pad_zero {
add_hash(hash, &[0]);
}
add_hash(hash, bytes);
let hash_mpint = |hash: &mut sha2::Sha256, bytes: &[u8]| {
keys::encode_mpint_for_hash(bytes, |data| add_hash(hash, data));
};
hash_string(
@ -293,18 +286,23 @@ impl ServerConnection {
self.queue_msg(MsgKind::Packet(Packet {
payload: packet.to_bytes(),
}));
self.state = ServerState::NewKeys;
// TODO: set keys for transport
self.state = ServerState::NewKeys {
h: hash.into(),
k: shared_secret.to_bytes(),
};
}
ServerState::NewKeys => {
ServerState::NewKeys { h, k } => {
if packet.payload != &[Packet::SSH_MSG_NEWKEYS] {
return Err(client_error!("did not send SSH_MSG_NEWKEYS"));
}
let (h, k) = (*h, *k);
self.queue_msg(MsgKind::Packet(Packet {
payload: vec![Packet::SSH_MSG_NEWKEYS],
}));
self.state = ServerState::ServiceRequest {};
self.packet_transport.set_key(h, k);
}
ServerState::ServiceRequest {} => {}
}

View file

@ -1,6 +1,7 @@
use std::collections::VecDeque;
use crate::client_error;
use crate::keys::Session;
use crate::parse::{MpInt, NameList, Parser, Writer};
use crate::Result;
@ -8,11 +9,12 @@ use crate::Result;
pub(crate) struct PacketTransport {
state: PacketTransportState,
packets: VecDeque<Packet>,
next_recv_seq_nr: u32,
}
enum PacketTransportState {
Plaintext(PacketParser),
Keyed { key: () },
Keyed { session: Session },
}
impl PacketTransport {
@ -20,6 +22,7 @@ impl PacketTransport {
PacketTransport {
state: PacketTransportState::Plaintext(PacketParser::new()),
packets: VecDeque::new(),
next_recv_seq_nr: 0,
}
}
pub(crate) fn recv_bytes(&mut self, mut bytes: &[u8]) -> Result<()> {
@ -34,6 +37,18 @@ impl PacketTransport {
pub(crate) fn next_packet(&mut self) -> Option<Packet> {
self.packets.pop_front()
}
pub(crate) fn set_key(&mut self, h: [u8; 32], k: [u8; 32]) {
match &mut self.state {
PacketTransportState::Plaintext(_) => {
self.state = PacketTransportState::Keyed {
session: Session::new(h, k),
}
}
PacketTransportState::Keyed { session } => session.rekey(h, k),
}
}
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?
match &mut self.state {
@ -41,11 +56,18 @@ impl PacketTransport {
let result = packet.recv_bytes(bytes, ())?;
if let Some((consumed, result)) = result {
self.packets.push_back(result);
self.next_recv_seq_nr = self.next_recv_seq_nr.wrapping_add(1);
*packet = PacketParser::new();
return Ok(Some(consumed));
}
}
PacketTransportState::Keyed { key } => todo!(),
PacketTransportState::Keyed { session } => {
// TODO: don't yolo?...
let encrypted_len = &bytes[..4];
// TODO: all of this is nonsense. how does AEAD even work with these partial decryptions?
// should i just validate it by hand?? i will find out tomorrow!
let decrypted_len = session.decrypt_bytes(encrypted_len)?;
}
}
Ok(None)