From patchwork Tue Sep 1 09:27:41 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Vijay Anusuri X-Patchwork-Id: 96955 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 2C9F9C624C6 for ; Tue, 1 Sep 2026 09:28:12 +0000 (UTC) Received: from mail-pj1-f42.google.com (mail-pj1-f42.google.com [209.85.216.42]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.3813.1788254882145499514 for ; Tue, 01 Sep 2026 02:28:02 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@mvista.com header.s=google header.b=cY8AuFWe; spf=pass (domain: mvista.com, ip: 209.85.216.42, mailfrom: vanusuri@mvista.com) Received: by mail-pj1-f42.google.com with SMTP id 98e67ed59e1d1-38ec1402b05so735591a91.2 for ; Tue, 01 Sep 2026 02:28:02 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=mvista.com; s=google; t=1788254881; x=1788859681; darn=lists.openembedded.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=J3/GxAxXBBJJkdpvVXLa/5+cCjB0ys+yuGYITv1cFOk=; b=cY8AuFWeMLW0Z+R9cqjH1T/Ew/SDQ5jo1iEqIMr461EjKvckYcuVFbje9kzanfOjWR WNVdfwb9XnEFKCxXjECQl+BVu7at7MRBKrT27eRVQwstLcaI/7mvmSJ6whD04WwI8aCz AUKK8Me6l6/Cx+9CrkcSANGst98j7lvyJHRNk= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1788254881; x=1788859681; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=J3/GxAxXBBJJkdpvVXLa/5+cCjB0ys+yuGYITv1cFOk=; b=R5S3q2HE3nbKVEqnAYXUY80W/OguDqdr+NhXDeJ6FSNAqTKAHBL1D21WR6K4UR+xb+ yoQKj4qSLz0Q5AbQpttAfcdqUyznW+Z1z0NFVVXfFYWbDk4CtFVkMhDK0UAfSVyU+20M y8E8k5uXiBVk/WL57B7+SXDOkIqfPYbKEivcFxXJYoKe1yb/Lin9obnR/bQel0BcjPCX uSN9E/i6csvefdyH8sqEm2FcWXUYH8LROdRPyLKWhiBJYr0LBTWqYx2mem+AdBFMZ7JT hmhEqtntqMZYfSQxKtplhIZCRl+Alz2clM1Ll/TF0mb0jEf5cnxcmovBw/9DkO3DNBAP 7I+A== X-Gm-Message-State: AFuF++ncTSW9PlXl7dxXBIqD8RJPc9K1Ud3rEGueBa/funVXkJvxv03F vntr9rJc8v8Sp63imSsiFyDg1OWRNrE7Vt/aN7bLkoUUxTC9cNH0XmEdseJ7de5pbjHOpO1a/x9 RboNP X-Gm-Gg: AYBFou19Cq0m5xrxvJwZkRN+2ixOxMvoWlOvsOgesgR4Zhp23ZECACPDd6Rn0D+Aiml 8gZGxcnEzpuQZypvTrnIx6b1hKxeuKL02Zrair7pYAqePzSbskSfir2kuJ/3R31c5I06nv6iUwb GlpzeyXov7WDy4FFwXh8UirON2M82+xepM/ZOcCHjJLbdp3Hp6kC2dE4AIbby+eZ7+jAZuuS2h5 xzzwg1aDAXsQjq+xyOoT/lOuWtILZzdNJ24uxs0w3ODC9RVlcrj5PZZtGew/Tj074u8JLIr/qLT elLSdykuBBdP0mf1VQT9mSgulyM1P2Diwo9OBwY9oUz8bkEdsPDIoMxlJAQkl0VLQynMewwg48y giVgJbaW9Anjuba1K/pDJBz9+BZKN6qWD65Io5fuOx8ad2Z/WC0ocChYGBIKGvsq1pIMWa0fpkr q27mbbmuT2uh+xBNtB8iOFx2pqjoQRmejPMTgM8v6s0zkEw/VykGsVZ6j/NBXVFcjj X-Received: by 2002:a17:90b:582e:b0:398:9be9:ab8e with SMTP id 98e67ed59e1d1-3989be9acb8mr33898863a91.19.1788254881216; Tue, 01 Sep 2026 02:28:01 -0700 (PDT) Received: from MVIN00352.. ([2401:4900:1f29:c0f2:70c7:b40a:9f7a:1a18]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-32b4bc05416sm23271473eec.6.2026.09.01.02.27.59 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 01 Sep 2026 02:28:00 -0700 (PDT) From: Vijay Anusuri To: openembedded-core@lists.openembedded.org Cc: Vijay Anusuri Subject: [OE-core][scarthgap][patch 3/3] python3-cryptography: Fix CVE-2026-69249 Date: Tue, 1 Sep 2026 14:57:41 +0530 Message-ID: <20260901092741.34198-3-vanusuri@mvista.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260901092741.34198-1-vanusuri@mvista.com> References: <20260901092741.34198-1-vanusuri@mvista.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 01 Sep 2026 09:28:12 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/244816 Pick patch according to [2] [1] https://nvd.nist.gov/vuln/detail/cve-2026-69249 [2] https://security-tracker.debian.org/tracker/CVE-2026-69249 Signed-off-by: Vijay Anusuri --- .../python3-cryptography/CVE-2026-69249.patch | 349 ++++++++++++++++++ .../python/python3-cryptography_42.0.5.bb | 1 + 2 files changed, 350 insertions(+) create mode 100644 meta/recipes-devtools/python/python3-cryptography/CVE-2026-69249.patch diff --git a/meta/recipes-devtools/python/python3-cryptography/CVE-2026-69249.patch b/meta/recipes-devtools/python/python3-cryptography/CVE-2026-69249.patch new file mode 100644 index 0000000000..a920b4a7cc --- /dev/null +++ b/meta/recipes-devtools/python/python3-cryptography/CVE-2026-69249.patch @@ -0,0 +1,349 @@ +From 4a12cf49675a184e47f912b00b04f3a629283582 Mon Sep 17 00:00:00 2001 +From: William Woodruff +Date: Sat, 6 Jun 2026 23:30:03 -0400 +Subject: [PATCH] Add a signature validation budget during path construction + (#14960) + +* Add a signature validation budget during path construction + +This extends our existing NC budget check to include a budget +for signature validations. If a path construction exceeds the +budget by performing more than the allowed number of signature +validation steps, the entire construction fails. + +For now, our budget is 128 signature validations. This is +consistent with (higher than) Go and rustls-webpki, which +both set a limit of 100. Like Go, we attempt to make the "best" +use of our signature budget by ordering by likelihood, using +AKI/SKI match as the strongest signal of fitness. + +* Bump limbo + +* Temporary commit + +* Revert "Temporary commit" + +This reverts commit bcdb6808562a8b8f484f85d21cb201cfdb2bbbd7. + +* Fudge a coverage test into place + +* Coverage for the coverage god + +Upstream-Status: Backport [import from suse python-cryptography-42.0.4-slfo.1.1_6.1.src.rpm +Upstream commit https://github.com/pyca/cryptography/commit/4a12cf49675a184e47f912b00b04f3a629283582] +CVE: CVE-2026-69249 +Signed-off-by: Vijay Anusuri +--- + .../cryptography-x509-verification/src/lib.rs | 209 +++++++++++++++++- + .../src/policy/mod.rs | 8 +- + 2 files changed, 208 insertions(+), 9 deletions(-) + +diff --git a/src/rust/cryptography-x509-verification/src/lib.rs b/src/rust/cryptography-x509-verification/src/lib.rs +index a505349..334eed4 100644 +--- a/src/rust/cryptography-x509-verification/src/lib.rs ++++ b/src/rust/cryptography-x509-verification/src/lib.rs +@@ -15,9 +15,12 @@ use std::vec; + + use cryptography_x509::extensions::{DuplicateExtensionsError, Extensions}; + use cryptography_x509::{ +- extensions::{NameConstraints, SubjectAlternativeName}, ++ extensions::{AuthorityKeyIdentifier, NameConstraints, SubjectAlternativeName}, + name::GeneralName, +- oid::{NAME_CONSTRAINTS_OID, SUBJECT_ALTERNATIVE_NAME_OID}, ++}; ++use cryptography_x509::oid::{ ++ AUTHORITY_KEY_IDENTIFIER_OID, NAME_CONSTRAINTS_OID, SUBJECT_ALTERNATIVE_NAME_OID, ++ SUBJECT_KEY_IDENTIFIER_OID, + }; + + use types::{DNSPattern}; +@@ -40,15 +43,23 @@ pub enum ValidationError { + + struct Budget { + name_constraint_checks: usize, ++ signature_checks: usize, + } + + impl Budget { +- // Same limit as other validators ++ // The maximum number of name constraint checks performed when attempting ++ // path construction. This is the same limit as other validators. + const DEFAULT_NAME_CONSTRAINT_CHECK_LIMIT: usize = 1 << 20; + ++ // The maximum number of signature verifications performed when attempting ++ // path construction. The is similar to other validators: ++ // both Go and rustls-webpki pick 100. ++ const DEFAULT_SIGNATURE_CHECK_LIMIT: usize = 1 << 7; ++ + fn new() -> Budget { + Budget { + name_constraint_checks: Self::DEFAULT_NAME_CONSTRAINT_CHECK_LIMIT, ++ signature_checks: Self::DEFAULT_SIGNATURE_CHECK_LIMIT, + } + } + +@@ -61,6 +72,15 @@ impl Budget { + ))?; + Ok(()) + } ++ ++ fn signature_check(&mut self) -> Result<(), ValidationError> { ++ self.signature_checks = self.signature_checks.checked_sub(1).ok_or_else(|| { ++ ValidationError::FatalError( ++ "Exceeded maximum signature check limit", ++ ) ++ })?; ++ Ok(()) ++ } + } + + impl From for ValidationError { +@@ -270,18 +290,57 @@ impl<'a, 'chain, B: CryptoOps> ChainBuilder<'a, 'chain, B> { + } + } + ++ /// Identify and return potential issuers for `cert`, considering ++ /// candidates from both the trusted store and untrusted intermediate set. ++ /// Trusted candidates are returned before untrusted intermediate ++ /// candidates, and both groups are opportunisitically ordered by ++ /// "likeliness" in terms of AKI/SKI match. + fn potential_issuers( + &'a self, + cert: &'a VerificationCertificate<'chain, B>, +- ) -> impl Iterator> + '_ { +- // TODO: Optimizations: +- // * Search by AKI and other identifiers? +- self.store ++ cert_extensions: &Extensions<'chain>, ++ ) -> Vec<&'a VerificationCertificate<'chain, B>> { ++ let mut candidates: Vec<&'a VerificationCertificate<'chain, B>> = self ++ .store + .get_by_subject(&cert.certificate().tbs_cert.issuer) + .iter() + .chain(self.intermediates.iter().filter(|&candidate| { + candidate.certificate().subject() == cert.certificate().issuer() + })) ++ .collect(); ++ ++ let want_kid: Option<&[u8]> = cert_extensions ++ .get_extension(&AUTHORITY_KEY_IDENTIFIER_OID) ++ .and_then(|ext| ext.value::>().ok()) ++ .and_then(|aki| aki.key_identifier); ++ ++ // This mirrors Go's `findPotentialParents`: we have a global ++ // signature budget, so we want to bucket candidates by likeliness ++ // to avoid wasting budget on (potentially adversarial) name collisions. ++ // ++ // Observe that we use a stable sort to preserve trusted candidates ++ // before untrusted candidates in each likeliness bucket. In other ++ // words, we always try a likely trusted candidate over an equally ++ // likely untrusted one. ++ // ++ // See: ++ candidates.sort_by_key(|candidate| { ++ let have_kid: Option<&[u8]> = ++ candidate.certificate().extensions().ok().and_then(|exts| { ++ exts.get_extension(&SUBJECT_KEY_IDENTIFIER_OID) ++ .and_then(|ext| ext.value::<&[u8]>().ok()) ++ }); ++ ++ match (want_kid, have_kid) { ++ // cert AKID matches candidate SKID, highest likelihood. ++ (Some(want), Some(have)) if want == have => 0, ++ // cert AKID and candidate SKID don't match, lowest likelihood. ++ (Some(_), Some(_)) => 2, ++ // cert AKID and/or candidate SKID is not present, medium likelihood. ++ _ => 1u8, ++ } ++ }); ++ candidates + } + + fn build_chain_inner( +@@ -314,7 +373,8 @@ impl<'a, 'chain, B: CryptoOps> ChainBuilder<'a, 'chain, B> { + // Otherwise, we collect a list of potential issuers for this cert, + // and continue with the first that verifies. + let mut last_err: Option = None; +- for issuing_cert_candidate in self.potential_issuers(working_cert) { ++ for issuing_cert_candidate in self.potential_issuers(working_cert, working_cert_extensions) ++ { + // A candidate issuer is said to verify if it both + // signs for the working certificate and conforms to the + // policy. +@@ -324,6 +384,7 @@ impl<'a, 'chain, B: CryptoOps> ChainBuilder<'a, 'chain, B> { + working_cert.certificate(), + current_depth, + &issuer_extensions, ++ budget, + ) { + Ok(_) => { + match self.build_chain_inner( +@@ -417,3 +478,135 @@ impl<'a, 'chain, B: CryptoOps> ChainBuilder<'a, 'chain, B> { + Ok(chain) + } + } ++ ++#[cfg(test)] ++mod tests { ++ use asn1::ParseError; ++ use cryptography_x509::certificate::Certificate; ++ use cryptography_x509::oid::SUBJECT_ALTERNATIVE_NAME_OID; ++ ++ use crate::certificate::tests::PublicKeyErrorOps; ++ use crate::ops::{CryptoOps, VerificationCertificate}; ++ use crate::policy::{Policy, PolicyDefinition, Subject}; ++ use crate::trust_store::Store; ++ use crate::types::DNSName; ++ use crate::{Budget, ChainBuilder, NameChain, ValidationError}; ++ ++ #[test] ++ fn test_validationerror_display() { ++ let err = ValidationError::Malformed( ++ ParseError::new(asn1::ParseErrorKind::InvalidLength), ++ ); ++ assert_eq!(err.to_string(), "ASN.1 parsing error: invalid length"); ++ ++ let err = ValidationError::ExtensionError{ ++ oid: SUBJECT_ALTERNATIVE_NAME_OID, ++ reason: "duplicate extension", ++ }; ++ assert_eq!( ++ err.to_string(), ++ "invalid extension: 2.5.29.17: duplicate extension" ++ ); ++ ++ let err = ValidationError::FatalError("oops"); ++ assert_eq!(err.to_string(), "fatal error: oops"); ++ } ++ ++ /// A `CryptoOps` whose public key extraction and signature verification ++ /// always succeed, so that `valid_issuer` can be driven to completion ++ /// without real cryptographic material. ++ struct NullOps; ++ ++ impl CryptoOps for NullOps { ++ type Key = (); ++ type Err = (); ++ type CertificateExtra = (); ++ type PolicyExtra = (); ++ ++ fn public_key(&self, _cert: &Certificate<'_>) -> Result { ++ Ok(()) ++ } ++ ++ fn verify_signed_by( ++ &self, ++ _cert: &Certificate<'_>, ++ _key: &Self::Key, ++ ) -> Result<(), Self::Err> { ++ Ok(()) ++ } ++ ++ fn clone_public_key(_key: &Self::Key) -> Self::Key {} ++ ++ fn clone_extra(_extra: &Self::CertificateExtra) -> Self::CertificateExtra {} ++ } ++ ++ #[test] ++ fn test_clone() { ++ assert_eq!(NullOps::clone_public_key(&()), ()); ++ assert_eq!(NullOps::clone_extra(&()), ()); ++ } ++ ++ // A self-issued ("looping") CA certificate that is its own issuer. ++ fn looping_ca_pem() -> pem::Pem { ++ pem::parse( ++ "-----BEGIN CERTIFICATE----- ++MIIBcjCCARmgAwIBAgIBATAKBggqhkjOPQQDAjAhMR8wHQYDVQQDDBZsb29waW5n ++IHNlbGYtc2lnbmVkIENBMB4XDTIzMTIzMTAwMDAwMFoXDTI0MDEzMTAwMDAwMFow ++ITEfMB0GA1UEAwwWbG9vcGluZyBzZWxmLXNpZ25lZCBDQTBZMBMGByqGSM49AgEG ++CCqGSM49AwEHA0IABKAoXUGnHdfXJbSXjRjeW+PCVHmlo4KEki69N5pJUA0QyQMR ++v9ySOMnWf3Ea7TR4g3zdguwTP7LdpSku3uR1QkmjQjBAMA8GA1UdEwEB/wQFMAMB ++Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBR23MGdG1Ma9iR+3CxKTafD/OE0 ++dTAKBggqhkjOPQQDAgNHADBEAiA4RCr07KfZdM16VfGNZAQFjvC60SWIU3RRVY/L ++qolIOwIgCaIgj9ipK0Q0p+45UJiq+L/ncrxsweJkFq/UYubzhX0= ++-----END CERTIFICATE-----", ++ ) ++ .unwrap() ++ } ++ ++ /// Exercises our pathlen overflow error scenario. ++ /// ++ /// This condition is logically unreachable from Python, since ++ /// we unconditionally limit signature checks to a number smaller ++ /// than `u8::MAX`, meaning that we always exhaust the signature budget ++ /// before potentially exhausting the pathlen budget. ++ /// ++ /// To test that directly, we manually lift the signature budget ++ /// and start our pathlen state right at `u8::MAX`, guaranteeing ++ /// an overflow on the immediate chain building step. ++ #[test] ++ fn test_build_chain_inner_depth_overflow() { ++ let pem = looping_ca_pem(); ++ let ca = asn1::parse_single::>(pem.contents()).unwrap(); ++ let ca_exts = ca.extensions().ok().unwrap(); ++ ++ // The same self-issued CA is both the working certificate and its own ++ // (only) candidate issuer, so the search recurses on itself. ++ let working = VerificationCertificate::::new(&ca, ()); ++ let intermediates = [VerificationCertificate::::new(&ca, ())]; ++ let store: Store<'_, NullOps> = Store::new([]); ++ ++ let subject = Subject::DNS(DNSName::new("example.com").unwrap()); ++ let time = asn1::DateTime::new(2024, 1, 1, 0, 0, 0).unwrap(); ++ let policy_def = ++ PolicyDefinition::server(NullOps, subject, time, Some(u8::MAX), None, None).unwrap(); ++ let policy = Policy::new(&policy_def, ()); ++ ++ let builder = ChainBuilder::new(&intermediates, &policy, &store); ++ let mut budget = Budget { ++ name_constraint_checks: usize::MAX, ++ signature_checks: usize::MAX, ++ }; ++ ++ let name_chain = NameChain::new::(None, &ca_exts, false) ++ .ok() ++ .unwrap(); ++ let err = builder ++ .build_chain_inner(&working, u8::MAX, &ca_exts, name_chain, &mut budget) ++ .unwrap_err(); ++ ++ assert!(matches!( ++ err.kind, ++ ValidationError::Other(msg) if msg.contains("current depth calculation overflowed") ++ )); ++ } ++} +diff --git a/src/rust/cryptography-x509-verification/src/policy/mod.rs b/src/rust/cryptography-x509-verification/src/policy/mod.rs +index d5a199d..5bdc8d5 100644 +--- a/src/rust/cryptography-x509-verification/src/policy/mod.rs ++++ b/src/rust/cryptography-x509-verification/src/policy/mod.rs +@@ -25,7 +25,7 @@ use once_cell::sync::Lazy; + use crate::ops::CryptoOps; + use crate::policy::extension::{ca, common, ee, Criticality, ExtensionPolicy, ExtensionValidator}; + use crate::types::{DNSName, DNSPattern, IPAddress}; +-use crate::{ValidationError, VerificationCertificate}; ++use crate::{Budget, ValidationError, VerificationCertificate}; + + // SubjectPublicKeyInfo AlgorithmIdentifier constants, as defined in CA/B 7.1.3.1. + +@@ -463,10 +463,16 @@ impl<'a, B: CryptoOps> Policy<'a, B> { + child: &Certificate<'_>, + current_depth: u8, + issuer_extensions: &Extensions<'_>, ++ budget: &mut Budget, + ) -> Result<(), ValidationError> { + // The issuer needs to be a valid CA at the current depth. + self.permits_ca(issuer.certificate(), current_depth, issuer_extensions)?; + ++ // Charge the (potentially expensive) signature verification against the ++ // budget before performing it, bounding the total work an attacker can ++ // force during chain building. ++ budget.signature_check()?; ++ + // CA/B 7.1.3.1 SubjectPublicKeyInfo + // NOTE: We check the issuer's SPKI here, since the issuer is + // definitionally a CA and thus subject to CABF key requirements. +-- +2.43.0 + diff --git a/meta/recipes-devtools/python/python3-cryptography_42.0.5.bb b/meta/recipes-devtools/python/python3-cryptography_42.0.5.bb index 8148ec0ba5..899332123f 100644 --- a/meta/recipes-devtools/python/python3-cryptography_42.0.5.bb +++ b/meta/recipes-devtools/python/python3-cryptography_42.0.5.bb @@ -14,6 +14,7 @@ SRC_URI += "file://0001-pyproject.toml-remove-benchmark-disable-option.patch \ file://CVE-2026-26007.patch \ file://CVE-2026-34073.patch \ file://CVE-2026-69248.patch \ + file://CVE-2026-69249.patch \ file://check-memfree.py \ file://run-ptest \ "