File use-cryptography.patch of Package python-pysaml2

From 930a652a240c8cd1489429a7d70cf5fa7ef1606a Mon Sep 17 00:00:00 2001
From: Patrick Rauscher <prauscher@prauscher.de>
Date: Wed, 12 Feb 2025 23:29:34 +0100
Subject: [PATCH] replace pyopenssl with cryptography

---
 pyproject.toml      |   3 +-
 src/saml2/cert.py   | 178 ++++++++++++++++++++++++--------------------
 src/saml2/sigver.py |  12 +--
 3 files changed, 105 insertions(+), 88 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 985692043..8a7cd9185 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,12 +37,11 @@ parse_xsd2 = "saml2.tools.parse_xsd2:main"
 
 [tool.poetry.dependencies]
 python = "^3.9"
-cryptography = ">=3.1"
+cryptography = ">=40.0"
 defusedxml = "*"
 importlib-metadata = {version = ">=1.7.0", python = "<3.8"}
 importlib-resources = {python = "<3.9", version = "*"}
 paste = {optional = true, version = "*"}
-pyopenssl = "<24.3.0"
 python-dateutil = "*"
 pytz = "*"
 "repoze.who" = {optional = true, version = "*"}
diff --git a/src/saml2/cert.py b/src/saml2/cert.py
index c5f626601..1759b9b24 100644
--- a/src/saml2/cert.py
+++ b/src/saml2/cert.py
@@ -5,7 +5,11 @@
 from os import remove
 from os.path import join
 
-from OpenSSL import crypto
+from cryptography import x509
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.x509.oid import NameOID
 import dateutil.parser
 import pytz
 
@@ -36,7 +40,6 @@ def create_certificate(
         valid_to=315360000,
         sn=1,
         key_length=1024,
-        hash_alg="sha256",
         write_to_file=False,
         cert_dir="",
         cipher_passphrase=None,
@@ -87,8 +90,6 @@ def create_certificate(
                                   is 1.
         :param key_length:        Length of the key to be generated. Defaults
                                   to 1024.
-        :param hash_alg:          Hash algorithm to use for the key. Default
-                                  is sha256.
         :param write_to_file:     True if you want to write the certificate
                                   to a file. The method will then return
                                   a tuple with path to certificate file and
@@ -131,49 +132,68 @@ def create_certificate(
             k_f = join(cert_dir, key_file)
 
         # create a key pair
-        k = crypto.PKey()
-        k.generate_key(crypto.TYPE_RSA, key_length)
+        k = rsa.generate_private_key(
+            public_exponent=65537,
+            key_size=key_length,
+        )
 
         # create a self-signed cert
-        cert = crypto.X509()
+        builder = x509.CertificateBuilder()
 
         if request:
-            cert = crypto.X509Req()
+            builder = x509.CertificateSigningRequestBuilder()
 
         if len(cert_info["country_code"]) != 2:
             raise WrongInput("Country code must be two letters!")
-        cert.get_subject().C = cert_info["country_code"]
-        cert.get_subject().ST = cert_info["state"]
-        cert.get_subject().L = cert_info["city"]
-        cert.get_subject().O = cert_info["organization"]  # noqa: E741
-        cert.get_subject().OU = cert_info["organization_unit"]
-        cert.get_subject().CN = cn
+        subject_name = x509.Name([
+            x509.NameAttribute(NameOID.COUNTRY_NAME,
+                               cert_info["country_code"]),
+            x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME,
+                               cert_info["state"]),
+            x509.NameAttribute(NameOID.LOCALITY_NAME,
+                               cert_info["city"]),
+            x509.NameAttribute(NameOID.ORGANIZATION_NAME,
+                               cert_info["organization"]),
+            x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME,
+                               cert_info["organization_unit"]),
+            x509.NameAttribute(NameOID.COMMON_NAME, cn),
+        ])
+        builder = builder.subject_name(subject_name)
         if not request:
-            cert.set_serial_number(sn)
-            cert.gmtime_adj_notBefore(valid_from)  # Valid before present time
-            cert.gmtime_adj_notAfter(valid_to)  # 3 650 days
-            cert.set_issuer(cert.get_subject())
-        cert.set_pubkey(k)
-        cert.sign(k, hash_alg)
+            now = datetime.datetime.now(datetime.UTC)
+            builder = builder.serial_number(
+                sn,
+            ).not_valid_before(
+                now + datetime.timedelta(seconds=valid_from),
+            ).not_valid_after(
+                now + datetime.timedelta(seconds=valid_to),
+            ).issuer_name(
+                subject_name,
+            ).public_key(
+                k.public_key(),
+            )
+        cert = builder.sign(k, hashes.SHA256())
 
         try:
-            if request:
-                tmp_cert = crypto.dump_certificate_request(crypto.FILETYPE_PEM, cert)
-            else:
-                tmp_cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
-            tmp_key = None
+            tmp_cert = cert.public_bytes(serialization.Encoding.PEM)
+            key_encryption = None
             if cipher_passphrase is not None:
                 passphrase = cipher_passphrase["passphrase"]
                 if isinstance(cipher_passphrase["passphrase"], str):
                     passphrase = passphrase.encode("utf-8")
-                tmp_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, k, cipher_passphrase["cipher"], passphrase)
+                key_encryption = serialization.BestAvailableEncryption(passphrase)
             else:
