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

68
Cargo.lock generated
View file

@ -17,6 +17,41 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aes-gcm"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@ -153,9 +188,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"rand_core",
"typenum",
]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
@ -325,6 +370,16 @@ dependencies = [
"wasi",
]
[[package]]
name = "ghash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]]
name = "gimli"
version = "0.29.0"
@ -565,6 +620,18 @@ dependencies = [
"universal-hash",
]
[[package]]
name = "polyval"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "ppv-lite86"
version = "0.2.20"
@ -859,6 +926,7 @@ dependencies = [
name = "ssh-transport"
version = "0.1.0"
dependencies = [
"aes-gcm",
"chacha20",
"ed25519-dalek",
"eyre",

View file

@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
aes-gcm = "0.10.3"
chacha20 = "0.9.1"
ed25519-dalek = { version = "2.1.1" }
eyre = "0.6.12"

View file

@ -1,3 +1,4 @@
use aes_gcm::aead::{Aead, AeadMutInPlace};
use chacha20::cipher::{KeyInit, StreamCipher, StreamCipherSeek};
use sha2::Digest;
use subtle::ConstantTimeEq;
@ -81,9 +82,11 @@ pub const KEX_ECDH_SHA2_NISTP256: KexAlgorithm = KexAlgorithm {
#[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,
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 {
@ -92,16 +95,35 @@ impl AlgorithmName for EncryptionAlgorithm {
}
pub const ENC_CHACHA20POLY1305: EncryptionAlgorithm = EncryptionAlgorithm {
name: "chacha20-poly1305@openssh.com",
decrypt_len: |keys, bytes, packet_number| {
let alg = SshChaCha20Poly1305::from_keys(keys);
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: |keys, bytes, packet_number| {
let alg = SshChaCha20Poly1305::from_keys(keys);
decrypt_packet: |state, bytes, packet_number| {
let alg = ChaCha20Poly1305OpenSsh::from_state(state);
alg.decrypt_packet(bytes, packet_number)
},
encrypt_packet: |keys, packet, packet_number| {
let alg = SshChaCha20Poly1305::from_keys(keys);
encrypt_packet: |state, packet, packet_number| {
let alg = ChaCha20Poly1305OpenSsh::from_state(state);
alg.encrypt_packet(packet, packet_number)
},
};
pub const ENC_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)
},
};
@ -130,10 +152,14 @@ impl<T: AlgorithmName> AlgorithmNegotiation<T> {
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,
client_to_server: Tunnel,
server_to_client: Tunnel,
}
struct Tunnel {
/// `key || IV`
state: Vec<u8>,
algorithm: EncryptionAlgorithm,
}
pub(crate) trait Keys: Send + Sync + 'static {
@ -196,20 +222,27 @@ impl Session {
session_id: [u8; 32],
h: [u8; 32],
k: &[u8],
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
alg_c2s: EncryptionAlgorithm,
alg_s2c: 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,
client_to_server: Tunnel {
algorithm: alg_c2s,
state: {
let mut state = derive_key(k, h, "C", session_id, alg_c2s.key_size);
state.extend_from_slice(&derive_key(k, h, "A", session_id, alg_c2s.iv_size));
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_server_to_client: derive("F").into(),
}
@ -218,24 +251,24 @@ impl Session {
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,
(self.client_to_server.algorithm.decrypt_len)(
&mut self.client_to_server.state,
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,
(self.client_to_server.algorithm.decrypt_packet)(
&mut self.client_to_server.state,
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,
let packet = (self.server_to_client.algorithm.encrypt_packet)(
&mut self.server_to_client.state,
packet,
packet_number,
);
@ -266,9 +299,15 @@ 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], 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],
key_size: usize,
) -> Vec<u8> {
let sha2len = sha2::Sha256::output_size();
let mut output = [0; 64];
let mut output = vec![0; key_size];
//let mut hash = sha2::Sha256::new();
//encode_mpint_for_hash(&k, |data| hash.update(data));
@ -277,7 +316,7 @@ fn derive_key(k: &[u8], h: [u8; 32], letter: &str, session_id: [u8; 32]) -> [u8;
//hash.update(session_id);
//output[..sha2len].copy_from_slice(&hash.finalize());
for i in 0..(64 / sha2len) {
for i in 0..(key_size / sha2len) {
let mut hash = <sha2::Sha256 as sha2::Digest>::new();
encode_mpint_for_hash(&k, |data| hash.update(data));
hash.update(h);
@ -311,13 +350,14 @@ pub(crate) fn encode_mpint_for_hash(mut key: &[u8], mut add_to_hash: impl FnMut(
/// `chacha20-poly1305@openssh.com` uses a 64-bit nonce, not the 96-bit one in the IETF version.
type SshChaCha20 = chacha20::ChaCha20Legacy;
struct SshChaCha20Poly1305 {
/// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
struct ChaCha20Poly1305OpenSsh {
header_key: chacha20::Key,
main_key: chacha20::Key,
}
impl SshChaCha20Poly1305 {
fn from_keys(keys: &[u8]) -> Self {
impl ChaCha20Poly1305OpenSsh {
fn from_state(keys: &[u8]) -> Self {
assert_eq!(keys.len(), 64);
Self {
main_key: <[u8; 32]>::try_from(&keys[..32]).unwrap().into(),
@ -370,7 +410,7 @@ impl SshChaCha20Poly1305 {
}
fn encrypt_packet(&self, packet: Packet, packet_number: u64) -> EncryptedPacket {
let mut bytes = packet.to_bytes(false);
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(
@ -404,3 +444,81 @@ impl SshChaCha20Poly1305 {
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::client_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 bytes = packet.to_bytes(
false,
<aes_gcm::aes::Aes256 as aes_gcm::aes::cipher::BlockSizeUser>::block_size() as u8,
);
let cipher = aes_gcm::Aes256Gcm::new(&self.key);
let bytes = cipher
.encrypt(
(&*self.nonce).into(),
aes_gcm::aead::Payload {
aad: &bytes[..4],
msg: &bytes[4..],
},
)
.unwrap();
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;
}
}
}

View file

@ -1,4 +1,4 @@
mod keys;
mod crypto;
pub mod packet;
pub mod parse;
@ -6,7 +6,7 @@ use core::str;
use std::{collections::VecDeque, mem::take};
use ed25519_dalek::ed25519::signature::Signer;
use keys::{AlgorithmName, AlgorithmNegotiation, EncryptionAlgorithm};
use crypto::{AlgorithmName, AlgorithmNegotiation, EncryptionAlgorithm};
use packet::{
DhKeyExchangeInitReplyPacket, KeyExchangeEcDhInitPacket, KeyExchangeInitPacket, Packet,
PacketTransport, SshPublicKey, SshSignature,
@ -63,7 +63,7 @@ enum ServerState {
client_identification: Vec<u8>,
client_kexinit: Vec<u8>,
server_kexinit: Vec<u8>,
kex_algorithm: keys::KexAlgorithm,
kex_algorithm: crypto::KexAlgorithm,
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
},
@ -179,7 +179,7 @@ impl ServerConnection {
};
let kex_algorithms = AlgorithmNegotiation {
supported: vec![keys::KEX_CURVE_25519_SHA256, keys::KEX_ECDH_SHA2_NISTP256],
supported: vec![crypto::KEX_CURVE_25519_SHA256, crypto::KEX_ECDH_SHA2_NISTP256],
};
let kex_algorithm = kex_algorithms.find(kex.kex_algorithms.0)?;
@ -187,13 +187,12 @@ impl ServerConnection {
require_algorithm("ssh-ed25519", kex.server_host_key_algorithms)?;
let encryption_algorithms_client_to_server = AlgorithmNegotiation {
supported: vec![keys::ENC_CHACHA20POLY1305],
supported: vec![crypto::ENC_CHACHA20POLY1305, crypto::ENC_AES256_GCM],
};
let encryption_algorithms_server_to_client = AlgorithmNegotiation {
supported: vec![keys::ENC_CHACHA20POLY1305],
supported: vec![crypto::ENC_CHACHA20POLY1305, crypto::ENC_AES256_GCM],
};
// TODO: support aes256-gcm@openssh.com
let encryption_client_to_server = encryption_algorithms_client_to_server
.find(kex.encryption_algorithms_client_to_server.0)?;
let encryption_server_to_client = encryption_algorithms_server_to_client
@ -270,7 +269,7 @@ impl ServerConnection {
let client_public_key = dh.qc;
let keys::KexAlgorithmOutput {
let crypto::KexAlgorithmOutput {
server_public_key,
shared_secret,
} = (kex_algorithm.exchange)(client_public_key, &mut *self.rng)?;
@ -289,7 +288,7 @@ impl ServerConnection {
add_hash(hash, bytes);
};
let hash_mpint = |hash: &mut sha2::Sha256, bytes: &[u8]| {
keys::encode_mpint_for_hash(bytes, |data| add_hash(hash, data));
crypto::encode_mpint_for_hash(bytes, |data| add_hash(hash, data));
};
hash_string(

View file

@ -3,7 +3,7 @@ mod ctors;
use std::collections::VecDeque;
use crate::client_error;
use crate::keys::{EncryptionAlgorithm, Keys, Plaintext, Session};
use crate::crypto::{EncryptionAlgorithm, Keys, Plaintext, Session};
use crate::parse::{NameList, Parser, Writer};
use crate::Result;
@ -33,7 +33,7 @@ impl Msg {
pub fn to_bytes(self) -> Vec<u8> {
match self.0 {
MsgKind::ServerProtocolInfo => crate::SERVER_IDENTIFICATION.to_vec(),
MsgKind::PlaintextPacket(v) => v.to_bytes(true),
MsgKind::PlaintextPacket(v) => v.to_bytes(true, Packet::DEFAULT_BLOCK_SIZE),
MsgKind::EncryptedPacket(v) => v.into_bytes(),
}
}
@ -199,6 +199,8 @@ impl Packet {
pub const SSH_MSG_CHANNEL_SUCCESS: u8 = 99;
pub const SSH_MSG_CHANNEL_FAILURE: u8 = 100;
pub const DEFAULT_BLOCK_SIZE: u8 = 8;
pub(crate) fn from_full(bytes: &[u8]) -> Result<Self> {
let Some(padding_length) = bytes.first() else {
return Err(client_error!("empty packet"));
@ -219,17 +221,20 @@ impl Packet {
})
}
pub(crate) fn to_bytes(&self, respect_len_for_padding: bool) -> Vec<u8> {
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 8.
let min_padding_len = (min_full_length.next_multiple_of(8) - min_full_length) as u8;
// 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 + 8
min_padding_len + block_size
} else {
min_padding_len
};