From 30e70e974dc9239e63d646a79ff0321a96069f1c Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Sun, 28 Jul 2024 16:47:27 -0600 Subject: [PATCH] ssh-cipher: make ChaCha20Poly1305 accept a 256-bit key (#254) In the OpenSSH protocol, ChaCha20Poly1305 uses two different keys: one for length encryption, and one for payloads. It wasn't previously possible to use both keys (or at least quite obtuse) because `ssh-cipher` chopped up `k_1` and `k_2` and only allowed `k_1` to be used. This change hoists that into `ssh-key` for file encryption uses, giving access to the raw `ChaCha20Poly1305` implementation and allowing use of either key. In addition, a nonce is now mandatory, but use of the default all-zero nonce for file encryption purposes is now handled in `ssh-key`. --- ssh-cipher/src/chacha20poly1305.rs | 37 +++--------------------------- ssh-cipher/src/lib.rs | 2 +- ssh-key/src/kdf.rs | 33 ++++++++++++++++++++++++-- 3 files changed, 35 insertions(+), 37 deletions(-) diff --git a/ssh-cipher/src/chacha20poly1305.rs b/ssh-cipher/src/chacha20poly1305.rs index 2a4e8d79..7a75289c 100644 --- a/ssh-cipher/src/chacha20poly1305.rs +++ b/ssh-cipher/src/chacha20poly1305.rs @@ -1,8 +1,6 @@ //! OpenSSH variant of ChaCha20Poly1305: `chacha20-poly1305@openssh.com` //! //! Differences from ChaCha20Poly1305 as described in RFC8439: -//! -//! - Construction uses two separately keyed instances of ChaCha20: one for data, one for lengths //! - The input of Poly1305 is not padded //! - The lengths of ciphertext and AAD are not authenticated using Poly1305 //! @@ -14,47 +12,18 @@ use cipher::{KeyInit, KeyIvInit, StreamCipher, StreamCipherSeek}; use poly1305::Poly1305; use subtle::ConstantTimeEq; -const KEY_SIZE: usize = 32; - pub(crate) struct ChaCha20Poly1305 { cipher: ChaCha20, mac: Poly1305, } impl ChaCha20Poly1305 { - /// Create a new [`ChaCha20Poly1305`] instance with a 64-byte key. - /// From [PROTOCOL.chacha20poly1305]: - /// - /// > The chacha20-poly1305@openssh.com cipher requires 512 bits of key - /// > material as output from the SSH key exchange. This forms two 256 bit - /// > keys (K_1 and K_2), used by two separate instances of chacha20. - /// > The first 256 bits constitute K_2 and the second 256 bits become - /// > K_1. - /// > - /// > The instance keyed by K_1 is a stream cipher that is used only - /// > to encrypt the 4 byte packet length field. The second instance, - /// > keyed by K_2, is used in conjunction with poly1305 to build an AEAD - /// > (Authenticated Encryption with Associated Data) that is used to encrypt - /// > and authenticate the entire packet. + /// Create a new [`ChaCha20Poly1305`] instance with a 32-byte key. /// /// [PROTOCOL.chacha20poly1305]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD pub fn new(key: &[u8], nonce: &[u8]) -> Result { - #[allow(clippy::arithmetic_side_effects)] - if key.len() != KEY_SIZE * 2 { - return Err(Error::KeySize); - } - - // TODO(tarcieri): support for using both keys - let (k_2, _k_1) = key.split_at(KEY_SIZE); - let key = Key::try_from(k_2).map_err(|_| Error::KeySize)?; - - let nonce = if nonce.is_empty() { - // For key encryption - Nonce::default() - } else { - Nonce::try_from(nonce).map_err(|_| Error::IvSize)? - }; - + let key = Key::try_from(key).map_err(|_| Error::KeySize)?; + let nonce = Nonce::try_from(nonce).map_err(|_| Error::IvSize)?; let mut cipher = ChaCha20::new(&key, &nonce.into()); let mut poly1305_key = poly1305::Key::default(); cipher.apply_keystream(&mut poly1305_key); diff --git a/ssh-cipher/src/lib.rs b/ssh-cipher/src/lib.rs index 57d2df54..2437c03e 100644 --- a/ssh-cipher/src/lib.rs +++ b/ssh-cipher/src/lib.rs @@ -172,7 +172,7 @@ impl Cipher { Self::Aes256Ctr => Some((32, 16)), Self::Aes128Gcm => Some((16, 12)), Self::Aes256Gcm => Some((32, 12)), - Self::ChaCha20Poly1305 => Some((64, 0)), + Self::ChaCha20Poly1305 => Some((32, 12)), Self::TDesCbc => Some((24, 8)), } } diff --git a/ssh-key/src/kdf.rs b/ssh-key/src/kdf.rs index 5396510a..a7ddaa36 100644 --- a/ssh-key/src/kdf.rs +++ b/ssh-key/src/kdf.rs @@ -86,14 +86,43 @@ impl Kdf { cipher: Cipher, password: impl AsRef<[u8]>, ) -> Result<(Zeroizing>, Vec)> { - let (key_size, iv_size) = cipher.key_and_iv_size().ok_or(Error::Decrypted)?; + let (key_size, iv_size) = match cipher { + // Derive two ChaCha20Poly1305 keys, but only use the first. + // In the typical SSH protocol, the second key is used for length encryption. + // + // From `PROTOCOL.chacha20poly1305`: + // + // > The chacha20-poly1305@openssh.com cipher requires 512 bits of key + // > material as output from the SSH key exchange. This forms two 256 bit + // > keys (K_1 and K_2), used by two separate instances of chacha20. + // > The first 256 bits constitute K_2 and the second 256 bits become + // > K_1. + // > + // > The instance keyed by K_1 is a stream cipher that is used only + // > to encrypt the 4 byte packet length field. The second instance, + // > keyed by K_2, is used in conjunction with poly1305 to build an AEAD + // > (Authenticated Encryption with Associated Data) that is used to encrypt + // > and authenticate the entire packet. + Cipher::ChaCha20Poly1305 => (64, 0), + _ => cipher.key_and_iv_size().ok_or(Error::Decrypted)?, + }; + let okm_size = key_size .checked_add(iv_size) .ok_or(encoding::Error::Length)?; let mut okm = Zeroizing::new(vec![0u8; okm_size]); self.derive(password, &mut okm)?; - let iv = okm.split_off(key_size); + let mut iv = okm.split_off(key_size); + + if cipher == Cipher::ChaCha20Poly1305 { + // Only use the first ChaCha20 key. + okm.truncate(32); + + // Use an all-zero nonce (with a key derived from password + salt providing uniqueness) + iv.extend_from_slice(&cipher::Nonce::default()); + } + Ok((okm, iv)) }