diff --git a/boring/src/aead.rs b/boring/src/aead.rs index 5340087b2..c17890480 100644 --- a/boring/src/aead.rs +++ b/boring/src/aead.rs @@ -36,22 +36,33 @@ //! following safe nonce practices for the selected algorithm. //! [`Algorithm::nonce_len`] returns the required nonce size in bytes. //! +//! # Mutability +//! +//! All seal and open methods require `&mut self`. While the generic AEADs +//! (AES-GCM, ChaCha20-Poly1305, etc.) are stateless after initialization, +//! the TLS-specific AEADs are **stateful** and internally mutate the context +//! during seal operations. Using `&mut self` universally prevents LLVM from +//! incorrectly optimizing away state changes — an issue that has caused +//! real-world cryptographic failures (see [quiche#2383]). +//! +//! [quiche#2383]: https://github.com/cloudflare/quiche/pull/2383 +//! //! # Example //! //! ``` //! use boring::aead::{AeadCtx, Algorithm}; //! //! let algorithm = Algorithm::aes_128_gcm(); -//! let ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap(); +//! let mut ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap(); //! let nonce = [0u8; 12]; //! let aad = b"record-header"; //! let mut payload = b"hello world".to_vec(); //! let mut tag = vec![0u8; algorithm.max_overhead()]; //! -//! ctx.seal_in_place(&nonce, payload.as_mut_slice(), &mut tag, aad) +//! ctx.seal_in_place_mut(&nonce, payload.as_mut_slice(), &mut tag, aad) //! .unwrap(); //! -//! ctx.open_in_place(&nonce, payload.as_mut_slice(), &tag, aad) +//! ctx.open_in_place_mut(&nonce, payload.as_mut_slice(), &tag, aad) //! .unwrap(); //! //! assert_eq!(payload.as_slice(), b"hello world"); @@ -110,6 +121,70 @@ impl Algorithm { unsafe { Self(ffi::EVP_aead_xchacha20_poly1305()) } } + /// AES-128-GCM with TLS 1.2 nonce construction. + /// + /// Seal operations fail if nonces do not match the TLS 1.2 nonce format. + /// Produces the same ciphertext as [`aes_128_gcm`](Self::aes_128_gcm) but + /// adds an AEAD-level nonce check. + /// + /// # Warning + /// + /// Contexts created with this algorithm are **stateful**. + /// `seal` operations must not be called concurrently on the same context. + #[corresponds(EVP_aead_aes_128_gcm_tls12)] + #[must_use] + pub fn aes_128_gcm_tls12() -> Self { + unsafe { Self(ffi::EVP_aead_aes_128_gcm_tls12()) } + } + + /// AES-256-GCM with TLS 1.2 nonce construction. + /// + /// Seal operations fail if nonces do not match the TLS 1.2 nonce format. + /// Produces the same ciphertext as [`aes_256_gcm`](Self::aes_256_gcm) but + /// adds an AEAD-level nonce check. + /// + /// # Warning + /// + /// Contexts created with this algorithm are **stateful**. + /// `seal` operations must not be called concurrently on the same context. + #[corresponds(EVP_aead_aes_256_gcm_tls12)] + #[must_use] + pub fn aes_256_gcm_tls12() -> Self { + unsafe { Self(ffi::EVP_aead_aes_256_gcm_tls12()) } + } + + /// AES-128-GCM with TLS 1.3 nonce construction. + /// + /// Seal operations fail if nonces do not match the TLS 1.3 nonce format. + /// Produces the same ciphertext as [`aes_128_gcm`](Self::aes_128_gcm) but + /// adds an AEAD-level nonce check. + /// + /// # Warning + /// + /// Contexts created with this algorithm are **stateful**. + /// `seal` operations must not be called concurrently on the same context. + #[corresponds(EVP_aead_aes_128_gcm_tls13)] + #[must_use] + pub fn aes_128_gcm_tls13() -> Self { + unsafe { Self(ffi::EVP_aead_aes_128_gcm_tls13()) } + } + + /// AES-256-GCM with TLS 1.3 nonce construction. + /// + /// Seal operations fail if nonces do not match the TLS 1.3 nonce format. + /// Produces the same ciphertext as [`aes_256_gcm`](Self::aes_256_gcm) but + /// adds an AEAD-level nonce check. + /// + /// # Warning + /// + /// Contexts created with this algorithm are **stateful**. + /// `seal` operations must not be called concurrently on the same context. + #[corresponds(EVP_aead_aes_256_gcm_tls13)] + #[must_use] + pub fn aes_256_gcm_tls13() -> Self { + unsafe { Self(ffi::EVP_aead_aes_256_gcm_tls13()) } + } + /// Returns the key length, in bytes, required by this algorithm. #[corresponds(EVP_AEAD_key_length)] #[allow(clippy::trivially_copy_pass_by_ref)] @@ -255,7 +330,7 @@ impl AeadCtxRef { /// use boring::aead::{AeadCtx, Algorithm}; /// /// let algorithm = Algorithm::chacha20_poly1305(); - /// let ctx = AeadCtx::new(&algorithm, &[7u8; 32], algorithm.max_tag_len()).unwrap(); + /// let mut ctx = AeadCtx::new(&algorithm, &[7u8; 32], algorithm.max_tag_len()).unwrap(); /// /// let nonce = [1u8; 12]; /// let aad = b"frame-header"; @@ -267,7 +342,7 @@ impl AeadCtxRef { /// let mut detached = vec![0u8; extra.len() + algorithm.max_overhead()]; /// /// let detached_written = ctx - /// .seal_scatter( + /// .seal_scatter_mut( /// &nonce, /// main.as_mut_slice(), /// detached.as_mut_slice(), @@ -285,12 +360,28 @@ impl AeadCtxRef { /// full_ciphertext.extend_from_slice(&detached_written[..extra_ct_len]); /// /// // `open_gather` takes ciphertext and detached tag separately. - /// ctx.open_gather(&nonce, full_ciphertext.as_mut_slice(), tag, aad) + /// ctx.open_gather_mut(&nonce, full_ciphertext.as_mut_slice(), tag, aad) /// .unwrap(); /// /// assert_eq!(full_ciphertext.as_slice(), b"hello world"); /// ``` #[corresponds(EVP_AEAD_CTX_seal_scatter)] + pub fn seal_scatter_mut<'a>( + &mut self, + nonce: &[u8], + in_out: &mut [u8], + out_tag: &'a mut [u8], + extra_in: Option<&[u8]>, + associated_data: &[u8], + ) -> Result<&'a mut [u8], ErrorStack> { + #[allow(deprecated)] + self.seal_scatter(nonce, in_out, out_tag, extra_in, associated_data) + } + + #[doc(hidden)] + #[deprecated( + note = "Non-thread-safe when used with TLS due to interior mutability in BoringSSL. Use `seal_scatter_mut` instead." + )] pub fn seal_scatter<'a>( &self, nonce: &[u8], @@ -341,6 +432,21 @@ impl AeadCtxRef { /// [`seal_scatter`](AeadCtxRef::seal_scatter). /// - `associated_data`: The same AAD that was passed during encryption. #[corresponds(EVP_AEAD_CTX_open_gather)] + pub fn open_gather_mut( + &mut self, + nonce: &[u8], + in_out: &mut [u8], + in_tag: &[u8], + associated_data: &[u8], + ) -> Result<(), ErrorStack> { + #[allow(deprecated)] + self.open_gather(nonce, in_out, in_tag, associated_data) + } + + #[doc(hidden)] + #[deprecated( + note = "Non-thread-safe when used with TLS due to interior mutability in BoringSSL. Use `open_gather_mut` instead." + )] pub fn open_gather( &self, nonce: &[u8], @@ -382,6 +488,20 @@ impl AeadCtxRef { /// authenticated but not encrypted. /// /// Returns the sub-slice of `tag` that was written to. + pub fn seal_in_place_mut<'a>( + &mut self, + nonce: &[u8], + buffer: &mut [u8], + tag: &'a mut [u8], + associated_data: &[u8], + ) -> Result<&'a mut [u8], ErrorStack> { + self.seal_scatter_mut(nonce, buffer, tag, None, associated_data) + } + + #[doc(hidden)] + #[deprecated( + note = "Non-thread-safe when used with TLS due to interior mutability in BoringSSL. Use `seal_in_place_mut` instead." + )] pub fn seal_in_place<'a>( &self, nonce: &[u8], @@ -389,6 +509,7 @@ impl AeadCtxRef { tag: &'a mut [u8], associated_data: &[u8], ) -> Result<&'a mut [u8], ErrorStack> { + #[allow(deprecated)] self.seal_scatter(nonce, buffer, tag, None, associated_data) } @@ -405,6 +526,20 @@ impl AeadCtxRef { /// - `tag`: The authentication tag produced by /// [`seal_in_place`](AeadCtxRef::seal_in_place). /// - `associated_data`: The same AAD that was passed during encryption. + pub fn open_in_place_mut( + &mut self, + nonce: &[u8], + buffer: &mut [u8], + tag: &[u8], + associated_data: &[u8], + ) -> Result<(), ErrorStack> { + self.open_gather_mut(nonce, buffer, tag, associated_data) + } + + #[doc(hidden)] + #[deprecated( + note = "Non-thread-safe when used with TLS due to interior mutability in BoringSSL. Use `open_in_place_mut` instead." + )] pub fn open_in_place( &self, nonce: &[u8], @@ -412,6 +547,7 @@ impl AeadCtxRef { tag: &[u8], associated_data: &[u8], ) -> Result<(), ErrorStack> { + #[allow(deprecated)] self.open_gather(nonce, buffer, tag, associated_data) } } @@ -423,16 +559,16 @@ mod tests { #[test] fn in_out() { let algorithm = Algorithm::aes_128_gcm(); - let ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap(); + let mut ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap(); let nonce = [0u8; 12]; let associated_data = b"this is authenticated"; let mut buffer = b"ABCDE".to_vec(); let mut tag = [0u8; 16]; - ctx.seal_in_place(&nonce, buffer.as_mut_slice(), &mut tag, associated_data) + ctx.seal_in_place_mut(&nonce, buffer.as_mut_slice(), &mut tag, associated_data) .unwrap(); - ctx.open_in_place(&nonce, buffer.as_mut_slice(), &tag, associated_data) + ctx.open_in_place_mut(&nonce, buffer.as_mut_slice(), &tag, associated_data) .unwrap(); assert_eq!(b"ABCDE", buffer.as_slice()); @@ -441,18 +577,18 @@ mod tests { #[test] fn xchacha_in_out() { let algorithm = Algorithm::xchacha20_poly1305(); - let ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 32]).unwrap(); + let mut ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 32]).unwrap(); let nonce = [0u8; 24]; let associated_data = b"xchacha"; let mut buffer = b"payload".to_vec(); let mut tag = [0u8; 16]; let tag_written = ctx - .seal_in_place(&nonce, buffer.as_mut_slice(), &mut tag, associated_data) + .seal_in_place_mut(&nonce, buffer.as_mut_slice(), &mut tag, associated_data) .unwrap(); let tag_len = tag_written.len(); - ctx.open_in_place( + ctx.open_in_place_mut( &nonce, buffer.as_mut_slice(), &tag[..tag_len], @@ -466,7 +602,7 @@ mod tests { #[test] fn seal_scatter_with_extra_in() { let algorithm = Algorithm::chacha20_poly1305(); - let ctx = AeadCtx::new(&algorithm, &[7u8; 32], algorithm.max_tag_len()).unwrap(); + let mut ctx = AeadCtx::new(&algorithm, &[7u8; 32], algorithm.max_tag_len()).unwrap(); let nonce = [1u8; 12]; let aad = b"frame-header"; @@ -475,7 +611,7 @@ mod tests { let mut detached = vec![0u8; extra.len() + algorithm.max_overhead()]; let detached_written = ctx - .seal_scatter( + .seal_scatter_mut( &nonce, main.as_mut_slice(), detached.as_mut_slice(), @@ -489,7 +625,7 @@ mod tests { let mut full_ciphertext = main; full_ciphertext.extend_from_slice(&detached_written[..extra_ct_len]); - ctx.open_gather(&nonce, full_ciphertext.as_mut_slice(), tag, aad) + ctx.open_gather_mut(&nonce, full_ciphertext.as_mut_slice(), tag, aad) .unwrap(); assert_eq!(full_ciphertext.as_slice(), b"hello world"); @@ -516,34 +652,208 @@ mod tests { // (AES-GCM accepts variable-length nonces per spec, so it is not // suitable for testing nonce-length rejection.) let algorithm = Algorithm::chacha20_poly1305(); - let ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 32]).unwrap(); + let mut ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 32]).unwrap(); let mut payload = [0u8; 8]; let mut tag = [0u8; 16]; - let result = ctx.seal_in_place(&[0u8; 11], &mut payload, &mut tag, b""); + let result = ctx.seal_in_place_mut(&[0u8; 11], &mut payload, &mut tag, b""); assert!(result.is_err()); } #[test] fn seal_rejects_insufficient_tag_buffer() { let algorithm = Algorithm::aes_128_gcm(); - let ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap(); + let mut ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap(); let mut payload = [0u8; 8]; // AES-128-GCM produces a 16-byte tag; an 8-byte buffer must be rejected. let mut short_tag = [0u8; 8]; - let result = ctx.seal_in_place(&[0u8; 12], &mut payload, &mut short_tag, b""); + let result = ctx.seal_in_place_mut(&[0u8; 12], &mut payload, &mut short_tag, b""); assert!(result.is_err()); } #[test] fn open_rejects_invalid_nonce_length() { let algorithm = Algorithm::chacha20_poly1305(); - let ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 32]).unwrap(); + let mut ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 32]).unwrap(); let mut payload = [0u8; 8]; let tag = [0u8; 16]; - let result = ctx.open_in_place(&[0u8; 11], &mut payload, &tag, b""); + let result = ctx.open_in_place_mut(&[0u8; 11], &mut payload, &tag, b""); + assert!(result.is_err()); + } + + /// Helper: seal with one context, open with another (TLS AEADs are + /// directional — the seal context tracks nonce state, so we use a + /// separate open context with the generic algorithm). + fn tls_seal_open_round_trip(algorithm: Algorithm, generic: Algorithm, key: &[u8]) { + let mut seal_ctx = AeadCtx::new_default_tag(&algorithm, key).unwrap(); + let mut open_ctx = AeadCtx::new_default_tag(&generic, key).unwrap(); + + let nonce = [0u8; 12]; + let aad = b"tls-record"; + let mut payload = b"hello TLS".to_vec(); + let original = payload.clone(); + + let mut tag = vec![0u8; algorithm.max_overhead()]; + seal_ctx + .seal_in_place_mut(&nonce, &mut payload, &mut tag, aad) + .unwrap(); + + // Ciphertext should differ from plaintext. + assert_ne!(payload.as_slice(), original.as_slice()); + + open_ctx + .open_in_place_mut(&nonce, &mut payload, &tag, aad) + .unwrap(); + + assert_eq!(payload.as_slice(), original.as_slice()); + } + + #[test] + fn aes_128_gcm_tls12_round_trip() { + tls_seal_open_round_trip( + Algorithm::aes_128_gcm_tls12(), + Algorithm::aes_128_gcm(), + &[0u8; 16], + ); + } + + #[test] + fn aes_256_gcm_tls12_round_trip() { + tls_seal_open_round_trip( + Algorithm::aes_256_gcm_tls12(), + Algorithm::aes_256_gcm(), + &[0u8; 32], + ); + } + + #[test] + fn aes_128_gcm_tls13_round_trip() { + tls_seal_open_round_trip( + Algorithm::aes_128_gcm_tls13(), + Algorithm::aes_128_gcm(), + &[0u8; 16], + ); + } + + #[test] + fn aes_256_gcm_tls13_round_trip() { + tls_seal_open_round_trip( + Algorithm::aes_256_gcm_tls13(), + Algorithm::aes_256_gcm(), + &[0u8; 32], + ); + } + + /// Build a 12-byte nonce with the given big-endian u64 counter in the + /// last 8 bytes (TLS 1.2 nonce layout: 4-byte implicit IV + 8-byte + /// explicit counter). + fn nonce_with_counter(counter: u64) -> [u8; 12] { + let mut nonce = [0u8; 12]; + nonce[4..].copy_from_slice(&counter.to_be_bytes()); + nonce + } + + #[test] + fn tls12_rejects_nonce_reuse() { + let algorithm = Algorithm::aes_128_gcm_tls12(); + let mut ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap(); + let mut payload = b"hello".to_vec(); + let mut tag = [0u8; 16]; + + // First seal with counter 0 succeeds. + ctx.seal_in_place_mut(&nonce_with_counter(0), &mut payload, &mut tag, b"") + .unwrap(); + + // Sealing again with the same counter (0) must fail — nonces must be + // strictly monotonically increasing. + let mut payload2 = b"world".to_vec(); + let result = ctx.seal_in_place_mut(&nonce_with_counter(0), &mut payload2, &mut tag, b""); + assert!(result.is_err()); + } + + #[test] + fn tls12_rejects_nonce_going_backwards() { + let algorithm = Algorithm::aes_128_gcm_tls12(); + let mut ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap(); + let mut tag = [0u8; 16]; + + // Seal with counter 5, then try counter 3. + let mut p1 = b"hello".to_vec(); + ctx.seal_in_place_mut(&nonce_with_counter(5), &mut p1, &mut tag, b"") + .unwrap(); + + let mut p2 = b"world".to_vec(); + let result = ctx.seal_in_place_mut(&nonce_with_counter(3), &mut p2, &mut tag, b""); assert!(result.is_err()); } + + #[test] + fn tls12_accepts_monotonic_nonces() { + let algorithm = Algorithm::aes_128_gcm_tls12(); + let mut ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap(); + let mut tag = [0u8; 16]; + + for counter in 0..5u64 { + let mut payload = b"test".to_vec(); + ctx.seal_in_place_mut(&nonce_with_counter(counter), &mut payload, &mut tag, b"") + .unwrap(); + } + } + + #[test] + fn tls13_rejects_nonce_reuse() { + let algorithm = Algorithm::aes_128_gcm_tls13(); + let mut ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap(); + let mut tag = [0u8; 16]; + + // TLS 1.3 nonce: sequence XOR mask. First call sets the mask. + let nonce0 = [0u8; 12]; + let mut p1 = b"hello".to_vec(); + ctx.seal_in_place_mut(&nonce0, &mut p1, &mut tag, b"") + .unwrap(); + + // Second call with the same nonce implies counter went backwards + // (counter 0 again), which must fail. + let mut p2 = b"world".to_vec(); + let result = ctx.seal_in_place_mut(&nonce0, &mut p2, &mut tag, b""); + assert!(result.is_err()); + } + + #[test] + fn generic_gcm_accepts_nonce_reuse() { + // Generic AES-GCM does NOT enforce nonce monotonicity — it is the + // caller's responsibility. Contrast with the tls12/tls13 variants. + let algorithm = Algorithm::aes_128_gcm(); + let mut ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap(); + let mut tag = [0u8; 16]; + let nonce = nonce_with_counter(0); + + let mut p1 = b"hello".to_vec(); + ctx.seal_in_place_mut(&nonce, &mut p1, &mut tag, b"") + .unwrap(); + + // Same nonce again — generic GCM accepts it (even though this is + // cryptographically unsafe, the AEAD layer does not reject it). + let mut p2 = b"world".to_vec(); + ctx.seal_in_place_mut(&nonce, &mut p2, &mut tag, b"") + .unwrap(); + } + + #[test] + fn generic_gcm_accepts_nonce_going_backwards() { + let algorithm = Algorithm::aes_128_gcm(); + let mut ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap(); + let mut tag = [0u8; 16]; + + let mut p1 = b"hello".to_vec(); + ctx.seal_in_place_mut(&nonce_with_counter(5), &mut p1, &mut tag, b"") + .unwrap(); + + // Counter 3 after 5 — generic GCM accepts it. + let mut p2 = b"world".to_vec(); + ctx.seal_in_place_mut(&nonce_with_counter(3), &mut p2, &mut tag, b"") + .unwrap(); + } }