Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-Twisted for openSUSE:Factory 
checked in at 2026-06-16 18:29:20
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-Twisted (Old)
 and      /work/SRC/openSUSE:Factory/.python-Twisted.new.1981 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-Twisted"

Tue Jun 16 18:29:20 2026 rev:80 rq:1359744 version:26.4.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-Twisted/python-Twisted.changes    
2026-05-16 19:23:53.272472336 +0200
+++ /work/SRC/openSUSE:Factory/.python-Twisted.new.1981/python-Twisted.changes  
2026-06-16 18:29:24.673550913 +0200
@@ -1,0 +2,5 @@
+Tue Jun 16 09:10:34 UTC 2026 - Dirk Müller <[email protected]>
+
+- add pycryptography-use-csr.patch
+
+-------------------------------------------------------------------

New:
----
  pycryptography-use-csr.patch

----------(New B)----------
  New:
- add pycryptography-use-csr.patch
----------(New E)----------

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-Twisted.spec ++++++
--- /var/tmp/diff_new_pack.mPNXpW/_old  2026-06-16 18:29:25.525586795 +0200
+++ /var/tmp/diff_new_pack.mPNXpW/_new  2026-06-16 18:29:25.529586964 +0200
@@ -46,6 +46,8 @@
 Patch2:         no-cython_test_exception_raiser.patch
 # PATCH-FIX-OPENSUSE remove-dependency-version-upper-bounds.patch boo#1190036 
-- run with h2 >= 4.0.0 and priority >= 2.0
 Patch3:         remove-dependency-version-upper-bounds.patch
+# PATCH-FIX-UPSTREAM: https://github.com/twisted/twisted/pull/12661
+Patch4:         pycryptography-use-csr.patch
 BuildRequires:  %{python_module base >= 3.9}
 BuildRequires:  %{python_module hatch-fancy-pypi-readme}
 BuildRequires:  %{python_module hatchling}

++++++ pycryptography-use-csr.patch ++++++
>From 5b4601c9965ffc92d6aa952b8c05127d5ac37307 Mon Sep 17 00:00:00 2001
From: Alex Gaynor <[email protected]>
Date: Mon, 8 Jun 2026 08:51:19 -0700
Subject: [PATCH 1/5] #12660 Replace OpenSSL.crypto.X509Req with
 pyca/cryptography CSR

Replace usage of pyOpenSSL's X509Req with pyca/cryptography's
CertificateSigningRequest in twisted.internet._sslverify and the
test_ssl certificate generation helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---
 src/twisted/internet/_sslverify.py   | 103 ++++++++++++++++++++++-----
 src/twisted/newsfragments/12660.misc |   0
 src/twisted/test/test_ssl.py         |  42 +++++++----
 3 files changed, 114 insertions(+), 31 deletions(-)
 create mode 100644 src/twisted/newsfragments/12660.misc

diff --git a/src/twisted/internet/_sslverify.py 
b/src/twisted/internet/_sslverify.py
index c6dabf6c4c4..f1cec9c2d87 100644
--- a/src/twisted/internet/_sslverify.py
+++ b/src/twisted/internet/_sslverify.py
@@ -20,6 +20,9 @@
 
 import attr
 from constantly import FlagConstant, Flags, NamedConstant, Names
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.x509.oid import NameOID
 from incremental import Version
 
 from twisted.internet.abstract import isIPAddress, isIPv6Address
@@ -169,6 +172,29 @@ def protocolNegotiationMechanisms() -> FlagConstant:
     "emailAddress": "emailAddress",
 }
 
+_x509NameOIDs = {
+    "commonName": NameOID.COMMON_NAME,
+    "organizationName": NameOID.ORGANIZATION_NAME,
+    "organizationalUnitName": NameOID.ORGANIZATIONAL_UNIT_NAME,
+    "localityName": NameOID.LOCALITY_NAME,
+    "stateOrProvinceName": NameOID.STATE_OR_PROVINCE_NAME,
+    "countryName": NameOID.COUNTRY_NAME,
+    "emailAddress": NameOID.EMAIL_ADDRESS,
+}
+
+# Reverse of _x509NameOIDs, for translating a parsed subject back into a
+# DistinguishedName.
+_x509OIDNames = {oid: name for name, oid in _x509NameOIDs.items()}
+
+_digestAlgorithms = {
+    "md5": hashes.MD5,
+    "sha1": hashes.SHA1,
+    "sha224": hashes.SHA224,
+    "sha256": hashes.SHA256,
+    "sha384": hashes.SHA384,
+    "sha512": hashes.SHA512,
+}
+
 
 class DistinguishedName(dict[str, bytes]):
     """
@@ -490,19 +516,51 @@ class CertificateRequest(CertBase):
 
     Certificate requests are given to certificate authorities to be signed and
     returned resulting in an actual certificate.
+
+    @ivar original: The underlying CSR object.
+    @type original: L{cryptography.x509.CertificateSigningRequest}
     """
 
     @classmethod
