Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-signxml for openSUSE:Factory checked in at 2026-07-06 12:35:08 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-signxml (Old) and /work/SRC/openSUSE:Factory/.python-signxml.new.1982 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-signxml" Mon Jul 6 12:35:08 2026 rev:4 rq:1363955 version:5.1.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-signxml/python-signxml.changes 2026-03-04 21:09:02.709938301 +0100 +++ /work/SRC/openSUSE:Factory/.python-signxml.new.1982/python-signxml.changes 2026-07-06 12:37:43.338644063 +0200 @@ -1,0 +2,14 @@ +Mon Jul 6 06:03:21 UTC 2026 - Dirk Müller <[email protected]> + +- update to 5.1.0: + * Implement point-in-time verification + * xades: fix CertDigest handling + * Confirm certs passed to validate are X.509 formatted + * Clarify location parameter syntax + * Increase minimum cryptography version to 45 + * X.509 certificate valitation now requires the provided + certificate to list digital signature as a key usage. + * fix rst formatting + * Support setting certificate validation policy + +------------------------------------------------------------------- Old: ---- signxml-4.4.0.tar.gz New: ---- signxml-5.1.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-signxml.spec ++++++ --- /var/tmp/diff_new_pack.2lNZpC/_old 2026-07-06 12:37:43.882662966 +0200 +++ /var/tmp/diff_new_pack.2lNZpC/_new 2026-07-06 12:37:43.886663104 +0200 @@ -17,7 +17,7 @@ Name: python-signxml -Version: 4.4.0 +Version: 5.1.0 Release: 0 Summary: Python XML Signature and XAdES library License: Apache-2.0 ++++++ signxml-4.4.0.tar.gz -> signxml-5.1.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/signxml-4.4.0/.github/workflows/ci.yml new/signxml-5.1.0/.github/workflows/ci.yml --- old/signxml-4.4.0/.github/workflows/ci.yml 2020-02-02 01:00:00.000000000 +0100 +++ new/signxml-5.1.0/.github/workflows/ci.yml 2020-02-02 01:00:00.000000000 +0100 @@ -8,11 +8,11 @@ strategy: max-parallel: 8 matrix: - os: [ubuntu-22.04, ubuntu-latest, macos-14, macos-latest] + os: [ubuntu-22.04, ubuntu-latest, macos-15, macos-latest] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: ${{matrix.python-version}} - run: | @@ -26,13 +26,13 @@ isort: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 - - uses: isort/[email protected] + - uses: actions/checkout@v6 + - uses: isort/[email protected] ruff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: astral-sh/ruff-action@v1 - - uses: astral-sh/ruff-action@v1 + - uses: actions/checkout@v6 + - uses: astral-sh/[email protected] + - uses: astral-sh/[email protected] with: args: "format --check" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/signxml-4.4.0/Changes.rst new/signxml-5.1.0/Changes.rst --- old/signxml-4.4.0/Changes.rst 2020-02-02 01:00:00.000000000 +0100 +++ new/signxml-5.1.0/Changes.rst 2020-02-02 01:00:00.000000000 +0100 @@ -1,3 +1,32 @@ +Changes for v5.1.0 (2026-07-04) +=============================== + +- Implement point-in-time verification +- xades: fix CertDigest handling +- Confirm certs passed to validate are X.509 formatted +- Clarify location parameter syntax + +Changes for v5.0.1 (2026-06-23) +=============================== + +- Increase minimum cryptography version to 45 (#295) + +Changes for v5.0.0 (2026-06-20) +=============================== + +- X.509 certificate valitation now requires the provided certificate to + list digital signature as a key usage. + +Changes for v4.5.1 (2026-06-13) +=============================== + +- fix rst formatting + +Changes for v4.5.0 (2026-06-13) +=============================== + +- Support setting certificate validation policy + Changes for v4.4.0 (2026-03-01) =============================== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/signxml-4.4.0/PKG-INFO new/signxml-5.1.0/PKG-INFO --- old/signxml-4.4.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 +++ new/signxml-5.1.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: signxml -Version: 4.4.0 +Version: 5.1.0 Summary: Python XML Signature and XAdES library Project-URL: Homepage, https://github.com/XML-Security/signxml Project-URL: Documentation, https://xml-security.github.io/signxml/ @@ -32,7 +32,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.9 Requires-Dist: certifi>=2023.11.17 -Requires-Dist: cryptography>=43 +Requires-Dist: cryptography>=45 Requires-Dist: lxml<7,>=5.2.1 Provides-Extra: test Requires-Dist: build; extra == 'test' @@ -95,7 +95,7 @@ To make this example self-sufficient for test purposes: - Generate a test certificate and key using - ``openssl req -x509 -nodes -subj "/CN=test" -days 1 -newkey rsa -keyout privkey.pem -out cert.pem`` + ``openssl req -x509 -nodes -subj "/CN=test" -days 1 -newkey rsa -keyout privkey.pem -out cert.pem -addext "keyUsage = digitalSignature"`` (run ``apt-get install openssl``, ``yum install openssl``, or ``brew install openssl`` if the ``openssl`` executable is not found). - Pass the ``x509_cert=cert`` keyword argument to ``XMLVerifier.verify()``. (In production, ensure this is replaced with @@ -145,6 +145,17 @@ config = SignatureConfiguration(location="./") XMLVerifier(...).verify(..., expect_config=config) + The ``location`` value describes the expected parent path for the ``ds:Signature`` element. SignXML + appends ``ds:Signature`` to this path internally. For example, if a SAML ``Response`` root contains a + signed ``Assertion`` child, configure verification like this: + + .. code-block:: python + + config = SignatureConfiguration( + location="./{urn:oasis:names:tc:SAML:2.0:assertion}Assertion/" + ) + XMLVerifier().verify(response_xml, x509_cert=cert, expect_config=config) + **Recommended reading:** `W3C XML Signature Best Practices for Applications <http://www.w3.org/TR/xmldsig-bestpractices/#practices-applications>`_, `On Breaking SAML: Be Whoever You Want to Be <https://www.usenix.org/system/files/conference/usenixsecurity12/sec12-final91.pdf>`_, `Duo Finds SAML Vulnerabilities @@ -162,6 +173,33 @@ subject name that must be in the signing X.509 certificate given by the signature (verified as if it were a domain name), or ``ca_pem_file`` to give a custom CA. + To verify a signature at a specific point in time, such as an archived signature whose certificate has since + expired, configure ``verification_time``. The configured time is used for X.509 certificate validity checks, + including certificate chain validation and certificates supplied with ``x509_cert``: + + .. code-block:: python + + from datetime import datetime, timezone + from signxml import SignatureConfiguration, XMLVerifier + + config = SignatureConfiguration( + verification_time=datetime(2018, 5, 28, 17, 0, tzinfo=timezone.utc) + ) + verified_data = XMLVerifier().verify(data, expect_config=config).signed_xml + +Relaxing digital signature key usage extension validation for certificates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +When verifying X.509 certificate chains, SignXML's default end-entity certificate policy requires the +``KeyUsage`` extension to be present and to allow ``digitalSignature``. To accept certificates without that +key usage, pass a custom end-entity certificate policy that does not require it: + +.. code-block:: python + + ee_policy = x509.verification.ExtensionPolicy.permit_all() + + XMLVerifier(...).verify(..., ee_policy=ee_policy) + + XML signature construction methods: enveloped, detached, enveloping ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The XML Signature specification defines three ways to compose a signature with the data being signed: enveloped, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/signxml-4.4.0/README.rst new/signxml-5.1.0/README.rst --- old/signxml-4.4.0/README.rst 2020-02-02 01:00:00.000000000 +0100 +++ new/signxml-5.1.0/README.rst 2020-02-02 01:00:00.000000000 +0100 @@ -50,7 +50,7 @@ To make this example self-sufficient for test purposes: - Generate a test certificate and key using - ``openssl req -x509 -nodes -subj "/CN=test" -days 1 -newkey rsa -keyout privkey.pem -out cert.pem`` + ``openssl req -x509 -nodes -subj "/CN=test" -days 1 -newkey rsa -keyout privkey.pem -out cert.pem -addext "keyUsage = digitalSignature"`` (run ``apt-get install openssl``, ``yum install openssl``, or ``brew install openssl`` if the ``openssl`` executable is not found). - Pass the ``x509_cert=cert`` keyword argument to ``XMLVerifier.verify()``. (In production, ensure this is replaced with @@ -100,6 +100,17 @@ config = SignatureConfiguration(location="./") XMLVerifier(...).verify(..., expect_config=config) + The ``location`` value describes the expected parent path for the ``ds:Signature`` element. SignXML + appends ``ds:Signature`` to this path internally. For example, if a SAML ``Response`` root contains a + signed ``Assertion`` child, configure verification like this: + + .. code-block:: python + + config = SignatureConfiguration( + location="./{urn:oasis:names:tc:SAML:2.0:assertion}Assertion/" + ) + XMLVerifier().verify(response_xml, x509_cert=cert, expect_config=config) + **Recommended reading:** `W3C XML Signature Best Practices for Applications <http://www.w3.org/TR/xmldsig-bestpractices/#practices-applications>`_, `On Breaking SAML: Be Whoever You Want to Be <https://www.usenix.org/system/files/conference/usenixsecurity12/sec12-final91.pdf>`_, `Duo Finds SAML Vulnerabilities @@ -117,6 +128,33 @@ subject name that must be in the signing X.509 certificate given by the signature (verified as if it were a domain name), or ``ca_pem_file`` to give a custom CA. + To verify a signature at a specific point in time, such as an archived signature whose certificate has since + expired, configure ``verification_time``. The configured time is used for X.509 certificate validity checks, + including certificate chain validation and certificates supplied with ``x509_cert``: + + .. code-block:: python + + from datetime import datetime, timezone + from signxml import SignatureConfiguration, XMLVerifier + + config = SignatureConfiguration( + verification_time=datetime(2018, 5, 28, 17, 0, tzinfo=timezone.utc) + ) + verified_data = XMLVerifier().verify(data, expect_config=config).signed_xml + +Relaxing digital signature key usage extension validation for certificates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +When verifying X.509 certificate chains, SignXML's default end-entity certificate policy requires the +``KeyUsage`` extension to be present and to allow ``digitalSignature``. To accept certificates without that +key usage, pass a custom end-entity certificate policy that does not require it: + +.. code-block:: python + + ee_policy = x509.verification.ExtensionPolicy.permit_all() + + XMLVerifier(...).verify(..., ee_policy=ee_policy) + + XML signature construction methods: enveloped, detached, enveloping ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The XML Signature specification defines three ways to compose a signature with the data being signed: enveloped, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/signxml-4.4.0/pyproject.toml new/signxml-5.1.0/pyproject.toml --- old/signxml-4.4.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 +++ new/signxml-5.1.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 @@ -27,7 +27,7 @@ ] dependencies = [ "lxml >= 5.2.1, < 7", # Ubuntu 24.04 LTS - "cryptography >= 43", # Required to support client certificate validation + "cryptography >= 45", # Required to support Extension Policy "certifi >= 2023.11.17", # Ubuntu 24.04 LTS # "tsp-client >= 0.1.3", ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/signxml-4.4.0/signxml/__pyinstaller/hook-signxml.py new/signxml-5.1.0/signxml/__pyinstaller/hook-signxml.py --- old/signxml-4.4.0/signxml/__pyinstaller/hook-signxml.py 2020-02-02 01:00:00.000000000 +0100 +++ new/signxml-5.1.0/signxml/__pyinstaller/hook-signxml.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,5 +1,5 @@ """Hook for pyinstaller to include the files are signxml/schemas/* into the final build.""" -from PyInstaller.utils.hooks import collect_data_files # type: ignore +from PyInstaller.utils.hooks import collect_data_files datas = collect_data_files("signxml", excludes=["__pyinstaller"]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/signxml-4.4.0/signxml/algorithms.py new/signxml-5.1.0/signxml/algorithms.py --- old/signxml-4.4.0/signxml/algorithms.py 2020-02-02 01:00:00.000000000 +0100 +++ new/signxml-5.1.0/signxml/algorithms.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable, Dict, Type, Union +from typing import Callable, Dict, Iterable, Type, Union, cast from cryptography.hazmat.primitives import hashes @@ -37,7 +37,7 @@ class FragmentLookupMixin: @classmethod def from_fragment(cls, fragment): - for i in cls: # type: ignore[attr-defined] + for i in cast(Iterable[Enum], cls): if i.value.endswith("#" + fragment): return i else: @@ -50,7 +50,8 @@ raise InvalidInput(f"Unrecognized {cls.__name__}: {value}") def __repr__(self): - return f"{self.__class__.__name__}.{self.name}" # type: ignore[attr-defined] + enum_self = cast(Enum, self) + return f"{self.__class__.__name__}.{enum_self.name}" class DigestAlgorithm(FragmentLookupMixin, InvalidInputErrorMixin, Enum): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/signxml-4.4.0/signxml/processor.py new/signxml-5.1.0/signxml/processor.py --- old/signxml-4.4.0/signxml/processor.py 2020-02-02 01:00:00.000000000 +0100 +++ new/signxml-5.1.0/signxml/processor.py 2020-02-02 01:00:00.000000000 +0100 @@ -2,7 +2,7 @@ import logging import threading from functools import lru_cache -from typing import Any, List, Tuple +from typing import Any, Dict, List, Tuple from xml.etree import ElementTree as stdlibElementTree from cryptography.hazmat.primitives.asymmetric import ec @@ -27,15 +27,15 @@ class XMLProcessor: _schemas: List[Any] = [] + _schemas_lock = threading.Lock() schema_files: List[Any] = [] _default_parser, _parser = None, None @classmethod def schemas(cls): - with threading.Lock(): - if len(cls._schemas) == 0: - for schema_file in cls.schema_files: - cls._schemas.append(get_schema(schema_file)) + with cls._schemas_lock: + if "_schemas" not in cls.__dict__: + cls._schemas = [get_schema(schema_file) for schema_file in cls.schema_files] return cls._schemas @property @@ -85,14 +85,14 @@ # "urn:oid:1.3.132.0.36": ec.SECT409K1, # "urn:oid:1.3.132.0.37": ec.SECT409R1, # "urn:oid:1.3.132.0.38": ec.SECT571K1 - known_ecdsa_curves = { - "urn:oid:1.2.840.10045.3.1.7": ec.SECP256R1, - "urn:oid:1.3.132.0.34": ec.SECP384R1, - "urn:oid:1.3.132.0.35": ec.SECP521R1, - "urn:oid:1.2.840.10045.3.1.1": ec.SECP192R1, - "urn:oid:1.3.132.0.33": ec.SECP224R1, + known_ecdsa_curves: Dict[str, ec.EllipticCurve] = { + "urn:oid:1.2.840.10045.3.1.7": ec.SECP256R1(), + "urn:oid:1.3.132.0.34": ec.SECP384R1(), + "urn:oid:1.3.132.0.35": ec.SECP521R1(), + "urn:oid:1.2.840.10045.3.1.1": ec.SECP192R1(), + "urn:oid:1.3.132.0.33": ec.SECP224R1(), } - known_ecdsa_curve_oids = {ec().name: oid for oid, ec in known_ecdsa_curves.items()} # type: ignore[abstract] + known_ecdsa_curve_oids = {curve.name: oid for oid, curve in known_ecdsa_curves.items()} excise_empty_xmlns_declarations = False @@ -108,10 +108,11 @@ namespace = "ds" if ":" in query: namespace, _, query = query.partition(":") - result = element.find(f"{xpath}{namespace}:{query}", namespaces=namespaces) + expression = f"{xpath}{namespace}:{query}" + result = element.find(expression, namespaces=namespaces) if require and result is None: - raise InvalidInput(f"Expected to find XML element {query} in {element.tag}") + raise InvalidInput(f"Expected to find XML element {query} in {element.tag} using XPath {expression}") return result def _findall(self, element, query, xpath=""): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/signxml-4.4.0/signxml/signer.py new/signxml-5.1.0/signxml/signer.py --- old/signxml-4.4.0/signxml/signer.py 2020-02-02 01:00:00.000000000 +0100 +++ new/signxml-5.1.0/signxml/signer.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ from base64 import b64encode from dataclasses import dataclass, replace -from typing import List, Optional, Union +from typing import Dict, List, Optional, Union, cast from cryptography import x509 from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa, utils @@ -21,6 +21,7 @@ from .util import ( SigningSettings, _remove_sig, + add_pem_header, bits_to_bytes_unit, ds_tag, dsig11_tag, @@ -113,7 +114,7 @@ self.digest_alg = DigestAlgorithm(digest_algorithm) self.check_deprecated_methods() self.c14n_alg = CanonicalizationMethod(c14n_algorithm) - self.namespaces = dict(ds=namespaces.ds) + self.namespaces: Dict[str, str] = {"ds": namespaces.ds} self._parser = None self.signature_annotators = [self._add_key_info] @@ -127,7 +128,8 @@ See https://github.com/XML-Security/signxml/issues/275 """ - if None in self.namespaces and self.namespaces[None] == namespaces.ds: # type:ignore[index] + namespaces_map = cast(Dict[Optional[str], str], self.namespaces) + if namespaces_map.get(None) == namespaces.ds: return QName(None, tag) return ds_tag(tag) @@ -216,12 +218,7 @@ if id_attribute is not None: self.id_attributes = (id_attribute,) - if isinstance(cert, (str, bytes)): - cert_chain = list(iterate_pem(cert)) - if len(cert_chain) == 0: - raise InvalidInput("No PEM-encoded certificates found in string cert input data") - else: - cert_chain = cert # type:ignore[assignment] + cert_chain = self._get_cert_chain(cert) input_references = self._preprocess_reference_uri(reference_uri) @@ -265,7 +262,11 @@ signed_info_node, algorithm=self.c14n_alg, inclusive_ns_prefixes=inclusive_ns_prefixes ) if self.sign_alg.name.startswith("HMAC_"): - signer = HMAC(key=key, algorithm=digest_algorithm_implementations[self.sign_alg]()) # type:ignore[arg-type] + if isinstance(key, str): + key = key.encode() + elif not isinstance(key, bytes): + raise InvalidInput('Parameter "key" must be bytes when signing with HMAC') + signer = HMAC(key=key, algorithm=digest_algorithm_implementations[self.sign_alg]()) signer.update(signed_info_c14n) signature_value_node.text = b64encode(signer.finalize()).decode() sig_root.append(signature_value_node) @@ -304,6 +305,30 @@ return doc_root if self.construction_method == SignatureConstructionMethod.enveloped else sig_root + def _get_cert_chain(self, cert): + if isinstance(cert, (str, bytes)): + cert_chain = list(iterate_pem(cert)) + if len(cert_chain) == 0: + raise InvalidInput("No PEM-encoded certificates found in string cert input data") + elif cert is None: + return None + else: + cert_chain = list(cert) + if len(cert_chain) == 0: + raise InvalidInput("No certificates found in cert input data") + + for certificate in cert_chain: + if isinstance(certificate, x509.Certificate): + continue + if not isinstance(certificate, (str, bytes)): + raise InvalidInput("Expected cert input data to be a X.509 certificate or PEM-encoded certificate") + try: + x509.load_pem_x509_certificate(add_pem_header(certificate)) + except ValueError as exc: + raise InvalidInput("Invalid X.509 certificate supplied in cert input data") from exc + + return cert_chain + def _preprocess_reference_uri(self, reference_uris): if reference_uris is None: return None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/signxml-4.4.0/signxml/util/__init__.py new/signxml-5.1.0/signxml/util/__init__.py --- old/signxml-4.4.0/signxml/util/__init__.py 2020-02-02 01:00:00.000000000 +0100 +++ new/signxml-5.1.0/signxml/util/__init__.py 2020-02-02 01:00:00.000000000 +0100 @@ -228,12 +228,40 @@ contact SignXML maintainers. """ - def __init__(self, ca_pem_file=None, verification_time=None): + def __init__(self, ca_pem_file=None, verification_time=None, ee_policy=None, ca_policy=None): if ca_pem_file is None: ca_pem_file = certifi.where() self.ca_pem_file = ca_pem_file self.verification_time = verification_time + if ee_policy is not None: + if not isinstance(ee_policy, x509.verification.ExtensionPolicy): + raise ValueError("ee_policy must be an instance of x509.verification.ExtensionPolicy") + self.ee_policy = ee_policy + else: + self.ee_policy = self.get_default_ee_policy() + + if ca_policy is not None: + if not isinstance(ca_policy, x509.verification.ExtensionPolicy): + raise ValueError("ca_policy must be an instance of x509.verification.ExtensionPolicy") + self.ca_policy = ca_policy + else: + self.ca_policy = self.get_default_ca_policy() + + def validate_key_usage(self, policy, cert, key_usage): + if not key_usage.digital_signature: + raise ValueError("certificate KeyUsage does not allow digitalSignature") + + def get_default_ee_policy(self): + return x509.verification.ExtensionPolicy.permit_all().require_present( + x509.KeyUsage, + x509.verification.Criticality.AGNOSTIC, + self.validate_key_usage, + ) + + def get_default_ca_policy(self): + return x509.verification.ExtensionPolicy.webpki_defaults_ca() + @property def store(self): with open(self.ca_pem_file, "rb") as pems: @@ -244,6 +272,10 @@ def builder(self): builder = x509.verification.PolicyBuilder() builder = builder.store(self.store) + builder = builder.extension_policies( + ee_policy=self.ee_policy, + ca_policy=self.ca_policy, + ) if self.verification_time is not None: builder = builder.time(self.verification_time) return builder diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/signxml-4.4.0/signxml/verifier.py new/signxml-5.1.0/signxml/verifier.py --- old/signxml-4.4.0/signxml/verifier.py 2020-02-02 01:00:00.000000000 +0100 +++ new/signxml-5.1.0/signxml/verifier.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,7 @@ from base64 import b64decode from dataclasses import dataclass, replace -from typing import Callable, FrozenSet, List, Optional, Tuple, Union +from datetime import datetime, timezone +from typing import Any, Callable, FrozenSet, List, Optional, Tuple, Union, cast from warnings import warn import cryptography.exceptions @@ -87,6 +88,12 @@ ``True`` to bypass the check and validate the signature using X509Data only. """ + verification_time: Optional[datetime] = None + """ + The time to use when validating X.509 certificate validity periods. If unset, the current time is used. + Use this to verify signatures with certificates that are expired or not yet valid at the current time. + """ + default_reference_c14n_method: CanonicalizationMethod = CanonicalizationMethod.CANONICAL_XML_1_1 """ The default canonicalization method to use for referenced data structures, if there is no canonicalization @@ -158,8 +165,11 @@ key_data = b64decode(public_key.text)[1:] x = bytes_to_long(key_data[: len(key_data) // 2]) y = bytes_to_long(key_data[len(key_data) // 2 :]) - curve_class = self.known_ecdsa_curves[named_curve.get("URI")] - ecpn = ec.EllipticCurvePublicNumbers(x=x, y=y, curve=curve_class()) # type: ignore[abstract] + curve_uri = named_curve.get("URI") + if curve_uri is None: + raise InvalidInput("NamedCurve element is missing a URI attribute") + curve = self.known_ecdsa_curves[curve_uri] + ecpn = ec.EllipticCurvePublicNumbers(x=x, y=y, curve=curve) key = ecpn.public_key() elif not isinstance(key, ec.EllipticCurvePublicKey): raise InvalidInput("DER encoded key value does not match specified signature algorithm") @@ -257,8 +267,50 @@ payload = self._c14n(payload, algorithm=self.config.default_reference_c14n_method) return payload - def get_cert_chain_verifier(self, ca_pem_file): - return X509CertChainVerifier(ca_pem_file=ca_pem_file) + def get_cert_chain_verifier(self, ca_pem_file, ee_policy, ca_policy): + return X509CertChainVerifier( + ca_pem_file=ca_pem_file, + verification_time=self._get_cert_verification_time(), + ee_policy=ee_policy, + ca_policy=ca_policy, + ) + + def _get_cert_verification_time(self): + verification_time = self.config.verification_time or datetime.now(tz=timezone.utc) + if verification_time.tzinfo is None: + return verification_time.replace(tzinfo=timezone.utc) + return verification_time.astimezone(timezone.utc) + + def _get_cert_description(self, cert, cert_source): + try: + subject = cert.subject.rfc4514_string() + except ValueError: + subject = "<invalid subject>" + return f"{cert_source} (subject={subject}, serial={cert.serial_number})" + + def _get_cert_validity_error(self, cert, cert_source): + cert_description = self._get_cert_description(cert, cert_source) + verification_time = self._get_cert_verification_time() + if verification_time < cert.not_valid_before_utc: + return ( + f"{cert_description}: certificate is not yet valid at validation time " + f"{verification_time.isoformat()}; certificate is not valid before " + f"{cert.not_valid_before_utc.isoformat()}. Use SignatureConfiguration(verification_time=...) " + "to verify at a different point in time." + ) + if verification_time > cert.not_valid_after_utc: + return ( + f"{cert_description}: certificate has expired at validation time " + f"{verification_time.isoformat()}; certificate is not valid after " + f"{cert.not_valid_after_utc.isoformat()}. Use SignatureConfiguration(verification_time=...) " + "if you must verify with an expired cert." + ) + return None + + def _check_cert_validity(self, cert, cert_source): + msg = self._get_cert_validity_error(cert, cert_source) + if msg is not None: + raise InvalidCertificate(msg) def _match_key_values(self, key_value, der_encoded_key_value, signing_cert, signature_alg): if self.config.ignore_ambiguous_key_info is True: @@ -309,6 +361,8 @@ uri_resolver: Optional[Callable] = None, id_attribute: Optional[str] = None, expect_config: SignatureConfiguration = SignatureConfiguration(), + ee_policy: Optional[x509.verification.ExtensionPolicy] = None, + ca_policy: Optional[x509.verification.ExtensionPolicy] = None, **deprecated_kwargs, ) -> Union[VerifyResult, List[VerifyResult]]: """ @@ -334,6 +388,17 @@ config = SignatureConfiguration(location="./") XMLVerifier(...).verify(..., expect_config=config) + The ``location`` value describes the expected parent path for the ``ds:Signature`` element. SignXML + appends ``ds:Signature`` to this path internally. For example, if a SAML ``Response`` root contains a + signed ``Assertion`` child, configure verification like this: + + .. code-block:: python + + config = SignatureConfiguration( + location="./{urn:oasis:names:tc:SAML:2.0:assertion}Assertion/" + ) + XMLVerifier().verify(response_xml, x509_cert=cert, expect_config=config) + Depending on the canonicalization method used by the signature, comments in the XML data may not be subject to signing, so may need to be untrusted. If so, they are excised from the return value of ``verify()``. @@ -347,7 +412,8 @@ ``x509_cert`` argument to specify a certificate that was pre-shared out-of-band (e.g. via SAML metadata, as shown in :ref:`Verifying SAML assertions <verifying-saml-assertions>`), or ``cert_subject_name`` to specify a subject name that must be in the signing X.509 certificate given by the signature (verified as if it were a - domain name), or ``ca_pem_file`` to give a custom CA. + domain name), or ``ca_pem_file`` to give a custom CA. If you need to verify a signature at the time it was + created rather than the current time, pass ``expect_config=SignatureConfiguration(verification_time=...)``. :param data: Signature data to verify :type data: String, file-like object, or XML ElementTree Element API compatible object @@ -383,6 +449,10 @@ :param expect_config: Expected signature configuration. Pass a :class:`SignatureConfiguration` object to describe expected properties of the verified signature. Signatures with unexpected configurations will fail validation. + :param ee_policy: + Custom x509.verification.ExtensionPolicy to use for validating the end-entity certificate. + :param ca_policy: + Custom x509.verification.ExtensionPolicy to use for validating the CA certificate. :param deprecated_kwargs: Direct application of the parameters **require_x509**, **expect_references**, and **ignore_ambiguous_key_info** is deprecated. Use **expect_config** instead. @@ -412,7 +482,7 @@ "SignXML received a PyOpenSSL object as x509_cert input. Please pass a Cryptography.X509 object instead.", DeprecationWarning, ) - x509_cert = x509_cert.to_cryptography() # type: ignore[union-attr] + x509_cert = cast(Any, x509_cert).to_cryptography() self.x509_cert = x509_cert @@ -449,6 +519,7 @@ if x509_data is None: raise InvalidInput("Expected a X.509 certificate based signature") certs = [cert.text for cert in self._findall(x509_data, "X509Certificate")] + cert_chain_source = "X509Data/X509Certificate" if len(certs) == 0: x509_iss = x509_data.find("ds:X509IssuerSerial/ds:X509IssuerName", namespaces=namespaces) x509_sn = x509_data.find("ds:X509IssuerSerial/ds:X509SerialNumber", namespaces=namespaces) @@ -461,6 +532,7 @@ ) if len(cert_chain) == 0: raise InvalidCertificate("No certificate found for given X509 data") + cert_chain_source = "cert_resolver result" if not all(isinstance(c, x509.Certificate) for c in cert_chain): cert_chain = [x509.load_pem_x509_certificate(add_pem_header(cert)) for cert in cert_chain] else: @@ -470,13 +542,26 @@ else: cert_chain = [x509.load_pem_x509_certificate(add_pem_header(cert)) for cert in certs] - cert_verifier = self.get_cert_chain_verifier(ca_pem_file=ca_pem_file) + cert_verifier = self.get_cert_chain_verifier( + ca_pem_file=ca_pem_file, ee_policy=ee_policy, ca_policy=ca_policy + ) - signing_cert = cert_verifier.verify(cert_chain) + try: + signing_cert = cert_verifier.verify(cert_chain) + except InvalidCertificate as exc: + if "valid at validation time" in str(exc): + for index, cert in enumerate(cert_chain): + self._check_cert_validity(cert, f"{cert_chain_source} certificate at index {index}") + raise + signing_cert_source = f"verified {cert_chain_source} signing certificate" elif isinstance(self.x509_cert, x509.Certificate): signing_cert = self.x509_cert + signing_cert_source = "provided x509_cert" else: signing_cert = x509.load_pem_x509_certificate(add_pem_header(self.x509_cert)) + signing_cert_source = "provided x509_cert" + + self._check_cert_validity(signing_cert, signing_cert_source) if cert_subject_name is not None: cn_oid = x509.oid.NameOID.COMMON_NAME @@ -602,13 +687,16 @@ key_data = b64decode(pub_key.text)[1:] x = bytes_to_long(key_data[: len(key_data) // 2]) y = bytes_to_long(key_data[len(key_data) // 2 :]) - curve_class = self.known_ecdsa_curves[named_curve.get("URI")] + curve_uri = named_curve.get("URI") + if curve_uri is None: + return False + curve = self.known_ecdsa_curves[curve_uri] pubk_curve = public_key.public_numbers().curve pubk_x = public_key.public_numbers().x pubk_y = public_key.public_numbers().y - return isinstance(pubk_curve, curve_class) and x == pubk_x and y == pubk_y + return pubk_curve.name == curve.name and x == pubk_x and y == pubk_y elif signature_alg.name.startswith("DSA_") and isinstance(public_key, dsa.DSAPublicKey): dsa_key_value = self._find(key_value, "DSAKeyValue") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/signxml-4.4.0/signxml/xades/xades.py new/signxml-5.1.0/signxml/xades/xades.py --- old/signxml-4.4.0/signxml/xades/xades.py 2020-02-02 01:00:00.000000000 +0100 +++ new/signxml-5.1.0/signxml/xades/xades.py 2020-02-02 01:00:00.000000000 +0100 @@ -19,22 +19,38 @@ The main difference with plain XML Signature is that HMAC algorithms are not supported, and SHA1 is deprecated. """ +from __future__ import annotations + import datetime import os import secrets from base64 import b64decode, b64encode -from dataclasses import astuple, dataclass +from dataclasses import dataclass from functools import wraps -from typing import Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast from cryptography import x509 -from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from lxml.etree import SubElement, _Element from .. import SignatureConfiguration, VerifyResult, XMLSignatureProcessor, XMLSigner, XMLVerifier from ..algorithms import DigestAlgorithm, digest_algorithm_implementations from ..exceptions import InvalidDigest, InvalidInput -from ..util import SigningSettings, add_pem_header, ds_tag, namespaces, xades_tag +from ..util import SigningSettings, add_pem_header, ds_tag, ensure_str, namespaces, xades_tag + +if TYPE_CHECKING: + from typing import Callable, Concatenate, ParamSpec + + _P = ParamSpec("_P") + + +def _with_parent_signature( + wrapped: Callable[Concatenate[XMLSigner, _P], _Element], +) -> Callable[[Callable[..., _Element]], Callable[Concatenate[XMLSigner, _P], _Element]]: + def decorator(wrapper: Callable[..., _Element]) -> Callable[Concatenate[XMLSigner, _P], _Element]: + return cast(Any, wraps(wrapped)(wrapper)) + + return decorator @dataclass(frozen=True) @@ -129,8 +145,8 @@ self.data_object_format = data_object_format self.namespaces.update(xades=namespaces.xades) - @wraps(XMLSigner.sign) - def sign(self, data, always_add_key_value: bool = True, **kwargs) -> _Element: # type: ignore[override] + @_with_parent_signature(XMLSigner.sign) + def sign(self, data, always_add_key_value: bool = True, **kwargs: Any) -> _Element: return super().sign(data=data, always_add_key_value=always_add_key_value, **kwargs) def _get_token(self, length=4): @@ -221,11 +237,15 @@ digest_value_node.text = b64encode(cert_digest_sha1_bytes).decode() issuer_serial = SubElement(cert_node_legacy, xades_tag("IssuerSerial"), nsmap=self.namespaces) issuer_name = SubElement(issuer_serial, ds_tag("X509IssuerName"), nsmap=self.namespaces) - issuer_name.text = "C={C},O={O},OU={OU},CN={CN}".format( # type:ignore[str-bytes-safe] - C=loaded_cert.issuer.get_attributes_for_oid(x509.oid.NameOID.COUNTRY_NAME)[0].value, - O=loaded_cert.issuer.get_attributes_for_oid(x509.oid.NameOID.ORGANIZATION_NAME)[0].value, - OU=loaded_cert.issuer.get_attributes_for_oid(x509.oid.NameOID.ORGANIZATIONAL_UNIT_NAME)[0].value, - CN=loaded_cert.issuer.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)[0].value, + issuer_name.text = "C={C},O={O},OU={OU},CN={CN}".format( + C=ensure_str(loaded_cert.issuer.get_attributes_for_oid(x509.oid.NameOID.COUNTRY_NAME)[0].value), + O=ensure_str( + loaded_cert.issuer.get_attributes_for_oid(x509.oid.NameOID.ORGANIZATION_NAME)[0].value + ), + OU=ensure_str( + loaded_cert.issuer.get_attributes_for_oid(x509.oid.NameOID.ORGANIZATIONAL_UNIT_NAME)[0].value + ), + CN=ensure_str(loaded_cert.issuer.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)[0].value), ) serial_number = SubElement(issuer_serial, ds_tag("X509SerialNumber"), nsmap=self.namespaces) serial_number.text = str(loaded_cert.serial_number) @@ -297,37 +317,80 @@ Create a new XAdES Signature Verifier object, which can be used to verify multiple pieces of data. """ + expect_signature_policy: Optional[XAdESSignaturePolicy] + # TODO: document/support SignatureTimeStamp / timestamp attestation # SignatureTimeStamp is required by certain profiles but is an unsigned property def _verify_signing_time(self, verify_result: VerifyResult): pass - def _verify_cert_digest(self, signing_cert_node, expect_cert): - for cert in self._findall(signing_cert_node, "xades:Cert"): + def _get_cert_public_key(self, cert: x509.Certificate) -> bytes: + return cert.public_key().public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo) + + def _add_cert_if_missing(self, certs: List[x509.Certificate], cert: x509.Certificate) -> None: + cert_der = cert.public_bytes(Encoding.DER) + if not any(candidate.public_bytes(Encoding.DER) == cert_der for candidate in certs): + certs.append(cert) + + def _find_signing_cert( + self, verify_result: VerifyResult, certs: List[x509.Certificate] + ) -> Optional[x509.Certificate]: + if self.x509_cert is not None: + if isinstance(self.x509_cert, x509.Certificate): + return self.x509_cert + return x509.load_pem_x509_certificate(add_pem_header(self.x509_cert)) + + for cert in certs: + if self._get_cert_public_key(cert) == verify_result.signature_key: + return cert + return None + + def _verify_cert_digest( + self, + signing_cert_node, + certs: List[x509.Certificate], + signing_cert: Optional[x509.Certificate], + ): + if len(certs) == 0: + raise InvalidInput("Expected to find an X509Certificate element in the signature") + + for cert_idx, cert in enumerate(self._findall(signing_cert_node, "xades:Cert")): cert_digest = self._find(cert, "xades:CertDigest") digest_alg = DigestAlgorithm(self._find(cert_digest, "DigestMethod").get("Algorithm")) digest_value = self._find(cert_digest, "DigestValue") # check spec for specific method of retrieving cert digest_alg_impl = digest_algorithm_implementations[digest_alg]() - if b64decode(digest_value.text) != expect_cert.fingerprint(digest_alg_impl): + digest_value_bytes = b64decode(digest_value.text) + if not any(digest_value_bytes == candidate.fingerprint(digest_alg_impl) for candidate in certs): raise InvalidDigest("Digest mismatch for certificate digest") + if ( + cert_idx == 0 + and signing_cert is not None + and digest_value_bytes != signing_cert.fingerprint(digest_alg_impl) + ): + raise InvalidDigest("Digest mismatch for signing certificate digest") + def _verify_cert_digests(self, verify_result: VerifyResult): - x509_data = verify_result.signature_xml.find("ds:KeyInfo/ds:X509Data", namespaces=namespaces) - cert_from_key_info = x509.load_pem_x509_certificate( - add_pem_header(self._find(x509_data, "X509Certificate").text) - ) + certs = [] + for x509_data in self._findall(verify_result.signature_xml, "ds:KeyInfo/ds:X509Data"): + for cert in self._findall(x509_data, "X509Certificate"): + certs.append(x509.load_pem_x509_certificate(add_pem_header(cert.text))) + signing_cert = self._find_signing_cert(verify_result, certs) + if signing_cert is not None: + self._add_cert_if_missing(certs, signing_cert) + signed_signature_props = self._find(verify_result.signed_xml, "xades:SignedSignatureProperties") - signing_cert = self._find(signed_signature_props, "xades:SigningCertificate", require=False) - signing_cert_v2 = self._find(signed_signature_props, "xades:SigningCertificateV2", require=False) - if signing_cert is None and signing_cert_v2 is None: + signing_cert_node = self._find(signed_signature_props, "xades:SigningCertificate", require=False) + signing_cert_v2_node = self._find(signed_signature_props, "xades:SigningCertificateV2", require=False) + if signing_cert_node is None and signing_cert_v2_node is None: raise InvalidInput("Expected to find XML element xades:SigningCertificate or xades:SigningCertificateV2") - if signing_cert is not None and signing_cert_v2 is not None: + if signing_cert_node is not None and signing_cert_v2_node is not None: raise InvalidInput("Expected to find exactly one of xades:SigningCertificate or xades:SigningCertificateV2") - if signing_cert is not None: - self._verify_cert_digest(signing_cert, expect_cert=cert_from_key_info) - elif signing_cert_v2 is not None: - self._verify_cert_digest(signing_cert_v2, expect_cert=cert_from_key_info) + if signing_cert_node is not None: + self._verify_cert_digest(signing_cert_node, certs=certs, signing_cert=signing_cert) + elif signing_cert_v2_node is not None: + self._verify_cert_digest(signing_cert_v2_node, certs=certs, signing_cert=signing_cert) def _verify_signature_policy(self, verify_result: VerifyResult, expect_signature_policy: XAdESSignaturePolicy): signed_signature_props = self._find(verify_result.signed_xml, "xades:SignedSignatureProperties") @@ -362,14 +425,14 @@ ) return self._find(verify_result.signed_xml, "xades:SignedSignatureProperties") - def verify( # type: ignore[override] + def verify( self, data, *, expect_signature_policy: Optional[XAdESSignaturePolicy] = None, - expect_config: XAdESSignatureConfiguration = XAdESSignatureConfiguration(), - **xml_verifier_args, - ) -> List[XAdESVerifyResult]: + expect_config: SignatureConfiguration = XAdESSignatureConfiguration(), + **xml_verifier_args: Any, + ) -> List[VerifyResult]: """ Verify the XAdES signature supplied in the data and return a list of :class:`XAdESVerifyResult` data structures representing the data signed by the signature, or raise an exception if the signature is not valid. This method @@ -396,12 +459,16 @@ if verify_result.signed_xml is None: continue if verify_result.signed_xml.tag == xades_tag("SignedProperties"): - verify_results[i] = XAdESVerifyResult( # type: ignore[misc] - *astuple(verify_result), signed_properties=self._verify_signed_properties(verify_result) + verify_results[i] = XAdESVerifyResult( + signed_data=verify_result.signed_data, + signed_xml=verify_result.signed_xml, + signature_xml=verify_result.signature_xml, + signature_key=verify_result.signature_key, + signed_properties=self._verify_signed_properties(verify_result), ) break else: raise InvalidInput("Expected to find a xades:SignedProperties element") # TODO: assert all mandatory signed properties are set - return verify_results # type: ignore[return-value] + return verify_results diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/signxml-4.4.0/test/issues/issue_284_expired_cert.xml new/signxml-5.1.0/test/issues/issue_284_expired_cert.xml --- old/signxml-4.4.0/test/issues/issue_284_expired_cert.xml 1970-01-01 01:00:00.000000000 +0100 +++ new/signxml-5.1.0/test/issues/issue_284_expired_cert.xml 2020-02-02 01:00:00.000000000 +0100 @@ -0,0 +1 @@ +<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe Id="NFe41180508187168000160550010020000001020000009" versao="4.00"><ide><cUF>41</cUF><cNF>02000000</cNF><natOp>VENDA DE MERCADORIA ADQ. DE TERCEIRO - PF E PJ NAO CONTRIBUI</natOp><mod>55</mod><serie>1</serie><nNF>2000000</nNF><dhEmi>2018-05-28T17:00:00-03:00</dhEmi><dhSaiEnt>2018-05-28T17:00:00-03:00</dhSaiEnt><tpNF>1</tpNF><idDest>1</idDest><cMunFG>4115200</cMunFG><tpImp>1</tpImp><tpEmis>1</tpEmis><cDV>9</cDV><tpAmb>2</tpAmb><finNFe>1</finNFe><indFinal>1</indFinal><indPres>1</indPres><procEmi>0</procEmi><verProc>TecnoERP - 1.2.3</verProc></ide><emit><CNPJ>08187168000160</CNPJ><xNome>TECNOSPEED & TECNOLOGIA</xNome><xFant>TECNOSPEED & TECNOLOGIA</xFant><enderEmit><xLgr>AVENIDA DUQUE DE CAXIAS</xLgr><nro>882</nro><xBairro>ZONA 01</xBairro><cMun>4115200</cMun><xMun>MARINGA</xMun><UF>PR</UF><CEP>87020025</CEP><cPais>1058</cPais><xPais>BRASIL</xPais><fone>4430379500</fone></enderEmit><IE>9044016688</IE><CRT>3</CRT></emit><de st><CNPJ>08187168000160</CNPJ><xNome>NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL</xNome><enderDest><xLgr>AVENIDA DUQUE DE CAXIAS</xLgr><nro>882</nro><xBairro>ZONA 01</xBairro><cMun>4115200</cMun><xMun>MARINGA</xMun><UF>PR</UF><CEP>87020025</CEP><cPais>1058</cPais><xPais>BRASIL</xPais><fone>4430379500</fone></enderDest><indIEDest>1</indIEDest><IE>9044016688</IE></dest><det nItem="1"><prod><cProd>0999</cProd><cEAN/><xProd>AMIDO DE MILHO</xProd><NCM>11081200</NCM><CEST>0123456</CEST><indEscala>S</indEscala><CFOP>5102</CFOP><uCom>CX</uCom><qCom>1</qCom><vUnCom>0.0100</vUnCom><vProd>0.01</vProd><cEANTrib/><uTrib>CX</uTrib><qTrib>1</qTrib><vUnTrib>0.0100</vUnTrib><indTot>1</indTot></prod><imposto><ICMS><ICMS00><orig>0</orig><CST>00</CST><modBC>0</modBC><vBC>0.01</vBC><pICMS>12.00</pICMS><vICMS>0.01</vICMS></ICMS00></ICMS><PIS><PISAliq><CST>01</CST><vBC>0.01</vBC><pPIS>1.65</pPIS><vPIS>0.00</vPIS></PISAliq></PIS><COFINS><COFINSAliq><CST>01</CST><vBC>0.01</vBC><pCOFINS>7.60</ pCOFINS><vCOFINS>0.01</vCOFINS></COFINSAliq></COFINS></imposto></det><total><ICMSTot><vBC>0.01</vBC><vICMS>0.01</vICMS><vICMSDeson>0.00</vICMSDeson><vFCP>0.00</vFCP><vBCST>0.00</vBCST><vST>0.00</vST><vFCPST>0.00</vFCPST><vFCPSTRet>0.00</vFCPSTRet><vProd>0.01</vProd><vFrete>0.00</vFrete><vSeg>0.00</vSeg><vDesc>0.00</vDesc><vII>0.00</vII><vIPI>0.00</vIPI><vIPIDevol>0.00</vIPIDevol><vPIS>0.00</vPIS><vCOFINS>0.01</vCOFINS><vOutro>0.00</vOutro><vNF>0.01</vNF></ICMSTot></total><transp><modFrete>0</modFrete></transp><cobr><fat><nFat>123</nFat><vOrig>0.01</vOrig><vLiq>0.01</vLiq></fat></cobr><pag><detPag><tPag>15</tPag><vPag>0.01</vPag></detPag></pag></infNFe><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#NFe41180508187168000160550010020000001020000009"><Transforms><Transform Algorithm="http://www.w3 .org/2000/09/xmldsig#enveloped-signature"/><Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>QptSZtOaO3h0wlX5oRCZwDraOCo=</DigestValue></Reference></SignedInfo><SignatureValue>gMr87sT9Ru5MLGTYex6Bzqa1DDW1lwghCJQ440adN0BLGl/eY823QG0GWzHc8j4dyBmtYBt1Dt4HKCj1BFn9NPE0zWh4Jd8Nvo2WrVR2dq0BOct9FjkZuRK42kgZbFl5Oz1a0ROb52nhxIAwkJyRwkXzo/XmdleRML2x6X+PPCf3oZVo0GSr54jSU7M+yHQb0CESalpqCgzPw6bLYscD5gdy7dPyZmIxykOCvud3ChkuB7glwLcpq7cT7r8jb7i8WVz098TWyoMUQfapXRu8/TGmkHjV0JBWfZbzSgVYe5al8/LLUu2J35nggcFMhKbhHMq5jstTCKhq9RgjMGCv4Q==</SignatureValue><KeyInfo><X509Data><X509Certificate>MIIHsTCCBZmgAwIBAgIIJkEYBQJA1oowDQYJKoZIhvcNAQELBQAwgYkxCzAJBgNVBAYTAkJSMRMwEQYDVQQKEwpJQ1AtQnJhc2lsMTQwMgYDVQQLEytBdXRvcmlkYWRlIENlcnRpZmljYWRvcmEgUmFpeiBCcmFzaWxlaXJhIHYyMRIwEAYDVQQLEwlBQyBTT0xVVEkxGzAZBgNVBAMTEkFDIFNPTFVUSSBNdWx0aXBsYTAeFw0xODA1MDMyMDQ5MTBaFw0xOTA1MDMxNjM0MDBaMIHNMQswCQYDVQQGEwJCUjETMBEGA1 UEChMKSUNQLUJyYXNpbDE0MDIGA1UECxMrQXV0b3JpZGFkZSBDZXJ0aWZpY2Fkb3JhIFJhaXogQnJhc2lsZWlyYSB2MjESMBAGA1UECxMJQUMgU09MVVRJMRswGQYDVQQLExJBQyBTT0xVVEkgTXVsdGlwbGExGjAYBgNVBAsTEUNlcnRpZmljYWRvIFBKIEExMSYwJAYDVQQDEx1URUNOT1NQRUVEIFMgQTowODE4NzE2ODAwMDE2MDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJGqmKAfyc2BSRm0KslakFBx3qe/tthZavpKdbFF6lsKlKDNgiQ6W3wsyzPVUC1D4/xvlhTl1oSJIbiNtfmBQOqt68vm7r3lpXKLE4v1Q5VWTpAOndXoe6C4bpePr4MQxDy8XjCeVav2tU6Ejoj5PbYeKD88eucxKURoQVeaHIiw9aZ/cNF561M7+7Z4OQLdAfDgsQxj/R0PrvXKq4Ag0aE8aeR3J7IKF+ah2bEl7GvGBFrJziFg33H4ZcMEpPxhxr5uJfF4GrY65G5/NxCuQkOsR+h8DcQomNjooJH++M6gbSj2chqPtkVV7DamFkaU1tzdERw8N5YZBXh4Ft2h0/0CAwEAAaOCAtUwggLRMFQGCCsGAQUFBwEBBEgwRjBEBggrBgEFBQcwAoY4aHR0cDovL2NjZC5hY3NvbHV0aS5jb20uYnIvbGNyL2FjLXNvbHV0aS1tdWx0aXBsYS12MS5wN2IwHQYDVR0OBBYEFMVOO7pkK0WOMqZR+r3FAAO/aqRoMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUNa4xFPZe0npPWP40qBpnlwrEmwcwXgYDVR0gBFcwVTBTBgZgTAECASYwSTBHBggrBgEFBQcCARY7aHR0cHM6Ly9jY2QuYWNzb2x1dGkuY29tLmJyL2RvY3MvZHBjLWFjLXNvbHV0aS1tdWx0aXBsYS5wZGYwgd4 GA1UdHwSB1jCB0zA+oDygOoY4aHR0cDovL2NjZC5hY3NvbHV0aS5jb20uYnIvbGNyL2FjLXNvbHV0aS1tdWx0aXBsYS12MS5jcmwwP6A9oDuGOWh0dHA6Ly9jY2QyLmFjc29sdXRpLmNvbS5ici9sY3IvYWMtc29sdXRpLW11bHRpcGxhLXYxLmNybDBQoE6gTIZKaHR0cDovL3JlcG9zaXRvcmlvLmljcGJyYXNpbC5nb3YuYnIvbGNyL0FDU09MVVRJL2FjLXNvbHV0aS1tdWx0aXBsYS12MS5jcmwwDgYDVR0PAQH/BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDCBvQYDVR0RBIG1MIGygR9lcmlrZS5hbG1laWRhQHRlY25vc3BlZWQuY29tLmJyoCEGBWBMAQMCoBgTFkVSSUtFIExFSVRFIERFIEFMTUVJREGgGQYFYEwBAwOgEBMOMDgxODcxNjgwMDAxNjCgOAYFYEwBAwSgLxMtMTQxMDE5Nzg5OTA0OTMyNTkwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwoBcGBWBMAQMHoA4TDDAwMDAwMDAwMDAwMDANBgkqhkiG9w0BAQsFAAOCAgEARGADzr06vz0jDtcj6SETQUWv5WNnBqjf1paGAghO0dxDWdhh02KnQdg152BdyjBhb/3vMYjkqOiv8Sg6oVqqGHIxzD7mOj0ffLZPE4GHNvd5UgMecAkdnarPZ7QP/VmEcYstyaO/6hjrON1XWFZR/VnLJGijDl1PekYyRYJO8WUo6F++VEU8Vi1/ef0X5Y6kO5C20eI+o4hRQPDai13HvlHvrlpSZTUYpydqn07WYZ587qkNhRcggMRQ+QWPVTieW8YDpAPuKhrcwN3mphePVCnRlEyX2eG2IiBRCFl1cqVZ2RJSS0DCZMR1pZ8FZiRJH5M8FAmQPhAFXLX30BlRXtA+2MUGL+t56QNn Rs+LJW5ViMo6DaNt/jL5mW92gHNB7CatixAcmop1OhN8t9/Krzv6x4JbpAJQ4e2zjWVpWoB8Nm6WrsC6RBUsBdELhWu5zRB2RTnrJpf5SiY6CcHyOV5haZVypQ0wjq0pIex9GpGWT2wapH+/orz+Jc2G42nJuxvvz1Sq7o6Ws2wILKWppZ0vzqTVQQYmWnNnrzu+Vmsp5QTE1eyI3ffaCmRHaBs9zXdSNkuzAfBcKzCH0J8iyfhkZ1EaGEfVe/fxPqK2QUMcuESdybMc3zwJ5K4CA0dyDB9hi8xkXzg/xhb+RkaEqQoyO6Y5EJYoKKWvxl2oXw8=</X509Certificate></X509Data></KeyInfo></Signature></NFe> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/signxml-4.4.0/test/test.py new/signxml-5.1.0/test/test.py --- old/signxml-4.4.0/test/test.py 2020-02-02 01:00:00.000000000 +0100 +++ new/signxml-5.1.0/test/test.py 2020-02-02 01:00:00.000000000 +0100 @@ -4,20 +4,24 @@ import os import re import sys +import tempfile +import time import unittest from base64 import b64decode, b64encode from concurrent.futures import ThreadPoolExecutor from dataclasses import replace -from datetime import datetime +from datetime import datetime, timezone from glob import glob from xml.etree import ElementTree as stdlibElementTree import cryptography.exceptions +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa -from cryptography.x509 import load_der_x509_certificate, load_pem_x509_certificate from lxml import etree sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +import signxml.processor as processor # noqa:E402 from signxml import ( # noqa:E402 CanonicalizationMethod, DigestAlgorithm, @@ -59,6 +63,18 @@ xades_sha1_ok = XAdESSignatureConfiguration( signature_methods=list(SignatureMethod), digest_algorithms=list(DigestAlgorithm) ) +example_cert_verification_time = datetime(2015, 1, 1, tzinfo=timezone.utc) +example_cert_config = SignatureConfiguration(verification_time=example_cert_verification_time) +sha1_ok_example_cert = replace(sha1_ok, verification_time=example_cert_verification_time) +xades_sha1_ok_example_cert = replace(xades_sha1_ok, verification_time=example_cert_verification_time) + + +def get_cert_midpoint_verification_time(cert_text): + try: + cert = x509.load_der_x509_certificate(b64decode("".join(cert_text.split()))) + except ValueError: + return None + return cert.not_valid_before_utc + (cert.not_valid_after_utc - cert.not_valid_before_utc) / 2 def reset_tree(t, method): @@ -69,12 +85,33 @@ s.getparent().remove(s) +def get_no_dsig_key_usage_ee_policy_verifier_at_time(verification_time): + class _Verifier(XMLVerifier): + def verify(self, data, **kwargs): + expect_config = kwargs.pop("expect_config", SignatureConfiguration()) + expect_config = replace(expect_config, verification_time=verification_time) + return super().verify(data, expect_config=expect_config, **kwargs) + + def get_cert_chain_verifier(self, ca_pem_file, ee_policy, ca_policy): + ee_policy = x509.verification.ExtensionPolicy.webpki_defaults_ee() + return super().get_cert_chain_verifier(ca_pem_file, ee_policy, ca_policy) + + return _Verifier() + + +def get_no_dsig_key_usage_ee_policy_verifier_for_year(year: int): + verification_time = datetime(year, 1, 1, tzinfo=timezone.utc) + return get_no_dsig_key_usage_ee_policy_verifier_at_time(verification_time) + + def get_verifier_for_year(year: int): + verification_time = datetime(year, 1, 1, tzinfo=timezone.utc) + class _Verifier(XMLVerifier): - def get_cert_chain_verifier(self, ca_pem_file): - verifier = super().get_cert_chain_verifier(ca_pem_file) - verifier.verification_time = datetime(year, 1, 1) - return verifier + def verify(self, data, **kwargs): + expect_config = kwargs.pop("expect_config", SignatureConfiguration()) + expect_config = replace(expect_config, verification_time=verification_time) + return super().verify(data, expect_config=expect_config, **kwargs) return _Verifier() @@ -117,6 +154,7 @@ data=etree.parse(example_file), x509_cert=cert, expect_references=2, + expect_config=example_cert_config, ) def test_example_multi_unspecified_reference_count(self): @@ -128,11 +166,52 @@ data=etree.parse(example_file), x509_cert=cert, expect_references=True, + expect_config=example_cert_config, ) self.assertIsInstance(res, list) self.assertEqual(2, len(res)) + def test_schema_cache_is_scoped_to_processor_subclasses(self): + for first, second in ((XMLVerifier, XAdESVerifier), (XAdESVerifier, XMLVerifier)): + for cls in (XMLVerifier, XAdESVerifier): + if "_schemas" in cls.__dict__: + delattr(cls, "_schemas") + + first_schemas = first.schemas() + second_schemas = second.schemas() + + self.assertIsNot(first_schemas, second_schemas) + self.assertEqual(len(first_schemas), len(first.schema_files)) + self.assertEqual(len(second_schemas), len(second.schema_files)) + + def test_schema_cache_is_thread_safe_on_first_access(self): + class ThreadedProcessor(processor.XMLProcessor): + schema_files = ["schema-a.xsd", "schema-b.xsd"] + + original_get_schema = processor.get_schema + original_schemas = processor.XMLProcessor._schemas + calls = [] + + def fake_get_schema(schema_file): + time.sleep(0.01) + calls.append(schema_file) + return schema_file + + processor.XMLProcessor._schemas = [] + processor.get_schema = fake_get_schema + try: + with ThreadPoolExecutor(max_workers=8) as executor: + results = list(executor.map(lambda _: ThreadedProcessor.schemas(), range(8))) + finally: + processor.get_schema = original_get_schema + processor.XMLProcessor._schemas = original_schemas + if "_schemas" in ThreadedProcessor.__dict__: + delattr(ThreadedProcessor, "_schemas") + + self.assertEqual(ThreadedProcessor.schema_files, calls) + self.assertTrue(all(result is results[0] for result in results)) + class TestSignXML(unittest.TestCase, LoadExampleKeys): def setUp(self): @@ -260,7 +339,7 @@ pass def test_x509_certs(self): - verifier = get_verifier_for_year(2015) + verifier = get_no_dsig_key_usage_ee_policy_verifier_for_year(2015) tree = etree.parse(self.example_xml_files[0]) ca_pem_file = os.path.join(os.path.dirname(__file__), "example-ca.pem").encode("utf-8") crt, key = self.load_example_keys() @@ -270,8 +349,8 @@ signer = XMLSigner(method=method, signature_algorithm=SignatureMethod.RSA_SHA256) signed = signer.sign(data, key=key, cert=crt) signed_data = etree.tostring(signed) - verifier.verify(signed_data, x509_cert=crt) - verifier.verify(signed_data, x509_cert=load_pem_x509_certificate(crt)) + verifier.verify(signed_data, x509_cert=crt, expect_config=example_cert_config) + verifier.verify(signed_data, x509_cert=x509.load_pem_x509_certificate(crt)) verifier.verify(signed_data, x509_cert=crt, cert_subject_name="*.example.com") with self.assertRaises(ValueError): @@ -289,10 +368,218 @@ # TODO: negative: verify with wrong cert, wrong CA + def test_x509_cert_chain_verification_time(self): + def make_key_usage(digital_signature=False, key_cert_sign=False, crl_sign=False): + return x509.KeyUsage( + digital_signature=digital_signature, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=key_cert_sign, + crl_sign=crl_sign, + encipher_only=False, + decipher_only=False, + ) + + tree = etree.parse(self.example_xml_files[0]) + ca_key = ec.generate_private_key(ec.SECP384R1()) + ca_name = x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "SignXML Test CA")]) + ca_cert = ( + x509.CertificateBuilder() + .subject_name(ca_name) + .issuer_name(ca_name) + .public_key(ca_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime(2020, 1, 1, tzinfo=timezone.utc)) + .not_valid_after(datetime(2030, 1, 1, tzinfo=timezone.utc)) + .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) + .add_extension(make_key_usage(key_cert_sign=True, crl_sign=True), critical=True) + .add_extension(x509.SubjectKeyIdentifier.from_public_key(ca_key.public_key()), critical=False) + .sign(ca_key, hashes.SHA384()) + ) + + leaf_key = ec.generate_private_key(ec.SECP384R1()) + leaf_cert = ( + x509.CertificateBuilder() + .subject_name(x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "XML Signature Test")])) + .issuer_name(ca_cert.subject) + .public_key(leaf_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime(2020, 1, 1, tzinfo=timezone.utc)) + .not_valid_after(datetime(2021, 1, 1, tzinfo=timezone.utc)) + .add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True) + .add_extension(make_key_usage(digital_signature=True), critical=True) + .add_extension(x509.SubjectKeyIdentifier.from_public_key(leaf_key.public_key()), critical=False) + .add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()), critical=False) + .sign(ca_key, hashes.SHA384()) + ) + + signer = XMLSigner(method=methods.enveloped, signature_algorithm=SignatureMethod.ECDSA_SHA384) + signed = signer.sign(tree.getroot(), key=leaf_key, cert=[leaf_cert]) + signed_data = etree.tostring(signed) + + XMLVerifier().verify( + signed_data, + x509_cert=leaf_cert, + expect_config=SignatureConfiguration(verification_time=datetime(2020, 6, 1, tzinfo=timezone.utc)), + ) + with self.assertRaisesRegex( + InvalidCertificate, "provided x509_cert.*certificate is not yet valid.*SignatureConfiguration" + ): + XMLVerifier().verify( + signed_data, + x509_cert=leaf_cert, + expect_config=SignatureConfiguration(verification_time=datetime(2019, 1, 1, tzinfo=timezone.utc)), + ) + with self.assertRaisesRegex( + InvalidCertificate, "provided x509_cert.*certificate has expired.*SignatureConfiguration" + ): + XMLVerifier().verify( + signed_data, + x509_cert=leaf_cert, + expect_config=SignatureConfiguration(verification_time=datetime(2025, 1, 1, tzinfo=timezone.utc)), + ) + + with tempfile.TemporaryDirectory() as temp_dir: + ca_pem_file = os.path.join(temp_dir, "ca.pem") + with open(ca_pem_file, "wb") as fh: + fh.write(ca_cert.public_bytes(serialization.Encoding.PEM)) + + XMLVerifier().verify( + signed_data, + ca_pem_file=ca_pem_file, + expect_config=SignatureConfiguration(verification_time=datetime(2020, 6, 1, tzinfo=timezone.utc)), + ) + with self.assertRaisesRegex( + InvalidCertificate, + "X509Data/X509Certificate certificate at index 0.*certificate has expired.*SignatureConfiguration", + ): + XMLVerifier().verify( + signed_data, + ca_pem_file=ca_pem_file, + expect_config=SignatureConfiguration(verification_time=datetime(2025, 1, 1, tzinfo=timezone.utc)), + ) + + def test_issue_284_expired_certificate_can_verify_at_signing_time(self): + issue_file = os.path.join(os.path.dirname(__file__), "issues", "issue_284_expired_cert.xml") + with open(issue_file, "rb") as fh: + signed_xml = fh.read() + doc = etree.fromstring(signed_xml) + cert_text = doc.find(".//ds:X509Certificate", namespaces=namespaces).text + signing_cert = x509.load_der_x509_certificate(b64decode(cert_text)) + config = SignatureConfiguration( + signature_methods=frozenset({SignatureMethod.RSA_SHA1}), + digest_algorithms=frozenset({DigestAlgorithm.SHA1}), + default_reference_c14n_method=CanonicalizationMethod.CANONICAL_XML_1_0, + ) + + verifier = XMLVerifier() + verifier.excise_empty_xmlns_declarations = True + with self.assertRaisesRegex( + InvalidCertificate, "provided x509_cert.*certificate has expired.*SignatureConfiguration" + ): + verifier.verify(signed_xml, x509_cert=signing_cert, expect_config=config) + + verifier = XMLVerifier() + verifier.excise_empty_xmlns_declarations = True + result = verifier.verify( + signed_xml, + x509_cert=signing_cert, + expect_config=replace(config, verification_time=datetime(2018, 5, 28, 17, 0, tzinfo=timezone.utc)), + ) + self.assertIsInstance(result, VerifyResult) + + def test_sign_rejects_invalid_x509_cert_input(self): + _, key = self.load_example_keys() + data = etree.parse(self.example_xml_files[0]).getroot() + invalid_pem = "-----BEGIN CERTIFICATE-----\nnot a certificate\n-----END CERTIFICATE-----" + + invalid_cert_inputs = [ + ("", "No PEM-encoded certificates found"), + ([], "No certificates found"), + ([""], "Invalid X.509 certificate"), + (invalid_pem, "Invalid X.509 certificate"), + ([invalid_pem], "Invalid X.509 certificate"), + ] + for cert, message in invalid_cert_inputs: + with self.subTest(cert=repr(cert)): + with self.assertRaisesRegex(InvalidInput, message): + XMLSigner().sign(data, key=key, cert=cert) + + def test_x509_cert_chain_requires_digital_signature_key_usage(self): + def make_key_usage(digital_signature=False, key_cert_sign=False, crl_sign=False): + return x509.KeyUsage( + digital_signature=digital_signature, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=key_cert_sign, + crl_sign=crl_sign, + encipher_only=False, + decipher_only=False, + ) + + verifier = get_verifier_for_year(2027) + tree = etree.parse(self.example_xml_files[0]) + ca_key = ec.generate_private_key(ec.SECP384R1()) + ca_name = x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "SignXML Test CA")]) + ca_cert = ( + x509.CertificateBuilder() + .subject_name(ca_name) + .issuer_name(ca_name) + .public_key(ca_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime(2026, 1, 1)) + .not_valid_after(datetime(2030, 1, 1)) + .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) + .add_extension(make_key_usage(key_cert_sign=True, crl_sign=True), critical=True) + .add_extension(x509.SubjectKeyIdentifier.from_public_key(ca_key.public_key()), critical=False) + .sign(ca_key, hashes.SHA384()) + ) + + def make_leaf_cert(digital_signature): + leaf_key = ec.generate_private_key(ec.SECP384R1()) + leaf_cert = ( + x509.CertificateBuilder() + .subject_name(x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "XML Signature Test")])) + .issuer_name(ca_cert.subject) + .public_key(leaf_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime(2026, 1, 1)) + .not_valid_after(datetime(2030, 1, 1)) + .add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True) + .add_extension(make_key_usage(digital_signature=digital_signature), critical=True) + .add_extension(x509.SubjectKeyIdentifier.from_public_key(leaf_key.public_key()), critical=False) + .add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()), critical=False) + .sign(ca_key, hashes.SHA384()) + ) + return leaf_key, leaf_cert + + signer = XMLSigner(method=methods.enveloped, signature_algorithm=SignatureMethod.ECDSA_SHA384) + with tempfile.TemporaryDirectory() as temp_dir: + ca_pem_file = os.path.join(temp_dir, "ca.pem") + with open(ca_pem_file, "wb") as fh: + fh.write(ca_cert.public_bytes(serialization.Encoding.PEM)) + + leaf_key, leaf_cert = make_leaf_cert(digital_signature=True) + key_usage = leaf_cert.extensions.get_extension_for_class(x509.KeyUsage).value + self.assertTrue(key_usage.digital_signature) + signed = signer.sign(tree.getroot(), key=leaf_key, cert=[leaf_cert]) + verifier.verify(etree.tostring(signed), ca_pem_file=ca_pem_file) + + leaf_key, leaf_cert = make_leaf_cert(digital_signature=False) + key_usage = leaf_cert.extensions.get_extension_for_class(x509.KeyUsage).value + self.assertFalse(key_usage.digital_signature) + signed = signer.sign(etree.parse(self.example_xml_files[0]).getroot(), key=leaf_key, cert=[leaf_cert]) + with self.assertRaisesRegex(InvalidCertificate, "certificate KeyUsage does not allow digitalSignature"): + verifier.verify(etree.tostring(signed), ca_pem_file=ca_pem_file) + def test_xmldsig_interop_examples(self): ca_pem_file = os.path.join(os.path.dirname(__file__), "interop", "cacert.pem").encode("utf-8") - verifier = get_verifier_for_year(2015) + verifier = get_no_dsig_key_usage_ee_policy_verifier_for_year(2015) for signature_file in glob(os.path.join(os.path.dirname(__file__), "interop", "*.xml")): print("Verifying", signature_file) with open(signature_file, "rb") as fh: @@ -303,7 +590,7 @@ def test_xmldsig_interop_TR2012(self): def get_x509_cert(**kwargs): with open(os.path.join(interop_dir, "TR2012", "rsa-cert.der"), "rb") as fh: - return [load_der_x509_certificate(fh.read())] + return [x509.load_der_x509_certificate(fh.read())] signature_files = glob(os.path.join(interop_dir, "TR2012", "signature*.xml")) for signature_file in signature_files: @@ -381,7 +668,10 @@ with open(signature_file, "rb") as fh: try: sig = fh.read() - verifier = get_verifier_for_year(2010 if "phaos" in signature_file else 2014) + verification_time = datetime(2010 if "phaos" in signature_file else 2014, 1, 1, tzinfo=timezone.utc) + if "pyXMLSecurity" in signature_file: + verification_time = datetime(2009, 10, 15, tzinfo=timezone.utc) + verifier = get_no_dsig_key_usage_ee_policy_verifier_at_time(verification_time) verifier.excise_empty_xmlns_declarations = True verifier.verify( sig, @@ -422,7 +712,10 @@ if signature_file.endswith("expired-cert.xml") or signature_file.endswith( "wsfederation_metadata.xml" ): # noqa - with self.assertRaisesRegex(InvalidCertificate, "cert is not valid at validation time"): + with self.assertRaisesRegex( + InvalidCertificate, + "certificate (has expired|is not yet valid).*SignatureConfiguration", + ): raise elif signature_file.endswith("invalid_enveloped_transform.xml"): self.assertIsInstance(e, InvalidSignature) @@ -507,7 +800,7 @@ signed_data = etree.tostring(signed) verifier = XMLVerifier() verifier.excise_empty_xmlns_declarations = True - verifier.verify(signed_data, x509_cert=crt) + verifier.verify(signed_data, x509_cert=crt, expect_config=example_cert_config) def test_elementtree_compat(self): data = stdlibElementTree.parse(self.example_xml_files[0]).getroot() @@ -539,7 +832,9 @@ data = etree.fromstring(self.saml_test_vectors[0]) reference_uri = "assertionId" signed_root = XMLSigner().sign(data, reference_uri=reference_uri, key=key, cert=crt) - res = XMLVerifier().verify(etree.tostring(signed_root), x509_cert=crt, expect_references=True) + res = XMLVerifier().verify( + etree.tostring(signed_root), x509_cert=crt, expect_references=True, expect_config=example_cert_config + ) self.assertIsInstance(res, list) self.assertEqual(1, len(res)) @@ -552,7 +847,9 @@ data = etree.fromstring(d) reference_uri = ["assertionId", "assertion2"] if "assertion2" in d else "assertionId" signed_root = XMLSigner().sign(data, reference_uri=reference_uri, key=key, cert=crt) - res = XMLVerifier().verify(etree.tostring(signed_root), x509_cert=crt, expect_references=True) + res = XMLVerifier().verify( + etree.tostring(signed_root), x509_cert=crt, expect_references=True, expect_config=example_cert_config + ) signed_data_root = res[0].signed_xml ref = signed_root.xpath( "/samlp:Response/saml:Assertion/ds:Signature/ds:SignedInfo/ds:Reference", @@ -594,11 +891,17 @@ # Test setting both X509Data and KeyInfo s4 = XMLSigner().sign(data, reference_uri=reference_uri, key=key, cert=crt, always_add_key_value=True) try: - XMLVerifier().verify(s4, x509_cert=crt) + XMLVerifier().verify(s4, x509_cert=crt, expect_config=example_cert_config) except InvalidSignature as e: self.assertIn("Expected to find 1 references, but found 2", str(e)) expect_refs = etree.tostring(s4).decode().count("<ds:Reference") - XMLVerifier().verify(s4, x509_cert=crt, ignore_ambiguous_key_info=True, expect_references=expect_refs) + XMLVerifier().verify( + s4, + x509_cert=crt, + ignore_ambiguous_key_info=True, + expect_references=expect_refs, + expect_config=example_cert_config, + ) def test_inclusive_namespaces_signing(self): # Test exclusive canonicalization with InclusiveNamespace PrefixList @@ -696,9 +999,11 @@ InvalidInput, "Both X509Data and KeyValue found and they represent different public keys", ): - XMLVerifier().verify(signed_xml, x509_cert=crt) + XMLVerifier().verify(signed_xml, x509_cert=crt, expect_config=example_cert_config) - XMLVerifier().verify(signed_xml, x509_cert=crt, ignore_ambiguous_key_info=True) + XMLVerifier().verify( + signed_xml, x509_cert=crt, ignore_ambiguous_key_info=True, expect_config=example_cert_config + ) def test_mismatched_ecdsa_key_value_with_x509_data(self): crt, key = self.load_example_ecdsa_keys() @@ -773,7 +1078,9 @@ doc, cert=cert, key=key, reference_uri="#mytest", signature_properties=sigprop ) fulldoc = b"<root>" + etree.tostring(signature) + etree.tostring(doc) + b"</root>" - XMLVerifier().verify(etree.fromstring(fulldoc), x509_cert=cert, expect_references=2) + XMLVerifier().verify( + etree.fromstring(fulldoc), x509_cert=cert, expect_references=2, expect_config=example_cert_config + ) def test_signature_properties_with_detached_method_re_enveloping(self): doc = etree.Element("{http://somenamespace}Test", attrib={"Id": "mytest"}) @@ -789,7 +1096,9 @@ + etree.tostring(doc) + b"</ns0:root>" ) - XMLVerifier().verify(etree.fromstring(fulldoc), x509_cert=cert, expect_references=2) + XMLVerifier().verify( + etree.fromstring(fulldoc), x509_cert=cert, expect_references=2, expect_config=example_cert_config + ) def test_payload_c14n(self): doc = etree.fromstring('<abc xmlns="http://example.com"><foo xmlns="">bar</foo></abc>') @@ -806,7 +1115,7 @@ "</rDE>" ) root = XMLSigner().sign(doc, cert=cert, key=key, reference_uri="#target") - XMLVerifier().verify(root, x509_cert=cert) + XMLVerifier().verify(root, x509_cert=cert, expect_config=example_cert_config) def test_include_c14n_transform_element_by_default(self): cert, key = self.load_example_keys() @@ -816,7 +1125,7 @@ "</rDE>" ) root = XMLSigner().sign(doc, cert=cert, key=key, reference_uri="#target") - XMLVerifier().verify(root, x509_cert=cert) + XMLVerifier().verify(root, x509_cert=cert, expect_config=example_cert_config) transform_elements = root.findall( "ds:Signature/ds:SignedInfo/ds:Reference/ds:Transforms/ds:Transform", namespaces=namespaces ) @@ -843,11 +1152,13 @@ XMLVerifier().verify, root, x509_cert=cert, + expect_config=example_cert_config, ) # However, if we use the right configuration, it should verify correctly config = SignatureConfiguration( - default_reference_c14n_method=CanonicalizationMethod.CANONICAL_XML_1_0_WITH_COMMENTS + default_reference_c14n_method=CanonicalizationMethod.CANONICAL_XML_1_0_WITH_COMMENTS, + verification_time=example_cert_verification_time, ) XMLVerifier().verify(root, x509_cert=cert, expect_config=config) transform_elements = root.findall( @@ -864,17 +1175,21 @@ signer = XMLSigner() signed = signer.sign(data, cert=cert, key=key) verifier = XMLVerifier() - verifier.verify(signed, x509_cert=cert) + verifier.verify(signed, x509_cert=cert, expect_config=example_cert_config) config = SignatureConfiguration(location="./foo/bar/") - with self.assertRaisesRegex(InvalidInput, "Expected to find XML element Signature in data"): + with self.assertRaisesRegex( + InvalidInput, "Expected to find XML element Signature in data using XPath ./foo/bar/ds:Signature" + ): verifier.verify(signed, x509_cert=cert, expect_config=config) config = SignatureConfiguration(signature_methods=[]) with self.assertRaisesRegex(InvalidInput, "Signature method RSA_SHA256 forbidden by configuration"): verifier.verify(signed, x509_cert=cert, expect_config=config) - config = SignatureConfiguration(digest_algorithms=[DigestAlgorithm.SHA3_512]) + config = SignatureConfiguration( + digest_algorithms=[DigestAlgorithm.SHA3_512], verification_time=example_cert_verification_time + ) with self.assertRaisesRegex(InvalidInput, "Digest algorithm SHA256 forbidden by configuration"): verifier.verify(signed, x509_cert=cert, expect_config=config) - config = SignatureConfiguration(digest_algorithms=[]) + config = SignatureConfiguration(digest_algorithms=[], verification_time=example_cert_verification_time) with self.assertRaisesRegex(InvalidInput, "Digest algorithm SHA256 forbidden by configuration"): verifier.verify(signed, x509_cert=cert, expect_config=config) @@ -888,7 +1203,7 @@ verifier = XMLVerifier() with self.assertRaisesRegex(InvalidInput, "Signature method RSA_SHA1 forbidden by configuration"): verifier.verify(signed, x509_cert=cert) - verifier.verify(signed, x509_cert=cert, expect_config=sha1_ok) + verifier.verify(signed, x509_cert=cert, expect_config=sha1_ok_example_cert) class TestXAdES(unittest.TestCase, LoadExampleKeys): @@ -935,7 +1250,41 @@ verifier = XAdESVerifier() verify_results = verifier.verify( - signed_doc, x509_cert=cert, expect_references=3, expect_signature_policy=self.signature_policy + signed_doc, + x509_cert=cert, + expect_references=3, + expect_signature_policy=self.signature_policy, + expect_config=xades_sha1_ok_example_cert, + ) + self.assertIsInstance(verify_results[1], XAdESVerifyResult) + self.assertTrue(hasattr(verify_results[1], "signed_properties")) + + def test_xades_roundtrip_with_cert_chain(self): + cert, key = self.load_example_keys() + with open(os.path.join(os.path.dirname(__file__), "example-ca.pem"), "rb") as fh: + ca_cert = fh.read() + with open(os.path.join(os.path.dirname(__file__), "example.xml"), "rb") as fh: + doc = etree.parse(fh) + signer = XAdESSigner( + signature_policy=self.signature_policy, + claimed_roles=self.claimed_roles, + data_object_format=self.data_object_format, + ) + signed_doc = signer.sign(doc, key=key, cert=[cert, ca_cert]) + + self.assertEqual(2, len(signed_doc.findall(".//ds:X509Certificate", namespaces=namespaces))) + self.assertEqual( + 2, + len(signed_doc.findall(".//xades:SigningCertificateV2/xades:Cert", namespaces=namespaces)), + ) + + verifier = XAdESVerifier() + verify_results = verifier.verify( + signed_doc, + x509_cert=cert, + expect_references=3, + expect_signature_policy=self.signature_policy, + expect_config=xades_sha1_ok_example_cert, ) self.assertIsInstance(verify_results[1], XAdESVerifyResult) self.assertTrue(hasattr(verify_results[1], "signed_properties")) @@ -948,7 +1297,6 @@ "corrupted-cert": etree.DocumentInvalid, # FIXME - flaky validation "cert-v2-wrong-digest": InvalidDigest, "wrong-sign-cert-digest": InvalidDigest, - "nonconformant-X_BE_CONN_10": InvalidDigest, "sigPolStore-noDigest": InvalidInput, } for sig_file in glob(os.path.join(os.path.dirname(__file__), "xades", "*.xml")): @@ -956,10 +1304,14 @@ with open(sig_file, "rb") as fh: doc = etree.parse(fh) cert = doc.find(".//{http://www.w3.org/2000/09/xmldsig#}X509Certificate").text + expect_config = xades_sha1_ok + verification_time = get_cert_midpoint_verification_time(cert) + if verification_time is not None: + expect_config = replace(expect_config, verification_time=verification_time) kwargs = dict( x509_cert=cert, expect_references=self.expect_references.get(os.path.basename(sig_file), 2), - expect_config=xades_sha1_ok, + expect_config=expect_config, ) if "nonconformant" in sig_file: kwargs.update(validate_schema=False)
