From 075d9ffff30719be8568315f0022a58f14a5516e Mon Sep 17 00:00:00 2001 From: "Jan Winkelmann (keks)" Date: Thu, 27 Feb 2025 16:56:05 +0100 Subject: [PATCH] update libcrux chachapoly to use libcrux-chacha20poly1305 --- Cargo.lock | 42 +++ Cargo.toml | 1 + ciphers/Cargo.toml | 8 +- ciphers/src/lib.rs | 2 +- .../subtle/libcrux/chacha20poly1305_ietf.rs | 354 +++++++++++++----- 5 files changed, 303 insertions(+), 104 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb7fc0d..2fdedeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1187,6 +1187,17 @@ dependencies = [ "rand", ] +[[package]] +name = "libcrux-chacha20poly1305" +version = "0.0.2-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04b666b490e714ff52d157da772a9a7faa3f394d619e79e64cdf1425599279ee" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", + "libcrux-poly1305", +] + [[package]] name = "libcrux-hacl" version = "0.0.2-pre.2" @@ -1197,6 +1208,25 @@ dependencies = [ "libcrux-platform", ] +[[package]] +name = "libcrux-hacl-rs" +version = "0.0.2-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb04660b91e0be4185c3a9dd8e2c317c478cc0616f9cba2a4d523e033f852fa1" +dependencies = [ + "libcrux-macros", +] + +[[package]] +name = "libcrux-macros" +version = "0.0.2-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5848af0bd1aca5be28708945867adb2b841824a0a6f48779407a41287d5ca231" +dependencies = [ + "quote", + "syn 2.0.98", +] + [[package]] name = "libcrux-platform" version = "0.0.2-pre.2" @@ -1206,6 +1236,16 @@ dependencies = [ "libc", ] +[[package]] +name = "libcrux-poly1305" +version = "0.0.2-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82169ddf387e1912ebcde17979607e6a380dc758631757e7969652debe4dde9b" +dependencies = [ + "libcrux-hacl-rs", + "libcrux-macros", +] + [[package]] name = "libfuzzer-sys" version = "0.4.9" @@ -1840,6 +1880,8 @@ dependencies = [ "blake2", "chacha20poly1305", "libcrux", + "libcrux-chacha20poly1305", + "rand", "rosenpass-cipher-traits", "rosenpass-constant-time", "rosenpass-oqs", diff --git a/Cargo.toml b/Cargo.toml index e14a00f..cc07b5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ derive_builder = "0.20.1" tokio = { version = "1.42", features = ["macros", "rt-multi-thread"] } postcard = { version = "1.1.1", features = ["alloc"] } libcrux = { version = "0.0.2-pre.2" } +libcrux-chacha20poly1305 = { version = "0.0.2-beta.3" } hex-literal = { version = "0.4.1" } hex = { version = "0.4.3" } heck = { version = "0.5.0" } diff --git a/ciphers/Cargo.toml b/ciphers/Cargo.toml index d794f37..b259d10 100644 --- a/ciphers/Cargo.toml +++ b/ciphers/Cargo.toml @@ -10,7 +10,10 @@ repository = "https://github.com/rosenpass/rosenpass" readme = "readme.md" [features] -experiment_libcrux = ["dep:libcrux"] +experiment_libcrux = [ + "dep:libcrux", + "dep:libcrux-chacha20poly1305", +] [dependencies] anyhow = { workspace = true } @@ -26,3 +29,6 @@ chacha20poly1305 = { workspace = true } blake2 = { workspace = true } libcrux = { workspace = true, optional = true } sha3 = {workspace = true} +libcrux-chacha20poly1305 = { workspace = true, optional = true } +[dev-dependencies] +rand = { workspace = true } diff --git a/ciphers/src/lib.rs b/ciphers/src/lib.rs index a6a63c8..434c7d6 100644 --- a/ciphers/src/lib.rs +++ b/ciphers/src/lib.rs @@ -19,7 +19,7 @@ pub use crate::subtle::keyed_hash::KeyedHash; /// Authenticated encryption with associated data (AEAD) /// Chacha20poly1305 is used. #[cfg(feature = "experiment_libcrux")] -pub use subtle::libcrux::chacha20poly1305_ietf::Chacha20poly1305 as Aead; +pub use subtle::libcrux::chacha20poly1305_ietf::ChaCha20Poly1305 as Aead; /// Authenticated encryption with associated data (AEAD) /// Chacha20poly1305 is used. diff --git a/ciphers/src/subtle/libcrux/chacha20poly1305_ietf.rs b/ciphers/src/subtle/libcrux/chacha20poly1305_ietf.rs index 08082dd..b266a55 100644 --- a/ciphers/src/subtle/libcrux/chacha20poly1305_ietf.rs +++ b/ciphers/src/subtle/libcrux/chacha20poly1305_ietf.rs @@ -1,8 +1,5 @@ -use std::fmt::format; -use rosenpass_to::ops::copy_slice; -use rosenpass_to::To; - -use zeroize::Zeroize; +use rosenpass_cipher_traits::algorithms::AeadChaCha20Poly1305; +use rosenpass_cipher_traits::primitives::{Aead, AeadError}; /// The key length is 32 bytes or 256 bits. pub const KEY_LEN: usize = 32; // Grrrr! Libcrux, please provide me these constants. @@ -11,114 +8,267 @@ pub const TAG_LEN: usize = 16; /// The nonce length is 12 bytes or 96 bits. pub const NONCE_LEN: usize = 12; -/// Encrypts using ChaCha20Poly1305 as implemented in [libcrux](https://github.com/cryspen/libcrux). -/// Key and nonce MUST be chosen (pseudo-)randomly. The `key` slice MUST have a length of -/// [KEY_LEN]. The `nonce` slice MUST have a length of [NONCE_LEN]. The last [TAG_LEN] bytes -/// written in `ciphertext` are the tag guaranteeing integrity. `ciphertext` MUST have a capacity of -/// `plaintext.len()` + [TAG_LEN]. -/// -/// # Examples -///```rust -/// # use rosenpass_ciphers::subtle::chacha20poly1305_ietf_libcrux::{encrypt, TAG_LEN, KEY_LEN, NONCE_LEN}; -/// -/// const PLAINTEXT_LEN: usize = 43; -/// let plaintext = "post-quantum cryptography is very important".as_bytes(); -/// assert_eq!(PLAINTEXT_LEN, plaintext.len()); -/// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY -/// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE -/// let additional_data: &[u8] = "the encrypted message is very important".as_bytes(); -/// let mut ciphertext_buffer = [0u8; PLAINTEXT_LEN + TAG_LEN]; -/// -/// let res: anyhow::Result<()> = encrypt(&mut ciphertext_buffer, key, nonce, additional_data, plaintext); -/// assert!(res.is_ok()); -/// # let expected_ciphertext: &[u8] = &[239, 104, 148, 202, 120, 32, 77, 27, 246, 206, 226, 17, -/// # 83, 78, 122, 116, 187, 123, 70, 199, 58, 130, 21, 1, 107, 230, 58, 77, 18, 152, 31, 159, 80, -/// # 151, 72, 27, 236, 137, 60, 55, 180, 31, 71, 97, 199, 12, 60, 155, 70, 221, 225, 110, 132, 191, -/// # 8, 114, 85, 4, 25]; -/// # assert_eq!(expected_ciphertext, &ciphertext_buffer); -///``` -/// -#[inline] -pub fn encrypt( - ciphertext: &mut [u8], - key: &[u8], - nonce: &[u8], - ad: &[u8], - plaintext: &[u8], -) -> anyhow::Result<()> { - let (ciphertext, mac) = ciphertext.split_at_mut(ciphertext.len() - TAG_LEN); +/// An implementation of the ChaCha20Poly1305 AEAD from libcrux +pub struct ChaCha20Poly1305; - use libcrux::aead as C; - let crux_key = C::Key::Chacha20Poly1305(C::Chacha20Key(key.try_into().unwrap())); - let crux_iv = C::Iv(nonce.try_into().unwrap()); +impl Aead for ChaCha20Poly1305 { + fn encrypt( + &self, + ciphertext: &mut [u8], + key: &[u8; KEY_LEN], + nonce: &[u8; NONCE_LEN], + ad: &[u8], + plaintext: &[u8], + ) -> Result<(), AeadError> { + let (ctxt, tag) = libcrux_chacha20poly1305::encrypt(key, plaintext, ciphertext, ad, nonce) + .map_err(|_| AeadError::InternalError)?; - copy_slice(plaintext).to(ciphertext); - let crux_tag = libcrux::aead::encrypt(&crux_key, ciphertext, crux_iv, ad).unwrap(); - copy_slice(crux_tag.as_ref()).to(mac); + // return an error of the destination buffer is longer than expected + // because the caller wouldn't know where the end is + if ctxt.len() + tag.len() != ciphertext.len() { + return Err(AeadError::InternalError); + } - match crux_key { - C::Key::Chacha20Poly1305(mut k) => k.0.zeroize(), - _ => panic!(), + Ok(()) } - Ok(()) + fn decrypt( + &self, + plaintext: &mut [u8], + key: &[u8; KEY_LEN], + nonce: &[u8; NONCE_LEN], + ad: &[u8], + ciphertext: &[u8], + ) -> Result<(), AeadError> { + let ptxt = libcrux_chacha20poly1305::decrypt(key, plaintext, ciphertext, ad, nonce) + .map_err(|_| AeadError::DecryptError)?; + + // return an error of the destination buffer is longer than expected + // because the caller wouldn't know where the end is + if ptxt.len() != plaintext.len() { + return Err(AeadError::DecryptError); + } + + Ok(()) + } } -/// Decrypts a `ciphertext` and verifies the integrity of the `ciphertext` and the additional data -/// `ad`. using ChaCha20Poly1305 as implemented in [libcrux](https://github.com/cryspen/libcrux). -/// -/// The `key` slice MUST have a length of [KEY_LEN]. The `nonce` slice MUST have a length of -/// [NONCE_LEN]. The plaintext buffer must have a capacity of `ciphertext.len()` - [TAG_LEN]. -/// -/// # Examples -///```rust -/// # use rosenpass_ciphers::subtle::chacha20poly1305_ietf_libcrux::{decrypt, TAG_LEN, KEY_LEN, NONCE_LEN}; -/// let ciphertext: &[u8] = &[239, 104, 148, 202, 120, 32, 77, 27, 246, 206, 226, 17, -/// 83, 78, 122, 116, 187, 123, 70, 199, 58, 130, 21, 1, 107, 230, 58, 77, 18, 152, 31, 159, 80, -/// 151, 72, 27, 236, 137, 60, 55, 180, 31, 71, 97, 199, 12, 60, 155, 70, 221, 225, 110, 132, 191, -/// 8, 114, 85, 4, 25]; // this is the ciphertext generated by the example for the encryption -/// const PLAINTEXT_LEN: usize = 43; -/// assert_eq!(PLAINTEXT_LEN + TAG_LEN, ciphertext.len()); -/// -/// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY -/// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE -/// let additional_data: &[u8] = "the encrypted message is very important".as_bytes(); -/// let mut plaintext_buffer = [0u8; PLAINTEXT_LEN]; -/// -/// let res: anyhow::Result<()> = decrypt(&mut plaintext_buffer, key, nonce, additional_data, ciphertext); -/// assert!(res.is_ok()); -/// let expected_plaintext = "post-quantum cryptography is very important".as_bytes(); -/// assert_eq!(expected_plaintext, plaintext_buffer); -/// -///``` -#[inline] -pub fn decrypt( - plaintext: &mut [u8], - key: &[u8], - nonce: &[u8], - ad: &[u8], - ciphertext: &[u8], -) -> anyhow::Result<()> { - let (ciphertext, mac) = ciphertext.split_at(ciphertext.len() - TAG_LEN); +impl AeadChaCha20Poly1305 for ChaCha20Poly1305 {} - use libcrux::aead as C; - let crux_key = C::Key::Chacha20Poly1305(C::Chacha20Key(key.try_into()?)); - let crux_iv = C::Iv(nonce.try_into()?); - let crux_tag = match C::Tag::from_slice(mac) { - Ok(tag) => tag, - Err(err) => return Err(anyhow::anyhow!(format!("{:?}", err))), - }; +/// The idea of these tests is to check that the above implemenatation behaves, by and large, the +/// same as the one from the old libcrux and the one from RustCrypto. You can consider them janky, +/// self-rolled property-based tests. +#[cfg(test)] +mod equivalence_tests { + use super::*; + use rand::RngCore; - copy_slice(ciphertext).to(plaintext); - let dec_res = libcrux::aead::decrypt(&crux_key, plaintext, crux_iv, ad, &crux_tag); - if dec_res.is_err() { - return Err(anyhow::anyhow!("Decryption failed {:?}", dec_res.err())); + #[test] + fn fuzz_equivalence_libcrux_old_new() { + let ptxts: [&[u8]; 3] = [ + b"".as_slice(), + b"test".as_slice(), + b"abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd", + ]; + let mut key = [0; KEY_LEN]; + let mut rng = rand::thread_rng(); + + let mut ctxt_left = [0; 64 + TAG_LEN]; + let mut ctxt_right = [0; 64 + TAG_LEN]; + + let mut ptxt_left = [0; 64]; + let mut ptxt_right = [0; 64]; + + let nonce = [0; NONCE_LEN]; + let ad = b""; + + for ptxt in ptxts { + for _ in 0..1000 { + rng.fill_bytes(&mut key); + let ctxt_left = &mut ctxt_left[..ptxt.len() + TAG_LEN]; + let ctxt_right = &mut ctxt_right[..ptxt.len() + TAG_LEN]; + + let ptxt_left = &mut ptxt_left[..ptxt.len()]; + let ptxt_right = &mut ptxt_right[..ptxt.len()]; + + encrypt(ctxt_left, &key, &nonce, ad, ptxt).unwrap(); + ChaCha20Poly1305 + .encrypt(ctxt_right, &key, &nonce, ad, ptxt) + .unwrap(); + + assert_eq!(ctxt_left, ctxt_right); + + decrypt(ptxt_left, &key, &nonce, ad, ctxt_left).unwrap(); + ChaCha20Poly1305 + .decrypt(ptxt_right, &key, &nonce, ad, ctxt_right) + .unwrap(); + + assert_eq!(ptxt_left, ptxt); + assert_eq!(ptxt_right, ptxt); + } + } } - match crux_key { - C::Key::Chacha20Poly1305(mut k) => k.0.zeroize(), - _ => panic!(), + #[test] + fn fuzz_equivalence_libcrux_rustcrypto() { + use crate::subtle::rust_crypto::chacha20poly1305_ietf::ChaCha20Poly1305 as RustCryptoChaCha20Poly1305; + let ptxts: [&[u8]; 3] = [ + b"".as_slice(), + b"test".as_slice(), + b"abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd", + ]; + let mut key = [0; KEY_LEN]; + let mut rng = rand::thread_rng(); + + let mut ctxt_left = [0; 64 + TAG_LEN]; + let mut ctxt_right = [0; 64 + TAG_LEN]; + + let mut ptxt_left = [0; 64]; + let mut ptxt_right = [0; 64]; + + let nonce = [0; NONCE_LEN]; + let ad = b""; + + for ptxt in ptxts { + for _ in 0..1000 { + rng.fill_bytes(&mut key); + let ctxt_left = &mut ctxt_left[..ptxt.len() + TAG_LEN]; + let ctxt_right = &mut ctxt_right[..ptxt.len() + TAG_LEN]; + + let ptxt_left = &mut ptxt_left[..ptxt.len()]; + let ptxt_right = &mut ptxt_right[..ptxt.len()]; + + RustCryptoChaCha20Poly1305 + .encrypt(ctxt_left, &key, &nonce, ad, ptxt) + .unwrap(); + ChaCha20Poly1305 + .encrypt(ctxt_right, &key, &nonce, ad, ptxt) + .unwrap(); + + assert_eq!(ctxt_left, ctxt_right); + + RustCryptoChaCha20Poly1305 + .decrypt(ptxt_left, &key, &nonce, ad, ctxt_left) + .unwrap(); + ChaCha20Poly1305 + .decrypt(ptxt_right, &key, &nonce, ad, ctxt_right) + .unwrap(); + + assert_eq!(ptxt_left, ptxt); + assert_eq!(ptxt_right, ptxt); + } + } } - Ok(()) + // The functions below are from the old libcrux backend. I am keeping them around so we can + // check if they behave the same. + use rosenpass_to::ops::copy_slice; + use rosenpass_to::To; + use zeroize::Zeroize; + + /// Encrypts using ChaCha20Poly1305 as implemented in [libcrux](https://github.com/cryspen/libcrux). + /// Key and nonce MUST be chosen (pseudo-)randomly. The `key` slice MUST have a length of + /// [KEY_LEN]. The `nonce` slice MUST have a length of [NONCE_LEN]. The last [TAG_LEN] bytes + /// written in `ciphertext` are the tag guaranteeing integrity. `ciphertext` MUST have a capacity of + /// `plaintext.len()` + [TAG_LEN]. + /// + /// # Examples + ///```rust + /// # use rosenpass_ciphers::subtle::chacha20poly1305_ietf_libcrux::{encrypt, TAG_LEN, KEY_LEN, NONCE_LEN}; + /// + /// const PLAINTEXT_LEN: usize = 43; + /// let plaintext = "post-quantum cryptography is very important".as_bytes(); + /// assert_eq!(PLAINTEXT_LEN, plaintext.len()); + /// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY + /// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE + /// let additional_data: &[u8] = "the encrypted message is very important".as_bytes(); + /// let mut ciphertext_buffer = [0u8; PLAINTEXT_LEN + TAG_LEN]; + /// + /// let res: anyhow::Result<()> = encrypt(&mut ciphertext_buffer, key, nonce, additional_data, plaintext); + /// assert!(res.is_ok()); + /// # let expected_ciphertext: &[u8] = &[239, 104, 148, 202, 120, 32, 77, 27, 246, 206, 226, 17, + /// # 83, 78, 122, 116, 187, 123, 70, 199, 58, 130, 21, 1, 107, 230, 58, 77, 18, 152, 31, 159, 80, + /// # 151, 72, 27, 236, 137, 60, 55, 180, 31, 71, 97, 199, 12, 60, 155, 70, 221, 225, 110, 132, 191, + /// # 8, 114, 85, 4, 25]; + /// # assert_eq!(expected_ciphertext, &ciphertext_buffer); + ///``` + /// + #[inline] + pub fn encrypt( + ciphertext: &mut [u8], + key: &[u8], + nonce: &[u8], + ad: &[u8], + plaintext: &[u8], + ) -> anyhow::Result<()> { + let (ciphertext, mac) = ciphertext.split_at_mut(ciphertext.len() - TAG_LEN); + + use libcrux::aead as C; + let crux_key = C::Key::Chacha20Poly1305(C::Chacha20Key(key.try_into().unwrap())); + let crux_iv = C::Iv(nonce.try_into().unwrap()); + + copy_slice(plaintext).to(ciphertext); + let crux_tag = libcrux::aead::encrypt(&crux_key, ciphertext, crux_iv, ad).unwrap(); + copy_slice(crux_tag.as_ref()).to(mac); + + match crux_key { + C::Key::Chacha20Poly1305(mut k) => k.0.zeroize(), + _ => panic!(), + } + + Ok(()) + } + + /// Decrypts a `ciphertext` and verifies the integrity of the `ciphertext` and the additional data + /// `ad`. using ChaCha20Poly1305 as implemented in [libcrux](https://github.com/cryspen/libcrux). + /// + /// The `key` slice MUST have a length of [KEY_LEN]. The `nonce` slice MUST have a length of + /// [NONCE_LEN]. The plaintext buffer must have a capacity of `ciphertext.len()` - [TAG_LEN]. + /// + /// # Examples + ///```rust + /// # use rosenpass_ciphers::subtle::chacha20poly1305_ietf_libcrux::{decrypt, TAG_LEN, KEY_LEN, NONCE_LEN}; + /// let ciphertext: &[u8] = &[239, 104, 148, 202, 120, 32, 77, 27, 246, 206, 226, 17, + /// 83, 78, 122, 116, 187, 123, 70, 199, 58, 130, 21, 1, 107, 230, 58, 77, 18, 152, 31, 159, 80, + /// 151, 72, 27, 236, 137, 60, 55, 180, 31, 71, 97, 199, 12, 60, 155, 70, 221, 225, 110, 132, 191, + /// 8, 114, 85, 4, 25]; // this is the ciphertext generated by the example for the encryption + /// const PLAINTEXT_LEN: usize = 43; + /// assert_eq!(PLAINTEXT_LEN + TAG_LEN, ciphertext.len()); + /// + /// let key: &[u8] = &[0u8; KEY_LEN]; // THIS IS NOT A SECURE KEY + /// let nonce: &[u8] = &[0u8; NONCE_LEN]; // THIS IS NOT A SECURE NONCE + /// let additional_data: &[u8] = "the encrypted message is very important".as_bytes(); + /// let mut plaintext_buffer = [0u8; PLAINTEXT_LEN]; + /// + /// let res: anyhow::Result<()> = decrypt(&mut plaintext_buffer, key, nonce, additional_data, ciphertext); + /// assert!(res.is_ok()); + /// let expected_plaintext = "post-quantum cryptography is very important".as_bytes(); + /// assert_eq!(expected_plaintext, plaintext_buffer); + /// + ///``` + #[inline] + pub fn decrypt( + plaintext: &mut [u8], + key: &[u8], + nonce: &[u8], + ad: &[u8], + ciphertext: &[u8], + ) -> anyhow::Result<()> { + let (ciphertext, mac) = ciphertext.split_at(ciphertext.len() - TAG_LEN); + + use libcrux::aead as C; + let crux_key = C::Key::Chacha20Poly1305(C::Chacha20Key(key.try_into().unwrap())); + let crux_iv = C::Iv(nonce.try_into().unwrap()); + let crux_tag = C::Tag::from_slice(mac).unwrap(); + + copy_slice(ciphertext).to(plaintext); + libcrux::aead::decrypt(&crux_key, plaintext, crux_iv, ad, &crux_tag).unwrap(); + + match crux_key { + C::Key::Chacha20Poly1305(mut k) => k.0.zeroize(), + _ => panic!(), + } + + Ok(()) + } }