diff --git a/meta/recipes-devtools/python/python3-cryptography.bb b/meta/recipes-devtools/python/python3-cryptography.bb
index f3cb755ed5..a94b79ed4c 100644
--- a/meta/recipes-devtools/python/python3-cryptography.bb
+++ b/meta/recipes-devtools/python/python3-cryptography.bb
@@ -19,6 +19,7 @@ SRC_URI += "file://0001-pyproject.toml-remove-benchmark-disable-option.patch \
             file://CVE-2026-69249.patch \
             file://check-memfree.py \
             file://run-ptest \
+            file://CVE-2026-69247.patch \
            "
 
 require ${BPN}-crates.inc
diff --git a/meta/recipes-devtools/python/python3-cryptography/CVE-2026-69247.patch b/meta/recipes-devtools/python/python3-cryptography/CVE-2026-69247.patch
new file mode 100644
index 0000000000..85b47faac5
--- /dev/null
+++ b/meta/recipes-devtools/python/python3-cryptography/CVE-2026-69247.patch
@@ -0,0 +1,435 @@
+From 53fccd93413a8d7f07d6d8999681f27b75cffa3f Mon Sep 17 00:00:00 2001
+From: Alex Gaynor <alex.gaynor@gmail.com>
+Date: Fri, 31 Jul 2026 09:24:53 -0400
+Subject: [PATCH] Don't leak how PKCS#7 encryptedKey decryption failed (#15369)
+
+* Don't leak how PKCS#7 encryptedKey decryption failed
+
+pkcs7_decrypt_der and its PEM and S/MIME variants reported invalid RSA
+PKCS#1 v1.5 padding, a recovered key of the wrong length, and a wrong
+key as three distinct errors, and the middle one disclosed the exact
+recovered length. That is a Bleichenbacher oracle for any caller that
+decrypts untrusted messages. The wrong-length case also returned before
+the AES-CBC pass, so the same distinction was observable by timing,
+proportional to a content size the attacker chooses.
+
+Substitute a random key of the expected length on failure, per RFC 3218,
+and continue down an identical path so that every outcome looks alike.
+
+EnvelopedData still authenticates nothing, so revealing whether
+decryption succeeded remains a CBC padding oracle regardless. That is a
+property of PKCS#7 rather than of this implementation, and is now
+documented instead.
+
+Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
+
+* Cover the non-ValueError path out of encryptedKey decryption
+
+Only a ValueError from unwrapping the key is attacker controlled and so
+deliberately silenced. A private key implemented outside of this library
+can fail in other ways, and those still reach the caller -- but nothing
+exercised that arm, leaving it uncovered.
+
+Add a DummyRSAPrivateKey double whose decrypt() raises a TypeError, and
+tests for each of its methods.
+
+Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
+
+* fix?
+
+* error message
+
+---------
+
+Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
+
+CVE: CVE-2026-69247
+Upstream-Status: Backport [https://github.com/pyca/cryptography/commit/53fccd93413a8d7f07d6d8999681f27b75cffa3f]
+
+Backport Changes:
+- Dropped the CHANGELOG.rst update because it targets the 50.0.0 release.
+- Adapted DummyRSAPrivateKey to the 46.0.7 RSA abstract API, which lacks
+  NoDigestInfo and __deepcopy__.
+- Added only the RSA tests from tests/test_doubles.py because the older
+  source does not contain the unrelated Ed25519 test double.
+
+(cherry picked from commit 53fccd93413a8d7f07d6d8999681f27b75cffa3f)
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ .../primitives/asymmetric/serialization.rst   | 21 +++++
+ docs/spelling_wordlist.txt                    |  1 +
+ src/rust/src/pkcs7.rs                         | 81 +++++++++++--------
+ tests/doubles.py                              | 46 ++++++++++-
+ tests/hazmat/primitives/test_pkcs7.py         | 62 ++++++++++++++
+ tests/test_doubles.py                         | 53 ++++++++++++
+ 6 files changed, 229 insertions(+), 35 deletions(-)
+ create mode 100644 tests/test_doubles.py
+
+diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst
+index fb49c7d14..8364131da 100644
+--- a/docs/hazmat/primitives/asymmetric/serialization.rst
++++ b/docs/hazmat/primitives/asymmetric/serialization.rst
+@@ -1436,6 +1436,15 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``,
+     Deserialize and decrypt a DER-encoded PKCS7 message. PKCS7 (or S/MIME) has multiple versions,
+     but this supports a subset of :rfc:`5751`, also known as S/MIME Version 3.2.
+ 
++    .. warning::
++
++        ``EnvelopedData`` does not authenticate its content. An application
++        that decrypts attacker-supplied messages and reveals whether
++        decryption succeeded -- through an error, a status code, or timing --
++        gives the attacker a CBC padding oracle sufficient to recover the
++        plaintext. This is a property of PKCS#7 and cannot be fixed here,
++        so avoid decrypting untrusted ``EnvelopedData``.
++
+     :param data: The data, encoded in DER format.
+     :type data: bytes
+ 
+@@ -1491,6 +1500,12 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``,
+     Deserialize and decrypt a PEM-encoded PKCS7E message. PKCS7 (or S/MIME) has multiple versions,
+     but this supports a subset of :rfc:`5751`, also known as S/MIME Version 3.2.
+ 
++    .. warning::
++
++        The padding oracle described in
++        :func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_decrypt_der`
++        applies equally here.
++
+     :param data: The data, encoded in PEM format.
+     :type data: bytes
+ 
+@@ -1548,6 +1563,12 @@ contain certificates, CRLs, and much more. PKCS7 files commonly have a ``p7b``,
+     Deserialize and decrypt a S/MIME-encoded PKCS7 message. PKCS7 (or S/MIME) has multiple versions,
+     but this supports a subset of :rfc:`5751`, also known as S/MIME Version 3.2.
+ 
++    .. warning::
++
++        The padding oracle described in
++        :func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_decrypt_der`
++        applies equally here.
++
+     :param data: The data. It should be in S/MIME format, meaning MIME with content type
+         ``application/pkcs7-mime`` or ``application/x-pkcs7-mime``.
+     :type data: bytes
+diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
+index 831369fe9..956325782 100644
+--- a/docs/spelling_wordlist.txt
++++ b/docs/spelling_wordlist.txt
+@@ -151,6 +151,7 @@ Ubuntu
+ unencrypted
+ unicode
+ unpadded
++untrusted
+ unpadding
+ validator
+ Ventura
+diff --git a/src/rust/src/pkcs7.rs b/src/rust/src/pkcs7.rs
+index 3fb5abf15..dd64c3aff 100644
+--- a/src/rust/src/pkcs7.rs
++++ b/src/rust/src/pkcs7.rs
+@@ -266,27 +266,6 @@ fn decrypt_der<'p>(
+                 }
+             };
+ 
+-            // Raise error when the key encryption algorithm is not RSA
+-            let key = match recipient_info.key_encryption_algorithm.oid() {
+-                &oid::RSA_OID => {
+-                    let padding = types::PKCS1V15.get(py)?.call0()?;
+-                    private_key
+-                        .call_method1(
+-                            pyo3::intern!(py, "decrypt"),
+-                            (recipient_info.encrypted_key, &padding),
+-                        )?
+-                        .extract::<pyo3::pybacked::PyBackedBytes>()?
+-                }
+-                _ => {
+-                    return Err(CryptographyError::from(
+-                        exceptions::UnsupportedAlgorithm::new_err((
+-                            "Only RSA with PKCS #1 v1.5 padding is currently supported for key decryption.",
+-                            exceptions::Reasons::UNSUPPORTED_SERIALIZATION,
+-                        )),
+-                    ));
+-                }
+-            };
+-
+             // The function can decrypt content encrypted with AES-128-CBC, which the S/MIME v3.2
+             // RFC specifies as MUST support, and AES-256-CBC, which is specified as SHOULD+
+             // support. More info: https://datatracker.ietf.org/doc/html/rfc5751#section-2.7
+@@ -294,19 +273,9 @@ fn decrypt_der<'p>(
+             let algorithm_identifier = enveloped_data
+                 .encrypted_content_info
+                 .content_encryption_algorithm;
+-            let (algorithm, mode) = match algorithm_identifier.params {
+-                AlgorithmParameters::Aes128Cbc(iv) => (
+-                    types::AES128.get(py)?.call1((key,))?,
+-                    types::CBC
+-                        .get(py)?
+-                        .call1((pyo3::types::PyBytes::new(py, &iv),))?,
+-                ),
+-                AlgorithmParameters::Aes256Cbc(iv) => (
+-                    types::AES256.get(py)?.call1((key,))?,
+-                    types::CBC
+-                        .get(py)?
+-                        .call1((pyo3::types::PyBytes::new(py, &iv),))?,
+-                ),
++            let (algorithm_type, key_size, iv) = match algorithm_identifier.params {
++                AlgorithmParameters::Aes128Cbc(iv) => (&types::AES128, 16, iv),
++                AlgorithmParameters::Aes256Cbc(iv) => (&types::AES256, 32, iv),
+                 _ => {
+                     return Err(CryptographyError::from(
+                         exceptions::UnsupportedAlgorithm::new_err((
+@@ -317,6 +286,50 @@ fn decrypt_der<'p>(
+                 }
+             };
+ 
++            // Raise error when the key encryption algorithm is not RSA
++            if recipient_info.key_encryption_algorithm.oid() != &oid::RSA_OID {
++                return Err(CryptographyError::from(
++                    exceptions::UnsupportedAlgorithm::new_err((
++                        "Only RSA with PKCS #1 v1.5 padding is currently supported for key decryption.",
++                        exceptions::Reasons::UNSUPPORTED_SERIALIZATION,
++                    )),
++                ));
++            }
++
++            // Unwrap the content encryption key. RFC 3218 requires that a
++            // failure to recover a usable key is not distinguishable from
++            // recovering a key that simply isn't the right one -- otherwise
++            // this becomes a Bleichenbacher oracle for anything that decrypts
++            // attacker-supplied data. We substitute a random key of the
++            // expected size, so that every failure surfaces as the same
++            // content decryption error.
++            let padding = types::PKCS1V15.get(py)?.call0()?;
++            let random_key = crate::backend::rand::get_rand_bytes(py, key_size)?;
++            let key = match private_key.call_method1(
++                pyo3::intern!(py, "decrypt"),
++                (recipient_info.encrypted_key, &padding),
++            ) {
++                Ok(key) => {
++                    let key = key.extract::<pyo3::Bound<'_, pyo3::types::PyBytes>>()?;
++                    if key.as_bytes().len() == key_size {
++                        key
++                    } else {
++                        random_key
++                    }
++                }
++                // A ValueError here is entirely attacker controlled (invalid
++                // padding, or an encrypted key of the wrong length) and must
++                // stay silent. Any other exception reflects a caller mistake
++                // rather than the contents of the message.
++                Err(e) if e.is_instance_of::<pyo3::exceptions::PyValueError>(py) => random_key,
++                Err(e) => return Err(e.into()),
++            };
++
++            let algorithm = algorithm_type.get(py)?.call1((key,))?;
++            let mode = types::CBC
++                .get(py)?
++                .call1((pyo3::types::PyBytes::new(py, &iv),))?;
++
+             // Decrypt the content using the key and proper algorithm
+             let encrypted_content = match enveloped_data.encrypted_content_info.encrypted_content {
+                 Some(content) => content,
+diff --git a/tests/doubles.py b/tests/doubles.py
+index cf2c96a3e..25f1b4c59 100644
+--- a/tests/doubles.py
++++ b/tests/doubles.py
+@@ -2,9 +2,11 @@
+ # 2.0, and the BSD License. See the LICENSE file in the root of this repository
+ # for complete details.
+ 
++from __future__ import annotations
+ 
+ from cryptography.hazmat.primitives import hashes, serialization
+-from cryptography.hazmat.primitives.asymmetric import padding
++from cryptography.hazmat.primitives.asymmetric import padding, rsa
++from cryptography.hazmat.primitives.asymmetric import utils as asym_utils
+ from cryptography.hazmat.primitives.ciphers import (
+     BlockCipherAlgorithm,
+     CipherAlgorithm,
+@@ -53,3 +55,45 @@ class DummyKeySerializationEncryption(
+ 
+ class DummyAsymmetricPadding(padding.AsymmetricPadding):
+     name = "dummy-padding"
++
++
++class DummyRSAPrivateKey(rsa.RSAPrivateKey):
++    """
++    A fake RSAPrivateKey whose decrypt() raises a non-ValueError. Used for
++    testing that a failure originating outside of this library is not
++    mistaken for an attacker controlled decryption failure.
++    """
++
++    def decrypt(
++        self, ciphertext: bytes, padding: padding.AsymmetricPadding
++    ) -> bytes:
++        raise TypeError("TypeError for testing")
++
++    @property
++    def key_size(self) -> int:
++        raise NotImplementedError
++
++    def public_key(self) -> rsa.RSAPublicKey:
++        raise NotImplementedError
++
++    def sign(
++        self,
++        data: bytes,
++        padding: padding.AsymmetricPadding,
++        algorithm: asym_utils.Prehashed | hashes.HashAlgorithm,
++    ) -> bytes:
++        raise NotImplementedError
++
++    def private_numbers(self) -> rsa.RSAPrivateNumbers:
++        raise NotImplementedError
++
++    def private_bytes(
++        self,
++        encoding: serialization.Encoding,
++        format: serialization.PrivateFormat,
++        encryption_algorithm: serialization.KeySerializationEncryption,
++    ) -> bytes:
++        raise NotImplementedError
++
++    def __copy__(self) -> rsa.RSAPrivateKey:
++        raise NotImplementedError
+diff --git a/tests/hazmat/primitives/test_pkcs7.py b/tests/hazmat/primitives/test_pkcs7.py
+index 1496a23e1..87f2bec1a 100644
+--- a/tests/hazmat/primitives/test_pkcs7.py
++++ b/tests/hazmat/primitives/test_pkcs7.py
+@@ -18,6 +18,7 @@ from cryptography.hazmat.primitives import hashes, serialization
+ from cryptography.hazmat.primitives.asymmetric import ed25519, padding, rsa
+ from cryptography.hazmat.primitives.ciphers import algorithms
+ from cryptography.hazmat.primitives.serialization import pkcs7
++from tests.doubles import DummyRSAPrivateKey
+ from tests.x509.test_x509 import _generate_ca_and_leaf
+ 
+ from ...hazmat.primitives.fixtures_rsa import (
+@@ -1406,6 +1407,67 @@ class TestPKCS7Decrypt:
+                 email_message.as_bytes(), certificate, private_key, []
+             )
+ 
++    def test_decrypt_encrypted_key_is_not_an_oracle(
++        self, data, certificate, private_key
++    ):
++        # An attacker who replaces the encryptedKey must not be able to tell
++        # the RSA decryption outcomes apart. Recovering an unusable key, or
++        # failing to recover one at all, has to look exactly like recovering
++        # a key of the right length that simply isn't the right key. See
++        # RFC 3218 and Bleichenbacher's '98 attack.
++        enveloped = (
++            pkcs7.PKCS7EnvelopeBuilder()
++            .set_data(data)
++            .add_recipient(certificate)
++        )
++        der = enveloped.encrypt(serialization.Encoding.DER, [])
++
++        public_key = certificate.public_key()
++        assert isinstance(public_key, rsa.RSAPublicKey)
++
++        # The encryptedKey is the only OCTET STRING in the structure as long
++        # as the RSA modulus, so it can be located by its DER header.
++        key_bytes = public_key.key_size // 8
++        marker = b"\x04\x82" + key_bytes.to_bytes(2, "big")
++        assert der.count(marker) == 1
++        offset = der.index(marker) + len(marker)
++
++        for encrypted_key in [
++            # Invalid PKCS#1 v1.5 padding.
++            b"\x00" * key_bytes,
++            # Valid padding concealing a key of the wrong length.
++            public_key.encrypt(b"A" * 15, padding.PKCS1v15()),
++            public_key.encrypt(b"A" * 17, padding.PKCS1v15()),
++            public_key.encrypt(b"A" * 32, padding.PKCS1v15()),
++            # Valid padding concealing a correctly sized but wrong key.
++            public_key.encrypt(b"A" * 16, padding.PKCS1v15()),
++        ]:
++            tampered = der[:offset] + encrypted_key + der[offset + key_bytes :]
++            try:
++                pkcs7.pkcs7_decrypt_der(tampered, certificate, private_key, [])
++            except ValueError as exc:
++                # Decrypting under a substituted random key leaves plausible
++                # PKCS#7 padding roughly one time in 256, in which case the
++                # caller gets garbage instead of an error. That is equally
++                # uninformative, so it is an acceptable outcome here.
++                assert str(exc) == "Invalid padding bytes."
++
++    def test_decrypt_key_error_is_not_swallowed(self, data, certificate):
++        # Only a ValueError from unwrapping the key is attacker controlled and
++        # therefore silenced. A private key implemented outside this library
++        # can fail in other ways, and those must still reach the caller.
++        enveloped = (
++            pkcs7.PKCS7EnvelopeBuilder()
++            .set_data(data)
++            .add_recipient(certificate)
++            .encrypt(serialization.Encoding.DER, [])
++        )
++
++        with pytest.raises(TypeError, match="TypeError for testing"):
++            pkcs7.pkcs7_decrypt_der(
++                enveloped, certificate, DummyRSAPrivateKey(), []
++            )
++
+ 
+ @pytest.mark.supported(
+     only_if=lambda backend: backend.pkcs7_supported(),
+diff --git a/tests/test_doubles.py b/tests/test_doubles.py
+new file mode 100644
+index 000000000..f52bd3bbf
+--- /dev/null
++++ b/tests/test_doubles.py
+@@ -0,0 +1,53 @@
++# This file is dual licensed under the terms of the Apache License, Version
++# 2.0, and the BSD License. See the LICENSE file in the root of this repository
++# for complete details.
++
++import copy
++
++import pytest
++
++from cryptography.hazmat.primitives import hashes, serialization
++from cryptography.hazmat.primitives.asymmetric import padding
++
++from .doubles import DummyRSAPrivateKey
++
++
++class TestDummyRSAPrivateKey:
++    def test_decrypt(self):
++        key = DummyRSAPrivateKey()
++        with pytest.raises(TypeError):
++            key.decrypt(b"ciphertext", padding.PKCS1v15())
++
++    def test_key_size_not_implemented(self):
++        key = DummyRSAPrivateKey()
++        with pytest.raises(NotImplementedError):
++            key.key_size
++
++    def test_public_key_not_implemented(self):
++        key = DummyRSAPrivateKey()
++        with pytest.raises(NotImplementedError):
++            key.public_key()
++
++    def test_sign_not_implemented(self):
++        key = DummyRSAPrivateKey()
++        with pytest.raises(NotImplementedError):
++            key.sign(b"data", padding.PKCS1v15(), hashes.SHA256())
++
++    def test_private_numbers_not_implemented(self):
++        key = DummyRSAPrivateKey()
++        with pytest.raises(NotImplementedError):
++            key.private_numbers()
++
++    def test_private_bytes_not_implemented(self):
++        key = DummyRSAPrivateKey()
++        with pytest.raises(NotImplementedError):
++            key.private_bytes(
++                serialization.Encoding.PEM,
++                serialization.PrivateFormat.PKCS8,
++                serialization.NoEncryption(),
++            )
++
++    def test_copy_not_implemented(self):
++        key = DummyRSAPrivateKey()
++        with pytest.raises(NotImplementedError):
++            copy.copy(key)
+-- 
+2.35.6
