mirror of
https://git.yoctoproject.org/poky
synced 2026-10-15 17:07:16 +00:00
python3-cryptography: Fix CVE-2026-69249
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 (From OE-Core rev: cd8e9c134ccb9aa2c6cf4a9dabf610bf38002646) Signed-off-by: Vijay Anusuri <vanusuri@mvista.com> Signed-off-by: Yoann Congal <yoann.congal@smile.fr> Signed-off-by: Paul Barker <paul@pbarker.dev>
This commit is contained in:
1 parent
7603de1570
commit
ad4658cfe0
2 files changed
+355
No files matched your search
@@ -0,0 +1,354 @@
|
||||
From 4a12cf49675a184e47f912b00b04f3a629283582 Mon Sep 17 00:00:00 2001
|
||||
From: William Woodruff <william@yossarian.net>
|
||||
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
|
||||
|
||||
Note: SUSE patch CVE-2026-69249-signature-validation-budget.patch was adapted to our codebase where required. The budget-handling hunks that are not present in the backported patch are already available in our existing codebase, so those changes were not applied again.
|
||||
|
||||
Additionally, add tests for the ValidationError Display implementation
|
||||
to align with corresponding upstream commit.
|
||||
|
||||
Upstream-Status: Backport [import from suse https://download.opensuse.org/distribution/leap-micro/6.1/product/repo/openSUSE-Leap-Micro-6.1-x86_64-Source/src/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 <vanusuri@mvista.com>
|
||||
---
|
||||
.../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<asn1::ParseError> 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<Item = &'a VerificationCertificate<'chain, B>> + '_ {
|
||||
- // 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::<AuthorityKeyIdentifier<'_>>().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: <https://github.com/golang/go/blob/d00c67f297e/src/crypto/x509/cert_pool.go#L136>
|
||||
+ 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<ValidationError> = 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<Self::Key, Self::Err> {
|
||||
+ 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::<Certificate<'_>>(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::<NullOps>::new(&ca, ());
|
||||
+ let intermediates = [VerificationCertificate::<NullOps>::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::<NullOps>(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
|
||||
|
||||
@@ -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 \
|
||||
"
|
||||
|
||||
Reference in new issue
Block a user