try adding AES-GCM

This commit is contained in:
nora 2024-08-12 21:12:33 +02:00
parent 43c1696465
commit 4c3f0a97aa
5 changed files with 243 additions and 52 deletions

View file

@ -1,406 +0,0 @@
use chacha20::cipher::{KeyInit, StreamCipher, StreamCipherSeek};
use sha2::Digest;
use subtle::ConstantTimeEq;
use crate::{
client_error,
packet::{EncryptedPacket, MsgKind, Packet, RawPacket},
Msg, Result, SshRng,
};
pub trait AlgorithmName {
fn name(&self) -> &'static str;
}
#[derive(Clone, Copy)]
pub struct KexAlgorithm {
name: &'static str,
pub exchange: fn(
client_public_key: &[u8],
random: &mut (dyn SshRng + Send + Sync),
) -> Result<KexAlgorithmOutput>,
}
impl AlgorithmName for KexAlgorithm {
fn name(&self) -> &'static str {
self.name
}
}
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(),
})
},
};
/// <https://datatracker.ietf.org/doc/html/rfc5656>
pub const KEX_ECDH_SHA2_NISTP256: KexAlgorithm = KexAlgorithm {
name: "ecdh-sha2-nistp256",
exchange: |client_public_key, rng| {
let secret = p256::ecdh::EphemeralSecret::random(&mut crate::SshRngRandAdapter(rng));
let server_public_key = p256::EncodedPoint::from(secret.public_key()); // Q_S
let client_public_key =
p256::PublicKey::from_sec1_bytes(client_public_key).map_err(|_| {
crate::client_error!(
"invalid p256 public key length: {}",
client_public_key.len()
)
})?; // Q_C
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.raw_secret_bytes().to_vec(),
})
},
};
#[derive(Clone, Copy)]
pub struct EncryptionAlgorithm {
name: &'static str,
decrypt_len: fn(keys: &[u8], bytes: &mut [u8], packet_number: u64),
decrypt_packet: fn(keys: &[u8], bytes: RawPacket, packet_number: u64) -> Result<Packet>,
encrypt_packet: fn(keys: &[u8], packet: Packet, packet_number: u64) -> EncryptedPacket,
}
impl AlgorithmName for EncryptionAlgorithm {
fn name(&self) -> &'static str {
self.name
}
}
pub const ENC_CHACHA20POLY1305: EncryptionAlgorithm = EncryptionAlgorithm {
name: "chacha20-poly1305@openssh.com",
decrypt_len: |keys, bytes, packet_number| {
let alg = SshChaCha20Poly1305::from_keys(keys);
alg.decrypt_len(bytes, packet_number)
},
decrypt_packet: |keys, bytes, packet_number| {
let alg = SshChaCha20Poly1305::from_keys(keys);
alg.decrypt_packet(bytes, packet_number)
},
encrypt_packet: |keys, packet, packet_number| {
let alg = SshChaCha20Poly1305::from_keys(keys);
alg.encrypt_packet(packet, packet_number)
},
};
pub struct AlgorithmNegotiation<T> {
pub supported: Vec<T>,
}
impl<T: AlgorithmName> AlgorithmNegotiation<T> {
pub fn find<'a>(mut self, client_supports: &str) -> Result<T> {
for client_alg in client_supports.split(',') {
if let Some(alg) = self
.supported
.iter()
.position(|alg| alg.name() == client_alg)
{
return Ok(self.supported.remove(alg));
}
}
Err(client_error!(
"client does not support any matching algorithm: supported: {client_supports:?}"
))
}
}
pub(crate) struct Session {
session_id: [u8; 32],
encryption_key_client_to_server: [u8; 64],
encryption_client_to_server: EncryptionAlgorithm,
encryption_key_server_to_client: [u8; 64],
encryption_server_to_client: 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,
) -> 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,
) -> Result<(), ()> {
Err(())
}
}
impl Session {
pub(crate) fn new(
h: [u8; 32],
k: &[u8],
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
) -> Self {
Self::from_keys(
h,
h,
k,
encryption_client_to_server,
encryption_server_to_client,
)
}
/// <https://datatracker.ietf.org/doc/html/rfc4253#section-7.2>
fn from_keys(
session_id: [u8; 32],
h: [u8; 32],
k: &[u8],
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
) -> Self {
let encryption_key_client_to_server = derive_key(k, h, "C", session_id);
let encryption_key_server_to_client = derive_key(k, h, "D", session_id);
Self {
session_id,
// client_to_server_iv: derive("A").into(),
// server_to_client_iv: derive("B").into(),
encryption_key_client_to_server,
encryption_client_to_server,
encryption_key_server_to_client,
encryption_server_to_client,
// 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.encryption_client_to_server.decrypt_len)(
&self.encryption_key_client_to_server,
bytes,
packet_number,
);
}
fn decrypt_packet(&mut self, bytes: RawPacket, packet_number: u64) -> Result<Packet> {
(self.encryption_client_to_server.decrypt_packet)(
&self.encryption_key_client_to_server,
bytes,
packet_number,
)
}
fn encrypt_packet_to_msg(&mut self, packet: Packet, packet_number: u64) -> Msg {
let packet = (self.encryption_server_to_client.encrypt_packet)(
&self.encryption_key_server_to_client,
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,
) -> Result<(), ()> {
*self = Self::from_keys(
self.session_id,
h,
k,
encryption_client_to_server,
encryption_server_to_client,
);
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]) -> [u8; 64] {
let sha2len = sha2::Sha256::output_size();
let mut output = [0; 64];
//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);
//output[..sha2len].copy_from_slice(&hash.finalize());
for i in 0..(64 / 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
}
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);
}
/// `chacha20-poly1305@openssh.com` uses a 64-bit nonce, not the 96-bit one in the IETF version.
type SshChaCha20 = chacha20::ChaCha20Legacy;
struct SshChaCha20Poly1305 {
header_key: chacha20::Key,
main_key: chacha20::Key,
}
impl SshChaCha20Poly1305 {
fn from_keys(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::client_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);
// 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)
}
}