-                tmp_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, k)
+                key_encryption = serialization.NoEncryption()
+            tmp_key = k.private_bytes(
+                encoding=serialization.Encoding.PEM,
+                format=serialization.PrivateFormat.TraditionalOpenSSL,
+                encryption_algorithm=key_encryption,
+            )
             if write_to_file:
-                with open(c_f, "w") as fc:
-                    fc.write(tmp_cert.decode("utf-8"))
-                with open(k_f, "w") as fk:
-                    fk.write(tmp_key.decode("utf-8"))
+                with open(c_f, "wb") as fc:
+                    fc.write(tmp_cert)
+                with open(k_f, "wb") as fk:
+                    fk.write(tmp_key)
                 return c_f, k_f
             return tmp_cert, tmp_key
         except Exception as ex:
@@ -198,7 +218,6 @@ def create_cert_signed_certificate(
         sign_cert_str,
         sign_key_str,
         request_cert_str,
-        hash_alg="sha256",
         valid_from=0,
         valid_to=315360000,
         sn=1,
@@ -222,8 +241,6 @@ def create_cert_signed_certificate(
                                   the requested certificate. If you only have
                                   a file use the method read_str_from_file
                                   to get a string representation.
-        :param hash_alg:          Hash algorithm to use for the key. Default
-                                  is sha256.
         :param valid_from:        When the certificate starts to be valid.
                                   Amount of seconds from when the
                                   certificate is generated.
@@ -237,27 +254,29 @@ def create_cert_signed_certificate(
         :return:                  String representation of the signed
                                   certificate.
         """
-        ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, sign_cert_str)
-        ca_key = None
-        if passphrase is not None:
-            ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, sign_key_str, passphrase)
-        else:
-            ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, sign_key_str)
-        req_cert = crypto.load_certificate_request(crypto.FILETYPE_PEM, request_cert_str)
-
-        cert = crypto.X509()
-        cert.set_subject(req_cert.get_subject())
-        cert.set_serial_number(sn)
-        cert.gmtime_adj_notBefore(valid_from)
-        cert.gmtime_adj_notAfter(valid_to)
-        cert.set_issuer(ca_cert.get_subject())
-        cert.set_pubkey(req_cert.get_pubkey())
-        cert.sign(ca_key, hash_alg)
-
-        cert_dump = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
-        if isinstance(cert_dump, str):
-            return cert_dump
-        return cert_dump.decode("utf-8")
+        if isinstance(sign_cert_str, str):
+            sign_cert_str = sign_cert_str.encode("utf-8")
+        ca_cert = x509.load_pem_x509_certificate(sign_cert_str)
+        ca_key = serialization.load_pem_private_key(
+            sign_key_str, password=passphrase)
+        req_cert = x509.load_pem_x509_csr(request_cert_str)
+
+        now = datetime.datetime.now(datetime.UTC)
+        cert = x509.CertificateBuilder().subject_name(
+            req_cert.subject,
+        ).serial_number(
+            sn,
+        ).not_valid_before(
+            now + datetime.timedelta(seconds=valid_from),
+        ).not_valid_after(
+            now + datetime.timedelta(seconds=valid_to),
+        ).issuer_name(
+            ca_cert.subject,
+        ).public_key(
+            req_cert.public_key(),
+        ).sign(ca_key, hashes.SHA256())
+
+        return cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
 
     def verify_chain(self, cert_chain_str_list, cert_str):
         """
@@ -276,13 +295,6 @@ def verify_chain(self, cert_chain_str_list, cert_str):
                 cert_str = tmp_cert_str
             return (True, "Signed certificate is valid and correctly signed by CA " "certificate.")
 
-    def certificate_not_valid_yet(self, cert):
-        starts_to_be_valid = dateutil.parser.parse(cert.get_notBefore())
-        now = pytz.UTC.localize(datetime.datetime.utcnow())
-        if starts_to_be_valid < now:
-            return False
-        return True
-
     def verify(self, signing_cert_str, cert_str):
         """
         Verifies if a certificate is valid and signed by a given certificate.
@@ -303,34 +315,34 @@ def verify(self, signing_cert_str, cert_str):
                                  Message = Why the validation failed.
         """
         try:
-            ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, signing_cert_str)
-            cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_str)
-
-            if self.certificate_not_valid_yet(ca_cert):
+            if isinstance(signing_cert_str, str):
+                signing_cert_str = signing_cert_str.encode("utf-8")
+            if isinstance(cert_str, str):
+                cert_str = cert_str.encode("utf-8")
+            ca_cert = x509.load_pem_x509_certificate(signing_cert_str)
+            cert = x509.load_pem_x509_certificate(cert_str)
+            now = datetime.datetime.now(datetime.UTC)
+
+            if ca_cert.not_valid_before_utc >= now:
                 return False, "CA certificate is not valid yet."
 
-            if ca_cert.has_expired() == 1:
+            if ca_cert.not_valid_after_utc < now:
                 return False, "CA certificate is expired."
 
-            if cert.has_expired() == 1:
+            if cert.not_valid_after_utc < now:
                 return False, "The signed certificate is expired."
 
-            if self.certificate_not_valid_yet(cert):
+            if cert.not_valid_before_utc >= now:
                 return False, "The signed certificate is not valid yet."
 
-            if ca_cert.get_subject().CN == cert.get_subject().CN:
+            if ca_cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) == \
+               cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME):
                 return False, ("CN may not be equal for CA certificate and the " "signed certificate.")
 
