Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pysaml2 for openSUSE:Factory checked in at 2025-02-28 17:39:27 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pysaml2 (Old) and /work/SRC/openSUSE:Factory/.python-pysaml2.new.19136 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pysaml2" Fri Feb 28 17:39:27 2025 rev:33 rq:1249137 version:7.5.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pysaml2/python-pysaml2.changes 2024-10-29 14:37:27.979280983 +0100 +++ /work/SRC/openSUSE:Factory/.python-pysaml2.new.19136/python-pysaml2.changes 2025-02-28 17:40:48.288297549 +0100 @@ -1,0 +2,11 @@ +Fri Feb 28 04:15:23 UTC 2025 - Nico Krapp <[email protected]> + +- Update to 7.5.2 + * Include the XSD of the XML Encryption Syntax and Processing + Version 1.1 to the schema validator +- Update to 7.5.1 + * deps: restrict pyOpenSSL up to v24.2.1 until it is replaced + * deps: update dependncies for the lockfile and examples +- add use-cryptography.patch to fix tests + +------------------------------------------------------------------- Old: ---- v7.5.0.tar.gz New: ---- use-cryptography.patch v7.5.2.tar.gz BETA DEBUG BEGIN: New: * deps: update dependncies for the lockfile and examples - add use-cryptography.patch to fix tests BETA DEBUG END: ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pysaml2.spec ++++++ --- /var/tmp/diff_new_pack.l326UJ/_old 2025-02-28 17:40:49.184335049 +0100 +++ /var/tmp/diff_new_pack.l326UJ/_new 2025-02-28 17:40:49.188335216 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-pysaml2 # -# Copyright (c) 2024 SUSE LLC +# Copyright (c) 2025 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,20 +19,21 @@ %global modname pysaml2 %{?sle15_python_module_pythons} Name: python-pysaml2 -Version: 7.5.0 +Version: 7.5.2 Release: 0 Summary: Python implementation of SAML Version 2 to be used in a WSGI environment License: Apache-2.0 URL: https://github.com/IdentityPython/pysaml2 Source: https://github.com/IdentityPython/pysaml2/archive/v%{version}.tar.gz +# PATCH-FIX-UPSTREAM use-cryptography.patch https://github.com/IdentityPython/pysaml2/issues/879 +Patch0: use-cryptography.patch BuildRequires: %{python_module Paste} -BuildRequires: %{python_module cryptography >= 3.1} +BuildRequires: %{python_module cryptography >= 40.0} BuildRequires: %{python_module dbm} BuildRequires: %{python_module defusedxml} BuildRequires: %{python_module importlib-resources} BuildRequires: %{python_module pip} BuildRequires: %{python_module poetry-core} -BuildRequires: %{python_module pyOpenSSL} BuildRequires: %{python_module pymongo >= 3.5} BuildRequires: %{python_module pytest} BuildRequires: %{python_module python-dateutil} @@ -40,7 +41,7 @@ BuildRequires: %{python_module requests >= 1.0.0} BuildRequires: %{python_module responses} BuildRequires: %{python_module setuptools} -BuildRequires: %{python_module xmlschema >= 1.2.1} +BuildRequires: %{python_module xmlschema >= 2} BuildRequires: %{python_module zope.interface} BuildRequires: fdupes # This is needed as xmlsec itself does not pull any backend by default @@ -95,10 +96,11 @@ sed -i 's:import mock:from unittest import mock:' tests/test_41_response.py sed -i 's:mock.mock:unittest.mock:' tests/test_52_default_sign_alg.py # Excluded tests for i586 gh#IdentityPython/pysaml2#682 and gh#IdentityPython/pysaml2#759 +# Exclude broken namespace test (https://github.com/IdentityPython/pysaml2/issues/921) %ifarch %{ix86} -%pytest -k "not (test_assertion_consumer_service or test_swamid_sp or test_swamid_idp or test_other_response or test_mta or test_unknown_subject or test_filter_ava_registration_authority_1)" tests +%pytest -k "not (test_namespace_processing or test_assertion_consumer_service or test_swamid_sp or test_swamid_idp or test_other_response or test_mta or test_unknown_subject or test_filter_ava_registration_authority_1)" tests %else -%pytest tests +%pytest -k "not test_namespace_processing" tests %endif %post @@ -115,5 +117,5 @@ %python_alternative %{_bindir}/mdexport %python_alternative %{_bindir}/merge_metadata %{python_sitelib}/saml2 -%{python_sitelib}/pysaml2-%{version}*-info +%{python_sitelib}/pysaml2-%{version}.dist-info ++++++ use-cryptography.patch ++++++ >From 930a652a240c8cd1489429a7d70cf5fa7ef1606a Mon Sep 17 00:00:00 2001 From: Patrick Rauscher <[email protected]> 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 ++++++ v7.5.0.tar.gz -> v7.5.2.tar.gz ++++++ ++++ 2671 lines of diff (skipped)
