make kex more pluggable

This commit is contained in:
nora 2024-08-12 18:00:30 +02:00
parent 11fcb4cd84
commit 1cdea4763d
5 changed files with 86 additions and 66 deletions

View file

@ -11,3 +11,4 @@ Other relevant RFCs:
- [RFC 5656 Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer](https://datatracker.ietf.org/doc/html/rfc5656)
- [RFC 6668 SHA-2 Data Integrity Verification for the Secure Shell (SSH) Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc6668)
- [RFC 8709 Ed25519 and Ed448 Public Key Algorithms for the Secure Shell (SSH) Protocol](https://datatracker.ietf.org/doc/html/rfc8709)
- [RFC 8731 Secure Shell (SSH) Key Exchange Method Using Curve25519 and Curve448](https://datatracker.ietf.org/doc/html/rfc8731)

View file

@ -5,16 +5,45 @@ use subtle::ConstantTimeEq;
use crate::{
client_error,
packet::{EncryptedPacket, MsgKind, Packet, RawPacket},
Msg, Result,
Msg, Result, SshRng,
};
#[derive(Clone, Copy)]
pub struct KexAlgorithm {
pub name: &'static str,
pub exchange: fn(
client_public_key: &[u8],
random: &mut (dyn SshRng + Send + Sync),
) -> Result<KexAlgorithmOutput>,
}
pub struct KexAlgorithmOutput {
/// K
pub shared_secret: Vec<u8>,
/// Q_S
pub server_public_key: Vec<u8>,
}
/// <https://datatracker.ietf.org/doc/html/rfc8731>
pub const KEX_CURVE_25519_SHA256: KexAlgorithm = KexAlgorithm {
name: "curve25519-sha256",
exchange: |client_public_key, rng| {
let secret = x25519_dalek::EphemeralSecret::random_from_rng(crate::SshRngRandAdapter(rng));
let server_public_key = x25519_dalek::PublicKey::from(&secret); // Q_S
let Ok(arr) = <[u8; 32]>::try_from(client_public_key) else {
return Err(crate::client_error!(
"invalid x25519 public key length, should be 32, was: {}",
client_public_key.len()
));
};
let client_public_key = x25519_dalek::PublicKey::from(arr);
let shared_secret = secret.diffie_hellman(&client_public_key); // K
Ok(KexAlgorithmOutput {
server_public_key: server_public_key.as_bytes().to_vec(),
shared_secret: shared_secret.as_bytes().to_vec(),
})
},
};
pub struct AlgorithmNegotiation<T> {
@ -48,7 +77,7 @@ pub(crate) trait Keys: Send + Sync + 'static {
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; 32]) -> Result<(), ()>;
fn rekey(&mut self, h: [u8; 32], k: &[u8]) -> Result<(), ()>;
}
pub(crate) struct Plaintext;
@ -63,18 +92,18 @@ impl Keys for Plaintext {
fn additional_mac_len(&self) -> usize {
0
}
fn rekey(&mut self, _: [u8; 32], _: [u8; 32]) -> Result<(), ()> {
fn rekey(&mut self, _: [u8; 32], _: &[u8]) -> Result<(), ()> {
Err(())
}
}
impl Session {
pub(crate) fn new(h: [u8; 32], k: [u8; 32]) -> Self {
pub(crate) fn new(h: [u8; 32], k: &[u8]) -> Self {
Self::from_keys(h, 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 {
fn from_keys(session_id: [u8; 32], h: [u8; 32], k: &[u8]) -> Self {
let encryption_key_client_to_server =
SshChaCha20Poly1305::new(derive_key(k, h, "C", session_id));
let encryption_key_server_to_client =
@ -114,7 +143,7 @@ impl Keys for Session {
poly1305::BLOCK_SIZE
}
fn rekey(&mut self, h: [u8; 32], k: [u8; 32]) -> Result<(), ()> {
fn rekey(&mut self, h: [u8; 32], k: &[u8]) -> Result<(), ()> {
*self = Self::from_keys(self.session_id, h, k);
Ok(())
}
@ -122,7 +151,7 @@ impl Keys for Session {
/// 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; 32], h: [u8; 32], letter: &str, session_id: [u8; 32]) -> [u8; 64] {
fn derive_key(k: &[u8], h: [u8; 32], letter: &str, session_id: [u8; 32]) -> [u8; 64] {
let sha2len = sha2::Sha256::output_size();
let mut output = [0; 64];
@ -254,7 +283,7 @@ impl SshChaCha20Poly1305 {
// 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);
bytes.extend_from_slice(mac.as_slice());
EncryptedPacket::from_encrypted_full_bytes(bytes)
}

View file

@ -8,14 +8,13 @@ use std::{collections::VecDeque, mem::take};
use ed25519_dalek::ed25519::signature::Signer;
use keys::AlgorithmNegotiation;
use packet::{
DhKeyExchangeInitPacket, DhKeyExchangeInitReplyPacket, KeyExchangeInitPacket, Packet,
DhKeyExchangeInitReplyPacket, KeyExchangeEcDhInitPacket, KeyExchangeInitPacket, Packet,
PacketTransport, SshPublicKey, SshSignature,
};
use parse::{MpInt, NameList, Parser, Writer};
use parse::{NameList, Parser, Writer};
use rand::RngCore;
use sha2::Digest;
use tracing::{debug, info, trace};
use x25519_dalek::{EphemeralSecret, PublicKey};
pub use packet::Msg;
@ -64,10 +63,11 @@ enum ServerState {
client_identification: Vec<u8>,
client_kexinit: Vec<u8>,
server_kexinit: Vec<u8>,
kex_algorithm: keys::KexAlgorithm,
},
NewKeys {
h: [u8; 32],
k: [u8; 32],
k: Vec<u8>,
},
ServiceRequest,
Open,
@ -250,24 +250,24 @@ impl ServerConnection {
client_identification,
client_kexinit: packet.payload,
server_kexinit: server_kexinit_payload,
kex_algorithm,
};
}
ServerState::DhKeyInit {
client_identification,
client_kexinit,
server_kexinit,
kex_algorithm,
} => {
// TODO: move to keys.rs
let dh = DhKeyExchangeInitPacket::parse(&packet.payload)?;
let dh = KeyExchangeEcDhInitPacket::parse(&packet.payload)?;
let secret =
EphemeralSecret::random_from_rng(SshRngRandAdapter(&mut *self.rng));
let server_public_key = PublicKey::from(&secret); // Q_S
let client_public_key = dh.qc;
let client_public_key = dh.e; // Q_C
let shared_secret =
secret.diffie_hellman(&client_public_key.as_x25519_public_key()?); // K
let keys::KexAlgorithmOutput {
server_public_key,
shared_secret,
} = (kex_algorithm.exchange)(client_public_key, &mut *self.rng)?;
let pub_hostkey = SshPublicKey {
format: b"ssh-ed25519",
@ -297,12 +297,13 @@ impl ServerConnection {
hash_string(&mut hash, client_kexinit); // I_C
hash_string(&mut hash, server_kexinit); // I_S
add_hash(&mut hash, &pub_hostkey.to_bytes()); // 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.0); // Q_C
hash_string(&mut hash, server_public_key.as_bytes()); // Q_S
hash_mpint(&mut hash, shared_secret.as_bytes()); // K
// 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();
@ -317,8 +318,8 @@ impl ServerConnection {
// eprintln!("hash: {:x?}", hash);
let packet = DhKeyExchangeInitReplyPacket {
pubkey: pub_hostkey,
f: MpInt(server_public_key.as_bytes()),
public_host_key: pub_hostkey,
ephemeral_public_key: &server_public_key,
signature: SshSignature {
format: b"ssh-ed25519",
data: &signature.to_bytes(),
@ -329,7 +330,7 @@ impl ServerConnection {
});
self.state = ServerState::NewKeys {
h: hash.into(),
k: shared_secret.to_bytes(),
k: shared_secret,
};
}
ServerState::NewKeys { h, k } => {
@ -337,13 +338,11 @@ impl ServerConnection {
return Err(client_error!("did not send SSH_MSG_NEWKEYS"));
}
let (h, k) = (*h, *k);
self.packet_transport.queue_packet(Packet {
payload: vec![Packet::SSH_MSG_NEWKEYS],
});
self.packet_transport.set_key(*h, k);
self.state = ServerState::ServiceRequest {};
self.packet_transport.set_key(h, k);
}
ServerState::ServiceRequest => {
// TODO: this should probably move out of here? unsure.

View file

@ -4,7 +4,7 @@ use std::collections::VecDeque;
use crate::client_error;
use crate::keys::{Keys, Plaintext, Session};
use crate::parse::{MpInt, NameList, Parser, Writer};
use crate::parse::{NameList, Parser, Writer};
use crate::Result;
/// Frames the byte stream into packets.
@ -101,7 +101,7 @@ impl PacketTransport {
self.send_packets.pop_front()
}
pub(crate) fn set_key(&mut self, h: [u8; 32], k: [u8; 32]) {
pub(crate) fn set_key(&mut self, h: [u8; 32], k: &[u8]) {
if let Err(()) = self.keys.rekey(h, k) {
self.keys = Box::new(Session::new(h, k));
}
@ -147,7 +147,9 @@ impl Packet {
// 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:
@ -329,21 +331,21 @@ impl<'a> KeyExchangeInitPacket<'a> {
}
#[derive(Debug)]
pub(crate) struct DhKeyExchangeInitPacket<'a> {
pub(crate) e: MpInt<'a>,
pub(crate) struct KeyExchangeEcDhInitPacket<'a> {
pub(crate) qc: &'a [u8],
}
impl<'a> DhKeyExchangeInitPacket<'a> {
pub(crate) fn parse(payload: &'a [u8]) -> Result<DhKeyExchangeInitPacket<'_>> {
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 != Packet::SSH_MSG_KEXDH_INIT {
if kind != Packet::SSH_MSG_KEX_ECDH_INIT {
return Err(client_error!(
"expected SSH_MSG_KEXDH_INIT packet, found {kind}"
));
}
let e = c.mpint()?;
Ok(Self { e })
let qc = c.string()?;
Ok(Self { qc })
}
}
@ -371,17 +373,19 @@ pub(crate) struct SshSignature<'a> {
#[derive(Debug)]
pub(crate) struct DhKeyExchangeInitReplyPacket<'a> {
pub(crate) pubkey: SshPublicKey<'a>,
pub(crate) f: MpInt<'a>,
/// K_S
pub(crate) public_host_key: SshPublicKey<'a>,
/// Q_S
pub(crate) ephemeral_public_key: &'a [u8],
pub(crate) signature: SshSignature<'a>,
}
impl<'a> DhKeyExchangeInitReplyPacket<'a> {
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let mut data = Writer::new();
data.u8(Packet::SSH_MSG_KEXDH_REPLY);
data.write(&self.pubkey.to_bytes());
data.mpint(self.f);
data.u8(Packet::SSH_MSG_KEX_ECDH_REPLY);
data.write(&self.public_host_key.to_bytes());
data.string(self.ephemeral_public_key);
data.u32((4 + self.signature.format.len() + 4 + self.signature.data.len()) as u32);
// <https://datatracker.ietf.org/doc/html/rfc8709#section-6>
@ -480,7 +484,9 @@ impl PacketParser {
// 'padding_length', 'payload', 'random padding', and 'mac').
// Implementations SHOULD support longer packets, where they might be needed.
if packet_length > 500_000 {
return Err(client_error!("packet too large (>500_000): {packet_length}"));
return Err(client_error!(
"packet too large (>500_000): {packet_length}"
));
}
let remaining_len = std::cmp::min(bytes.len(), packet_length - (self.raw_data.len() - 4));

View file

@ -58,8 +58,7 @@ impl<'a> Parser<'a> {
}
pub fn mpint(&mut self) -> Result<MpInt<'a>> {
let data = self.string()?;
Ok(MpInt(data))
todo!("do correctly")
}
pub fn string(&mut self) -> Result<&'a [u8]> {
@ -101,8 +100,8 @@ impl Writer {
self.string(list.0.as_bytes());
}
pub fn mpint(&mut self, mpint: MpInt<'_>) {
self.string(mpint.0);
pub fn mpint(&mut self, _mpint: MpInt<'_>) {
todo!("implement correctly?")
}
pub fn string(&mut self, data: &[u8]) {
@ -143,19 +142,5 @@ impl Debug for NameList<'_> {
}
}
// TODO: THIS IS A BRITTLE MESS BECAUSE THE RFC SUCKS HERE
// DO NOT TOUCH MPINT ENCODING ANYWHERE
#[derive(Debug, Clone, Copy)]
pub struct MpInt<'a>(pub(crate) &'a [u8]);
impl<'a> MpInt<'a> {
pub(crate) fn as_x25519_public_key(&self) -> Result<x25519_dalek::PublicKey> {
let Ok(arr) = <[u8; 32]>::try_from(self.0) else {
return Err(crate::client_error!(
"invalid x25519 public key length, should be 32, was: {}",
self.0.len()
));
};
Ok(x25519_dalek::PublicKey::from(arr))
}
}
pub struct MpInt<'a>(pub &'a [u8]);