-            cert_algorithm = cert.get_signature_algorithm()
-            cert_algorithm = cert_algorithm.decode("ascii")
-            cert_str = cert_str.encode("ascii")
-
-            cert_crypto = saml2.cryptography.pki.load_pem_x509_certificate(cert_str)
-
             try:
-                crypto.verify(ca_cert, cert_crypto.signature, cert_crypto.tbs_certificate_bytes, cert_algorithm)
+                cert.verify_directly_issued_by(ca_cert)
                 return True, "Signed certificate is valid and correctly signed by CA certificate."
-            except crypto.Error as e:
+            except (ValueError, TypeError, InvalidSignature) as e:
                 return False, f"Certificate is incorrectly signed: {str(e)}"
         except Exception as e:
             return False, f"Certificate is not valid for an unknown reason. {str(e)}"
@@ -352,8 +364,14 @@ def read_cert_from_file(cert_file, cert_type="pem"):
         data = fp.read()
 
     try:
-        cert = saml2.cryptography.pki.load_x509_certificate(data, cert_type)
-        pem_data = saml2.cryptography.pki.get_public_bytes_from_cert(cert)
+        cert = None
+        if cert_type == "pem":
+            cert = x509.load_pem_x509_certificate(data)
+        elif cert_type == "der":
+            cert = x509.load_der_x509_certificate(data)
+        else:
+            raise ValueError(f"cert-type {cert_type} not supported")
+        pem_data = cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
     except Exception as e:
         raise CertificateError(e)
 
diff --git a/src/saml2/sigver.py b/src/saml2/sigver.py
index f3af1ec99..98d11b1d1 100644
--- a/src/saml2/sigver.py
+++ b/src/saml2/sigver.py
@@ -28,7 +28,7 @@
 
 from urllib import parse
 
-from OpenSSL import crypto
+from cryptography import x509
 import pytz
 
 from saml2 import ExtensionElement
@@ -383,14 +383,14 @@ def active_cert(key):
     """
     try:
         cert_str = pem_format(key)
-        cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_str)
+        cert = x509.load_pem_x509_certificate(cert_str)
     except AttributeError:
         return False
 
-    now = pytz.UTC.localize(datetime.datetime.utcnow())
-    valid_from = dateutil.parser.parse(cert.get_notBefore())
-    valid_to = dateutil.parser.parse(cert.get_notAfter())
-    active = not cert.has_expired() and valid_from <= now < valid_to
+    now = datetime.datetime.now(datetime.UTC)
+    valid_from = cert.not_valid_before_utc
+    valid_to = cert.not_valid_after_utc
+    active = valid_from <= now < valid_to
     return active
 
 
openSUSE Build Service is sponsored by