From 520cea6ec98e1f55a724c1515418a6e8711b2144 Mon Sep 17 00:00:00 2001 From: Fraser Tweedale Date: Sat, 16 May 2026 01:22:37 +1000 Subject: [PATCH] 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 --- 14819.patch | 207 +++++++++++++++++++++++++++++++++++++++ changelog | 1 + python-cryptography.spec | 4 + 3 files changed, 212 insertions(+) create mode 100644 14819.patch diff --git a/14819.patch b/14819.patch new file mode 100644 index 0000000..fb5bd9d --- /dev/null +++ b/14819.patch @@ -0,0 +1,207 @@ +From 744e9671dc8f2cc248b89a7b03bdbfa2b0299efe Mon Sep 17 00:00:00 2001 +From: Alexander Bokovoy +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 +--- + 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)>, + } + + 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 { ++ 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)>, ++ ) -> CryptographyResult { ++ 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, +@@ -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 + diff --git a/changelog b/changelog index 8edb0ba..2d85bdc 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,6 @@ * Sat May 16 2026 Fraser Tweedale - 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 - 42.0.8-1 - Update to 42.0.8, fixes rhbz#2251816 diff --git a/python-cryptography.spec b/python-cryptography.spec index 5c40131..b5cdd12 100644 --- a/python-cryptography.spec +++ b/python-cryptography.spec @@ -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