-    def load(Class, requestData, requestFormat=crypto.FILETYPE_ASN1):
-        req = crypto.load_certificate_request(requestFormat, requestData)
+    def load(
+        cls, requestData: bytes, requestFormat=crypto.FILETYPE_ASN1
+    ) -> CertificateRequest:
+        if requestFormat == crypto.FILETYPE_ASN1:
+            req = x509.load_der_x509_csr(requestData)
+        elif requestFormat == crypto.FILETYPE_PEM:
+            req = x509.load_pem_x509_csr(requestData)
+        else:
+            raise ValueError(f"Unsupported format: {requestFormat!r}")
+        if not req.is_signature_valid:
+            subject = req.subject
+            raise VerifyError(
+                f"Can't verify that request for {subject!r} is self-signed."
+            )
+        return cls(req)
+
+    def _subjectToDistinguishedName(self) -> DistinguishedName:
+        """
+        Retrieve the subject of this certificate request.
+
+        @return: A copy of the subject of this certificate request.
+        @rtype: L{DistinguishedName}
+        """
         dn = DistinguishedName()
-        dn._copyFrom(req.get_subject())
-        if not req.verify(req.get_pubkey()):
-            raise VerifyError(f"Can't verify that request for {dn!r} is 
self-signed.")
-        return Class(req)
+        for attribute in self.original.subject:
+            try:
+                name = _x509OIDNames[attribute.oid]
+            except KeyError:
+                raise ValueError(f"Unknown X509 name attribute: 
{attribute.oid!r}")
+            setattr(dn, name, attribute.value)
+        return dn
 
-    def dump(self, format=crypto.FILETYPE_ASN1):
-        return crypto.dump_certificate_request(format, self.original)
+    def dump(self, format=crypto.FILETYPE_ASN1) -> bytes:
+        if format == crypto.FILETYPE_ASN1:
+            return self.original.public_bytes(serialization.Encoding.DER)
+        elif format == crypto.FILETYPE_PEM:
+            return self.original.public_bytes(serialization.Encoding.PEM)
+        else:
+            raise ValueError(f"Unsupported format: {format!r}")
 
 
 class PrivateCertificate(Certificate):
@@ -714,10 +772,21 @@ def newCertificate(self, newCertData, 
format=crypto.FILETYPE_ASN1):
         return PrivateCertificate.load(newCertData, self, format)
 
     def requestObject(self, distinguishedName, digestAlgorithm="sha256"):
-        req = crypto.X509Req()
-        req.set_pubkey(self.original)
-        distinguishedName._copyInto(req.get_subject())
-        req.sign(self.original, digestAlgorithm)
+        req = (
+            x509.CertificateSigningRequestBuilder()
+            .subject_name(
+                x509.Name(
+                    [
+                        x509.NameAttribute(_x509NameOIDs[k], nativeString(v))
+                        for k, v in distinguishedName.items()
+                    ]
+                )
+            )
+            .sign(
+                self.original.to_cryptography_key(),
+                _digestAlgorithms[digestAlgorithm](),
+            )
+        )
         return CertificateRequest(req)
 
     def certificateRequest(
@@ -750,7 +819,7 @@ def signCertificateRequest(
         """
         hlreq = CertificateRequest.load(requestData, requestFormat)
 
-        dn = hlreq.getSubject()
+        dn = hlreq._subjectToDistinguishedName()
         vval = verifyDNCallback(dn)
 
         def verified(value):
@@ -787,8 +856,8 @@ def signRequestObject(
         req = requestObject.original
         cert = crypto.X509()
         issuerDistinguishedName._copyInto(cert.get_issuer())
-        cert.set_subject(req.get_subject())
-        cert.set_pubkey(req.get_pubkey())
+        
requestObject._subjectToDistinguishedName()._copyInto(cert.get_subject())
+        cert.set_pubkey(crypto.PKey.from_cryptography_key(req.public_key()))
         cert.gmtime_adj_notBefore(0)
         cert.gmtime_adj_notAfter(secondsToExpiry)
         cert.set_serial_number(serialNumber)
@@ -1656,10 +1725,10 @@ def contextSelectionCallback(connection: 
SSL.Connection) -> None:
         return ctx
 
 
-OpenSSLCertificateOptions.__getstate__ = deprecated(  # 
type:ignore[method-assign]
+OpenSSLCertificateOptions.__getstate__ = deprecated(  # type: 
ignore[method-assign]
     Version("Twisted", 15, 0, 0), "a real persistence system"
 )(OpenSSLCertificateOptions.__getstate__)
-OpenSSLCertificateOptions.__setstate__ = deprecated(  # 
type:ignore[method-assign]
+OpenSSLCertificateOptions.__setstate__ = deprecated(  # type: 
ignore[method-assign]
     Version("Twisted", 15, 0, 0), "a real persistence system"
 )(OpenSSLCertificateOptions.__setstate__)
 
diff --git a/src/twisted/newsfragments/12660.misc 
b/src/twisted/newsfragments/12660.misc
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/twisted/test/test_ssl.py b/src/twisted/test/test_ssl.py
index cb5d6925af7..8e2eb423949 100644
--- a/src/twisted/test/test_ssl.py
+++ b/src/twisted/test/test_ssl.py
@@ -21,6 +21,10 @@
 try:
     from OpenSSL import SSL, crypto
 
+    from cryptography import x509
+    from cryptography.hazmat.primitives import hashes, serialization
+    from cryptography.x509.oid import NameOID
+
     from twisted.internet import ssl
     from twisted.test.ssl_helpers import ClientTLSContext, certPath
 except ImportError:
@@ -164,21 +168,31 @@ def generateCertificateObjects(organization, 
organizationalUnit):
     """
     pkey = crypto.PKey()
     pkey.generate_key(crypto.TYPE_RSA, 2048)
-    req = crypto.X509Req()
-    subject = req.get_subject()
-    subject.O = organization
-    subject.OU = organizationalUnit
-    req.set_pubkey(pkey)
-    req.sign(pkey, "md5")
+    req = (
+        x509.CertificateSigningRequestBuilder()
+        .subject_name(
+            x509.Name(
+                [
+                    x509.NameAttribute(NameOID.ORGANIZATION_NAME, 
organization),
+                    x509.NameAttribute(
+                        NameOID.ORGANIZATIONAL_UNIT_NAME, organizationalUnit
+                    ),
+                ]
+            )
+        )
+        .sign(pkey.to_cryptography_key(), hashes.SHA256())
+    )
 
     # Here comes the actual certificate
     cert = crypto.X509()
     cert.set_serial_number(1)
     cert.gmtime_adj_notBefore(0)
     cert.gmtime_adj_notAfter(60)  # Testing certificates need not be long lived
-    cert.set_issuer(req.get_subject())
-    cert.set_subject(req.get_subject())
-    cert.set_pubkey(req.get_pubkey())
+    subject = cert.get_subject()
+    subject.O = organization
+    subject.OU = organizationalUnit
+    cert.set_issuer(cert.get_subject())
+    cert.set_pubkey(pkey)
     cert.sign(pkey, "md5")
 
     return pkey, req, cert
@@ -191,13 +205,13 @@ def generateCertificateFiles(basename, organization, 
organizationalUnit):
     """
     pkey, req, cert = generateCertificateObjects(organization, 
organizationalUnit)
 
-    for ext, obj, dumpFunc in [
-        ("key", pkey, crypto.dump_privatekey),
-        ("req", req, crypto.dump_certificate_request),
-        ("cert", cert, crypto.dump_certificate),
+    for ext, data in [
+        ("key", crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)),
+        ("req", req.public_bytes(serialization.Encoding.PEM)),
+        ("cert", crypto.dump_certificate(crypto.FILETYPE_PEM, cert)),
     ]:
         fName = os.extsep.join((basename, ext)).encode("utf-8")
-        FilePath(fName).setContent(dumpFunc(crypto.FILETYPE_PEM, obj))
+        FilePath(fName).setContent(data)
 
 
 class ContextGeneratingMixin:

>From 3fc8db4e7e3924eb3e6b93bb67380bcdb273e17e Mon Sep 17 00:00:00 2001
From: Alex Gaynor <[email protected]>
Date: Mon, 8 Jun 2026 09:00:00 -0700
Subject: [PATCH 2/5] Fix mypy errors in CertificateRequest

Annotate the format parameters and type the original CSR attribute so
public_bytes is no longer inferred as Any.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---
 src/twisted/internet/_sslverify.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/twisted/internet/_sslverify.py 
b/src/twisted/internet/_sslverify.py
index f1cec9c2d87..fdfa37adeef 100644
--- a/src/twisted/internet/_sslverify.py
+++ b/src/twisted/internet/_sslverify.py
@@ -521,9 +521,11 @@ class CertificateRequest(CertBase):
     @type original: L{cryptography.x509.CertificateSigningRequest}
     """
 
+    original: x509.CertificateSigningRequest
+
     @classmethod
     def load(
-        cls, requestData: bytes, requestFormat=crypto.FILETYPE_ASN1
+        cls, requestData: bytes, requestFormat: int = crypto.FILETYPE_ASN1
     ) -> CertificateRequest:
         if requestFormat == crypto.FILETYPE_ASN1:
             req = x509.load_der_x509_csr(requestData)
@@ -554,7 +556,7 @@ def _subjectToDistinguishedName(self) -> DistinguishedName:
             setattr(dn, name, attribute.value)
         return dn
 
-    def dump(self, format=crypto.FILETYPE_ASN1) -> bytes:
+    def dump(self, format: int = crypto.FILETYPE_ASN1) -> bytes:
         if format == crypto.FILETYPE_ASN1:
             return self.original.public_bytes(serialization.Encoding.DER)
         elif format == crypto.FILETYPE_PEM:

>From 7cb81ce013455a0adfee8e439c309593bd9d192d Mon Sep 17 00:00:00 2001
From: Alex Gaynor <[email protected]>
Date: Mon, 8 Jun 2026 09:17:26 -0700
Subject: [PATCH 3/5] Add CertificateRequest test coverage

Cover the PEM/DER load and dump branches, unsupported-format and
unverifiable-signature errors, and the unknown subject attribute path.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---
 src/twisted/test/test_sslverify.py | 81 ++++++++++++++++++++++++++++++
 1 file changed, 81 insertions(+)

diff --git a/src/twisted/test/test_sslverify.py 
b/src/twisted/test/test_sslverify.py
index c50fb8b1055..579ae1d03f6 100644
--- a/src/twisted/test/test_sslverify.py
+++ b/src/twisted/test/test_sslverify.py
@@ -51,6 +51,7 @@
     from cryptography import x509
     from cryptography.hazmat.backends import default_backend
     from cryptography.hazmat.primitives import hashes
+    from cryptography.hazmat.primitives.asymmetric import ec
     from cryptography.hazmat.primitives.asymmetric.rsa import (
         RSAPrivateKey,
         generate_private_key,
@@ -3472,3 +3473,83 @@ def test_noTrailingNewlinePemCert(self):
 
         certPEM = noTrailingNewlineKeyPemPath.getContent()
         ssl.Certificate.loadPEM(certPEM)
+
+
+class CertificateRequestTests(SynchronousTestCase):
+    """
+    Tests for L{sslverify.CertificateRequest}.
+    """
+
+    if skipSSL:
+        skip = skipSSL
+
+    def _makeRequest(self):
+        """
+        Create a self-signed L{sslverify.CertificateRequest}.
+
+        @return: a fresh certificate request.
+        @rtype: L{sslverify.CertificateRequest}
+        """
+        dn = 
sslverify.DistinguishedName(commonName="example.twistedmatrix.com")
+        return sslverify.KeyPair.generate().requestObject(dn)
+
+    def test_pemRoundTrip(self):
+        """
+        A L{sslverify.CertificateRequest} dumped to PEM format and loaded back
+        again preserves its subject.
+        """
+        request = self._makeRequest()
+        pem = request.dump(FILETYPE_PEM)
+        self.assertIn(b"BEGIN CERTIFICATE REQUEST", pem)
+        loaded = sslverify.CertificateRequest.load(pem, FILETYPE_PEM)
+        self.assertEqual(
+            loaded._subjectToDistinguishedName(),
+            request._subjectToDistinguishedName(),
+        )
+
+    def test_loadUnsupportedFormat(self):
+        """
+        L{sslverify.CertificateRequest.load} raises L{ValueError} when given an
+        unrecognized format.
+        """
+        request = self._makeRequest()
+        with self.assertRaises(ValueError):
+            sslverify.CertificateRequest.load(request.dump(), object())
+
+    def test_loadUnverifiableSignature(self):
+        """
+        L{sslverify.CertificateRequest.load} raises L{sslverify.VerifyError}
+        when the request's self-signature does not verify.
+        """
+        data = bytearray(self._makeRequest().dump())
+        # Corrupt the trailing signature bytes so the self-signature no longer
+        # verifies while the structure still parses.
+        data[-1] ^= 0xFF
+        with self.assertRaises(sslverify.VerifyError):
+            sslverify.CertificateRequest.load(bytes(data))
+
+    def test_dumpUnsupportedFormat(self):
+        """
+        L{sslverify.CertificateRequest.dump} raises L{ValueError} when given an
+        unrecognized format.
+        """
+        with self.assertRaises(ValueError):
+            self._makeRequest().dump(object())
+
+    def test_subjectUnknownAttribute(self):
+        """
+        L{sslverify.CertificateRequest._subjectToDistinguishedName} raises
+        L{ValueError} when the subject contains a name attribute that does not
+        correspond to a known L{sslverify.DistinguishedName} field.
+        """
+        key = ec.generate_private_key(ec.SECP256R1(), 
backend=default_backend())
+        csr = (
+            x509.CertificateSigningRequestBuilder()
+            .subject_name(
+                x509.Name([x509.NameAttribute(NameOID.GIVEN_NAME, "Alice")])
+            )
+            .sign(key, hashes.SHA256())
+        )
+        request = sslverify.CertificateRequest(csr)
+        with self.assertRaises(ValueError):
+            request._subjectToDistinguishedName()

>From 7f5219d25e9eabed51f20f707ac10b1533c7a8c3 Mon Sep 17 00:00:00 2001
From: Alex Gaynor <[email protected]>
Date: Wed, 10 Jun 2026 06:14:45 -0700
Subject: [PATCH 4/5] remove comment that's no longer required

---
 src/twisted/internet/_sslverify.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/twisted/internet/_sslverify.py 
b/src/twisted/internet/_sslverify.py
index fdfa37adeef..bd71a401636 100644
--- a/src/twisted/internet/_sslverify.py
+++ b/src/twisted/internet/_sslverify.py
@@ -518,7 +518,6 @@ class CertificateRequest(CertBase):
     returned resulting in an actual certificate.
 
     @ivar original: The underlying CSR object.
-    @type original: L{cryptography.x509.CertificateSigningRequest}
     """
 
     original: x509.CertificateSigningRequest

>From b2201db3e08760d4d0ae6563b44109d5c5f555df Mon Sep 17 00:00:00 2001
From: Alex Gaynor <[email protected]>
Date: Wed, 10 Jun 2026 06:15:39 -0700
Subject: [PATCH 5/5] fmt

---
 src/twisted/test/test_sslverify.py | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/twisted/test/test_sslverify.py 
b/src/twisted/test/test_sslverify.py
index 579ae1d03f6..282121bc023 100644
--- a/src/twisted/test/test_sslverify.py
+++ b/src/twisted/test/test_sslverify.py
@@ -3545,9 +3545,7 @@ def test_subjectUnknownAttribute(self):
         key = ec.generate_private_key(ec.SECP256R1(), 
backend=default_backend())
         csr = (
             x509.CertificateSigningRequestBuilder()
-            .subject_name(
-                x509.Name([x509.NameAttribute(NameOID.GIVEN_NAME, "Alice")])
-            )
+            .subject_name(x509.Name([x509.NameAttribute(NameOID.GIVEN_NAME, 
"Alice")]))
             .sign(key, hashes.SHA256())
         )
         request = sslverify.CertificateRequest(csr)

Reply via email to