Fix AES-GCM with FIPS provider version mismatch
Add patch from upstream PR https://github.com/pyca/cryptography/pull/14819 to fix AESGCM failing when an older FIPS provider (e.g. OpenSSL 3.0.7) is loaded alongside a newer main OpenSSL library (>= 3.2). The fix adds a fallback mechanism that re-initializes cipher contexts from stored parameters when EVP_CIPHER_CTX_copy fails, which can occur due to version mismatches between the FIPS provider and main library. Resolves: RHEL-173746
This commit is contained in:
parent
0cb0602032
commit
520cea6ec9
207
14819.patch
Normal file
207
14819.patch
Normal file
@ -0,0 +1,207 @@
|
||||
From 744e9671dc8f2cc248b89a7b03bdbfa2b0299efe Mon Sep 17 00:00:00 2001
|
||||
From: Alexander Bokovoy <abokovoy@redhat.com>
|
||||
Date: Sat, 9 May 2026 08:43:33 +0300
|
||||
Subject: [PATCH] fix: fall back to fresh context when EVP_CIPHER_CTX_copy
|
||||
fails
|
||||
|
||||
EvpCipherAead pre-initialises a cipher context with the key and
|
||||
clones it per operation via EVP_CIPHER_CTX_copy. When an older FIPS
|
||||
provider (e.g. OpenSSL 3.0.7 FIPS module) is loaded alongside a newer
|
||||
main library (>= 3.2), EVP_CIPHER_CTX_copy may fail because that
|
||||
provider version does not support copying a pre-keyed context.
|
||||
|
||||
Add a copy_fallback field to EvpCipherAead that stores the cipher type
|
||||
and key. If the per-operation copy() fails and a fallback is present,
|
||||
encrypt_into/decrypt_into re-initialise a fresh context from the stored
|
||||
parameters instead of propagating the error.
|
||||
|
||||
AesGcm on OpenSSL >= 3.2 uses new_with_copy_fallback() to populate the
|
||||
field. Other EvpCipherAead users (AESSIV, AESOCB3, AESGCMSIV,
|
||||
ChaCha20Poly1305) are unaffected: they are never used under FIPS so
|
||||
copy() never fails for them, and copy_fallback remains None.
|
||||
|
||||
AES_GCM_TAG_LEN replaces the magic 16 literals throughout the AesGcm
|
||||
methods.
|
||||
|
||||
Fixes: https://github.com/pyca/cryptography/issues/14795
|
||||
|
||||
Assisted-by: Claude Code (Sonnet 4.6)
|
||||
Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
|
||||
---
|
||||
src/rust/src/backend/aead.rs | 122 ++++++++++++++++++++++++++++++++++-
|
||||
1 files changed, 119 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/src/rust/src/backend/aead.rs b/src/rust/src/backend/aead.rs
|
||||
index 842ef5fab2d9..79fdb7b9ab5a 100644
|
||||
--- a/src/rust/src/backend/aead.rs
|
||||
+++ b/src/rust/src/backend/aead.rs
|
||||
@@ -31,9 +31,14 @@ pub(crate) struct EvpCipherAead {
|
||||
base_decryption_ctx: openssl::cipher_ctx::CipherCtx,
|
||||
tag_len: usize,
|
||||
tag_first: bool,
|
||||
+ // Cipher and key stored to reinitialise a fresh context when
|
||||
+ // EVP_CIPHER_CTX_copy fails (e.g. an older FIPS provider loaded alongside
|
||||
+ // a newer main library). None for algorithms where copy always succeeds.
|
||||
+ copy_fallback: Option<(&'static openssl::cipher::CipherRef, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl EvpCipherAead {
|
||||
+ #[cfg(not(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC)))]
|
||||
pub(crate) fn new(
|
||||
cipher: &openssl::cipher::CipherRef,
|
||||
key: &[u8],
|
||||
@@ -50,6 +55,33 @@ impl EvpCipherAead {
|
||||
base_decryption_ctx,
|
||||
tag_len,
|
||||
tag_first,
|
||||
+ copy_fallback: None,
|
||||
+ })
|
||||
+ }
|
||||
+
|
||||
+ #[cfg(any(
|
||||
+ CRYPTOGRAPHY_OPENSSL_320_OR_GREATER,
|
||||
+ CRYPTOGRAPHY_IS_BORINGSSL,
|
||||
+ CRYPTOGRAPHY_IS_LIBRESSL,
|
||||
+ CRYPTOGRAPHY_IS_AWSLC
|
||||
+ ))]
|
||||
+ pub(crate) fn new_with_copy_fallback(
|
||||
+ cipher: &'static openssl::cipher::CipherRef,
|
||||
+ key: &[u8],
|
||||
+ tag_len: usize,
|
||||
+ tag_first: bool,
|
||||
+ ) -> CryptographyResult<EvpCipherAead> {
|
||||
+ let mut base_encryption_ctx = openssl::cipher_ctx::CipherCtx::new()?;
|
||||
+ base_encryption_ctx.encrypt_init(Some(cipher), Some(key), None)?;
|
||||
+ let mut base_decryption_ctx = openssl::cipher_ctx::CipherCtx::new()?;
|
||||
+ base_decryption_ctx.decrypt_init(Some(cipher), Some(key), None)?;
|
||||
+
|
||||
+ Ok(EvpCipherAead {
|
||||
+ base_encryption_ctx,
|
||||
+ base_decryption_ctx,
|
||||
+ tag_len,
|
||||
+ tag_first,
|
||||
+ copy_fallback: Some((cipher, key.to_vec())),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -138,7 +170,13 @@ impl EvpCipherAead {
|
||||
buf: &mut [u8],
|
||||
) -> CryptographyResult<()> {
|
||||
let mut ctx = openssl::cipher_ctx::CipherCtx::new()?;
|
||||
- ctx.copy(&self.base_encryption_ctx)?;
|
||||
+ let copy_result = ctx.copy(&self.base_encryption_ctx);
|
||||
+ if copy_result.is_err() {
|
||||
+ match &self.copy_fallback {
|
||||
+ Some((cipher, key)) => ctx.encrypt_init(Some(*cipher), Some(key), None)?,
|
||||
+ None => copy_result?,
|
||||
+ }
|
||||
+ }
|
||||
|
||||
Self::encrypt_with_context(
|
||||
ctx,
|
||||
@@ -203,7 +241,13 @@ impl EvpCipherAead {
|
||||
buf: &mut [u8],
|
||||
) -> CryptographyResult<()> {
|
||||
let mut ctx = openssl::cipher_ctx::CipherCtx::new()?;
|
||||
- ctx.copy(&self.base_decryption_ctx)?;
|
||||
+ let copy_result = ctx.copy(&self.base_decryption_ctx);
|
||||
+ if copy_result.is_err() {
|
||||
+ match &self.copy_fallback {
|
||||
+ Some((cipher, key)) => ctx.decrypt_init(Some(*cipher), Some(key), None)?,
|
||||
+ None => copy_result?,
|
||||
+ }
|
||||
+ }
|
||||
|
||||
Self::decrypt_with_context(
|
||||
ctx,
|
||||
@@ -260,6 +304,73 @@ impl EvpCipherAead {
|
||||
}
|
||||
}
|
||||
|
||||
+#[cfg(test)]
|
||||
+impl EvpCipherAead {
|
||||
+ // Build an instance whose base contexts are uninitialised so that
|
||||
+ // EVP_CIPHER_CTX_copy fails with EVP_R_INPUT_NOT_INITIALIZED, forcing
|
||||
+ // the encrypt_into / decrypt_into fallback branches (lines 175-177,
|
||||
+ // 246-248) to be exercised.
|
||||
+ fn with_copy_failure(
|
||||
+ copy_fallback: Option<(&'static openssl::cipher::CipherRef, Vec<u8>)>,
|
||||
+ ) -> CryptographyResult<Self> {
|
||||
+ Ok(EvpCipherAead {
|
||||
+ base_encryption_ctx: openssl::cipher_ctx::CipherCtx::new()?,
|
||||
+ base_decryption_ctx: openssl::cipher_ctx::CipherCtx::new()?,
|
||||
+ tag_len: 16,
|
||||
+ tag_first: false,
|
||||
+ copy_fallback,
|
||||
+ })
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+#[cfg(test)]
|
||||
+mod evp_cipher_aead_tests {
|
||||
+ use super::*;
|
||||
+
|
||||
+ // Exercise the Some branch of the copy-fallback match (lines 176, 247):
|
||||
+ // copy() fails → fallback reinitialises the context → round-trip succeeds.
|
||||
+ #[test]
|
||||
+ fn test_copy_fallback_some_roundtrip() {
|
||||
+ let key = vec![0u8; 16];
|
||||
+ let aead =
|
||||
+ EvpCipherAead::with_copy_failure(Some((openssl::cipher::Cipher::aes_128_gcm(), key)))
|
||||
+ .unwrap_or_else(|_| panic!("with_copy_failure"));
|
||||
+
|
||||
+ let nonce = [0u8; 12];
|
||||
+ let plaintext = b"hello fallback";
|
||||
+ // _py is unused inside encrypt_into / decrypt_into; the GIL token is
|
||||
+ // only there for API symmetry with LazyEvpCipherAead.
|
||||
+ let py = unsafe { pyo3::Python::assume_attached() };
|
||||
+
|
||||
+ let mut enc_buf = vec![0u8; plaintext.len() + 16];
|
||||
+ aead.encrypt_into(py, plaintext, None, Some(&nonce), &mut enc_buf)
|
||||
+ .unwrap_or_else(|_| panic!("encrypt_into"));
|
||||
+
|
||||
+ let mut dec_buf = vec![0u8; plaintext.len()];
|
||||
+ aead.decrypt_into(py, &enc_buf, None, Some(&nonce), &mut dec_buf)
|
||||
+ .unwrap_or_else(|_| panic!("decrypt_into"));
|
||||
+ assert_eq!(&dec_buf, plaintext);
|
||||
+ }
|
||||
+
|
||||
+ // Exercise the None branch of the copy-fallback match (lines 177, 248):
|
||||
+ // copy() fails and there is no fallback → the error must propagate.
|
||||
+ #[test]
|
||||
+ fn test_copy_fallback_none_propagates_error() {
|
||||
+ let aead =
|
||||
+ EvpCipherAead::with_copy_failure(None).unwrap_or_else(|_| panic!("with_copy_failure"));
|
||||
+ let nonce = [0u8; 12];
|
||||
+ let py = unsafe { pyo3::Python::assume_attached() };
|
||||
+
|
||||
+ let mut buf = vec![0u8; 20];
|
||||
+ assert!(aead
|
||||
+ .encrypt_into(py, b"data", None, Some(&nonce), &mut buf)
|
||||
+ .is_err());
|
||||
+ assert!(aead
|
||||
+ .decrypt_into(py, b"ciphertext+tag__", None, Some(&nonce), &mut buf)
|
||||
+ .is_err());
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
struct LazyEvpCipherAead {
|
||||
cipher: &'static openssl::cipher::CipherRef,
|
||||
key: pyo3::Py<pyo3::PyAny>,
|
||||
@@ -676,7 +787,12 @@ impl AesGcm {
|
||||
CRYPTOGRAPHY_IS_AWSLC
|
||||
))] {
|
||||
Ok(AesGcm {
|
||||
- ctx: EvpCipherAead::new(cipher, key_buf.as_bytes(), 16, false)?,
|
||||
+ ctx: EvpCipherAead::new_with_copy_fallback(
|
||||
+ cipher,
|
||||
+ key_buf.as_bytes(),
|
||||
+ 16,
|
||||
+ false,
|
||||
+ )?,
|
||||
})
|
||||
} else {
|
||||
Ok(AesGcm {
|
||||
--
|
||||
2.47.0
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
* Sat May 16 2026 Fraser Tweedale <ftweedal@redhat.com> - 48.0.0-1
|
||||
- Update to 48.0.0, resolves RHEL-172409
|
||||
- Fix AES-GCM with FIPS provider version mismatch, resolves RHEL-173746
|
||||
|
||||
* Tue Jul 02 2024 Jeremy Cline <jeremycline@linux.microsoft.com> - 42.0.8-1
|
||||
- Update to 42.0.8, fixes rhbz#2251816
|
||||
|
||||
@ -22,6 +22,10 @@ Source2: conftest-skipper.py
|
||||
# RHEL 10 only has python3-cffi 1.16 and maturin 1.4.0, step down requirements
|
||||
Patch: stepdown-cffi-and-maturin.patch
|
||||
|
||||
# Fix AES-GCM with FIPS provider version mismatch (RHEL-173746)
|
||||
# https://github.com/pyca/cryptography/pull/14819
|
||||
Patch: 14819.patch
|
||||
|
||||
ExclusiveArch: %{rust_arches}
|
||||
|
||||
BuildRequires: openssl-devel
|
||||
|
||||
Loading…
Reference in New Issue
Block a user