diff --git a/Cargo.lock b/Cargo.lock index 89f23f8..cceebe1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,16 +152,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "rand_core", - "subtle", -] - [[package]] name = "crypto-common" version = "0.1.6" @@ -701,8 +691,8 @@ dependencies = [ name = "ssh-transport" version = "0.1.0" dependencies = [ + "chacha20", "chacha20poly1305", - "crypto-bigint", "ed25519-dalek", "eyre", "hex-literal", diff --git a/ssh-transport/Cargo.toml b/ssh-transport/Cargo.toml index 2847746..74302c3 100644 --- a/ssh-transport/Cargo.toml +++ b/ssh-transport/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] +chacha20 = "0.9.1" chacha20poly1305 = "0.10.1" -crypto-bigint = "0.5.5" ed25519-dalek = { version = "2.1.1" } eyre = "0.6.12" rand = "0.8.5" diff --git a/ssh-transport/README.md b/ssh-transport/README.md new file mode 100644 index 0000000..f99e429 --- /dev/null +++ b/ssh-transport/README.md @@ -0,0 +1,12 @@ +# ssh-transport + +Transport layer of SSH. + +Based on [RFC 4253 The Secure Shell (SSH) Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc4253) +and [RFC 4251 The Secure Shell (SSH) Protocol Architecture](https://datatracker.ietf.org/doc/html/rfc4251). + +Other relevant RFCs: +- [RFC 5649 AES Galois Counter Mode for the Secure Shell Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc5647) +- [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) diff --git a/ssh-transport/src/keys.rs b/ssh-transport/src/keys.rs index bba636a..3f114ec 100644 --- a/ssh-transport/src/keys.rs +++ b/ssh-transport/src/keys.rs @@ -8,12 +8,8 @@ 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], + encryption_key_client_to_server: SshChaCha20Poly1305, + encryption_key_server_to_client: SshChaCha20Poly1305, } impl Session { @@ -27,36 +23,58 @@ impl Session { /// 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")); + let encryption_key_client_to_server = + SshChaCha20Poly1305::new(derive_key(k, h, "C", session_id)); + let encryption_key_server_to_client = + SshChaCha20Poly1305::new(derive_key(k, h, "D", session_id)); Self { session_id, - client_to_server_iv: derive("A").into(), - server_to_client_iv: derive("B").into(), + // 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(), + // 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> { + pub(crate) fn decrypt_len(&mut self, bytes: &mut [u8], packet_number: u64) { self.encryption_key_client_to_server - .decrypt(&[0; 12].into(), bytes) - .map_err(|_| crate::client_error!("failed to decrypt, invalid message")) + .decrypt_len(bytes, packet_number); } } +/// Derive a key from the shared secret K and exchange hash H. +/// +fn derive_key( + k: [u8; 32], + h: [u8; 32], + letter: &str, + session_id: [u8; 32], +) -> [u8; KEY_LEN] { + let sha2len = sha2::Sha256::output_size(); + + let mut output = [0; KEY_LEN]; + + for i in 0..(KEY_LEN / sha2len) { + let mut hash = sha2::Sha256::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); + } + 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..]; @@ -69,3 +87,29 @@ pub(crate) fn encode_mpint_for_hash(mut key: &[u8], mut add_to_hash: impl FnMut( } 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: [u8; 32], + main: ChaCha20Poly1305, +} + +impl SshChaCha20Poly1305 { + fn new(key: [u8; 64]) -> Self { + Self { + main: ChaCha20Poly1305::new(&<[u8; 32]>::try_from(&key[..32]).unwrap().into()), + header_key: key[32..].try_into().unwrap(), + } + } + + fn decrypt_len(&self, bytes: &mut [u8], packet_number: u64) { + use chacha20::cipher::{KeyIvInit, StreamCipher}; + + // + let mut cipher = + SshChaCha20::new(&self.header_key.into(), &packet_number.to_be_bytes().into()); + cipher.apply_keystream(bytes); + } +} diff --git a/ssh-transport/src/lib.rs b/ssh-transport/src/lib.rs index b972123..365588a 100644 --- a/ssh-transport/src/lib.rs +++ b/ssh-transport/src/lib.rs @@ -221,12 +221,12 @@ impl ServerConnection { let secret = EphemeralSecret::random_from_rng(SshRngRandAdapter(&mut *self.rng)); - let server_public_key = PublicKey::from(&secret); // f + let server_public_key = PublicKey::from(&secret); // Q_S - let client_public_key = dh.e; // e + let client_public_key = dh.e; // Q_C let shared_secret = - secret.diffie_hellman(&client_public_key.to_x25519_public_key()?); // k + secret.diffie_hellman(&client_public_key.to_x25519_public_key()?); // K let pub_hostkey = SshPublicKey { format: b"ssh-ed25519", @@ -256,11 +256,11 @@ 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 - - // While the RFC says that e and f are mpints, we need to *NOT* treat them as mpints here. - // Neither RFC4253 nor RFC8709 mention this. - hash_string(&mut hash, &client_public_key.0); // e - hash_string(&mut hash, server_public_key.as_bytes()); // f + // 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. + // + 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 let hash = hash.finalize(); diff --git a/ssh-transport/src/packet.rs b/ssh-transport/src/packet.rs index 7b636ae..6f2550d 100644 --- a/ssh-transport/src/packet.rs +++ b/ssh-transport/src/packet.rs @@ -9,7 +9,7 @@ use crate::Result; pub(crate) struct PacketTransport { state: PacketTransportState, packets: VecDeque, - next_recv_seq_nr: u32, + next_recv_seq_nr: u64, } enum PacketTransportState { @@ -62,11 +62,17 @@ impl PacketTransport { } } 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)?; + let mut len = [0_u8; 4]; + let Some(len_bytes) = bytes.get(0..4) else { + return Err(client_error!( + "packet too short, not enough bytes for length" + )); + }; + len.copy_from_slice(len_bytes); + session.decrypt_len(&mut len, self.next_recv_seq_nr); + let len = u32::from_be_bytes(len); + dbg!(len); + // TODO: dont assume we get it all as one.... AAaAAA } }