From 6879c986c4e9fef706608783138156b5d5b17e0b Mon Sep 17 00:00:00 2001 From: Andrey Kislyuk Date: Sun, 13 Nov 2022 00:10:37 -0800 Subject: [PATCH] Documentation and consistency improvements --- docs/conf.py | 8 ++++++- signxml/algorithms.py | 21 ++++++++++------- signxml/signer.py | 51 ++++++++++++++++++---------------------- signxml/util/__init__.py | 11 +++++---- signxml/verifier.py | 14 +++++++---- signxml/xades/xades.py | 9 +++---- 6 files changed, 64 insertions(+), 50 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index f7e47f2..664f52b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,7 +7,7 @@ release = "" language = "en" master_doc = "index" -extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.intersphinx"] source_suffix = [".rst", ".md"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] pygments_style = "sphinx" @@ -15,6 +15,12 @@ autodoc_typehints = "description" typehints_fully_qualified = True always_document_param_types = True +intersphinx_mapping = { + "https://docs.python.org/3": None, + "https://lxml.de/apidoc": "https://lxml.de/apidoc/objects.inv", + "https://cryptography.io/en/latest": "https://cryptography.io/en/latest/objects.inv", + "https://www.pyopenssl.org/en/stable": "https://www.pyopenssl.org/en/stable/objects.inv", +} if "readthedocs.org" in os.getcwd().split("/"): with open("index.rst", "w") as fh: diff --git a/signxml/algorithms.py b/signxml/algorithms.py index ce6ccdf..24fe6ca 100644 --- a/signxml/algorithms.py +++ b/signxml/algorithms.py @@ -1,4 +1,4 @@ -from enum import Enum, auto +from enum import Enum from typing import Callable from cryptography.hazmat.primitives import hashes @@ -13,19 +13,19 @@ class SignatureConstructionMethod(Enum): `_. """ - enveloped = auto() + enveloped = "http://www.w3.org/2000/09/xmldsig#enveloped-signature" """ The signature is over the XML content that contains the signature as an element. The content provides the root XML document element. This is the most common XML signature type in modern applications. """ - enveloping = auto() + enveloping = "enveloping-signature" """ The signature is over content found within an Object element of the signature itself. The Object (or its content) is identified via a Reference (via a URI fragment identifier or transform). """ - detached = auto() + detached = "detached-signature" """ The signature is over content external to the Signature element, and can be identified via a URI or transform. Consequently, the signature is "detached" from the content it signs. This definition typically applies to @@ -52,7 +52,9 @@ def _missing_(cls, value): class DigestAlgorithm(FragmentLookupMixin, InvalidInputErrorMixin, Enum): """ - An enumeration of digest algorithms supported by SignXML. See RFC 9231 for details. + An enumeration of digest algorithms supported by SignXML. See `RFC 9231 + `_ and the `Algorithm Identifiers and Implementation Requirements + `_ section of the XML Signature 1.1 standard for details. """ SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1" @@ -76,8 +78,9 @@ def implementation(self) -> Callable: # TODO: check if padding errors are fixed by using padding=MGF1 class SignatureMethod(FragmentLookupMixin, InvalidInputErrorMixin, Enum): """ - An enumeration of signature methods (also referred to as signature algorithms) supported by SignXML. See RFC 9231 - for details. + An enumeration of signature methods (also referred to as signature algorithms) supported by SignXML. See `RFC 9231 + `_ and the `Algorithm Identifiers and Implementation Requirements + `_ section of the XML Signature 1.1 standard for details. """ DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1" @@ -109,7 +112,9 @@ class SignatureMethod(FragmentLookupMixin, InvalidInputErrorMixin, Enum): class CanonicalizationMethod(InvalidInputErrorMixin, Enum): """ An enumeration of XML canonicalization methods (also referred to as canonicalization algorithms) supported by - SignXML. See RFC 9231 for details. + SignXML. See `RFC 9231 `_ and the `Algorithm Identifiers and + Implementation Requirements `_ section of the XML Signature 1.1 + standard for details. """ CANONICAL_XML_1_0 = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" diff --git a/signxml/signer.py b/signxml/signer.py index e1c03fb..4d9e0fa 100644 --- a/signxml/signer.py +++ b/signxml/signer.py @@ -2,12 +2,12 @@ from dataclasses import dataclass from typing import List, Optional, Union -from cryptography.hazmat.primitives.asymmetric import ec, utils +from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa, utils from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 from cryptography.hazmat.primitives.hmac import HMAC from cryptography.hazmat.primitives.serialization import load_pem_private_key from lxml.etree import Element, SubElement, _Element -from OpenSSL.crypto import FILETYPE_PEM, dump_certificate +from OpenSSL.crypto import FILETYPE_PEM, X509, dump_certificate from .algorithms import ( CanonicalizationMethod, @@ -62,13 +62,14 @@ class XMLSigner(XMLSignatureProcessor): ``signxml.methods.enveloped``, ``signxml.methods.enveloping``, or ``signxml.methods.detached``. See :class:`SignatureConstructionMethod` for details. :param signature_algorithm: - Algorithm that will be used to generate the signature, composed of the signature algorithm and the digest - algorithm, separated by a hyphen. All algorithm IDs listed under the `Algorithm Identifiers and - Implementation Requirements `_ section of the XML Signature - 1.1 standard are supported. - :param digest_algorithm: Algorithm that will be used to hash the data during signature generation. All algorithm IDs - listed under the `Algorithm Identifiers and Implementation Requirements - `_ section of the XML Signature 1.1 standard are supported. + Algorithm that will be used to generate the signature. See :class:`SignatureMethod` for the list of algorithm + IDs supported. + :param digest_algorithm: + Algorithm that will be used to hash the data during signature generation. See :class:`DigestAlgorithm` for the + list of algorithm IDs supported. + :param c14n_algorithm: + Algorithm that will be used to canonicalize (serialize in a reproducible way) the XML that is signed. See + :class:`CanonicalizationMethod` for the list of algorithm IDs supported. """ signature_annotators: List @@ -92,7 +93,7 @@ def __init__( method: SignatureConstructionMethod = SignatureConstructionMethod.enveloped, signature_algorithm: Union[SignatureMethod, str] = SignatureMethod.RSA_SHA256, digest_algorithm: Union[DigestAlgorithm, str] = DigestAlgorithm.SHA256, - c14n_algorithm=CanonicalizationMethod.CANONICAL_XML_1_1, + c14n_algorithm: Union[CanonicalizationMethod, str] = CanonicalizationMethod.CANONICAL_XML_1_1, ): if method is None or method not in SignatureConstructionMethod: raise InvalidInput(f"Unknown signature construction method {method}") @@ -113,16 +114,16 @@ def __init__( def sign( self, data, - key=None, + key: Optional[Union[str, bytes, rsa.RSAPrivateKey, dsa.DSAPrivateKey, ec.EllipticCurvePrivateKey]] = None, passphrase: Optional[bytes] = None, - cert=None, + cert: Optional[Union[str, List[str], List[X509]]] = None, reference_uri: Optional[Union[str, List[str], List[XMLSignatureReference]]] = None, key_name: Optional[str] = None, key_info: Optional[_Element] = None, id_attribute: Optional[str] = None, always_add_key_value: bool = False, inclusive_ns_prefixes: Optional[List[str]] = None, - signature_properties=None, + signature_properties: Optional[Union[_Element, List[_Element]]] = None, ) -> _Element: """ Sign the data and return the root element of the resulting XML tree. @@ -131,20 +132,15 @@ def sign( :type data: String, file-like object, or XML ElementTree Element API compatible object :param key: Key to be used for signing. When signing with a certificate or RSA/DSA/ECDSA key, this can be a string/bytes - containing a PEM-formatted key, or a :py:class:`cryptography.hazmat.primitives.interfaces.RSAPrivateKey`, - :py:class:`cryptography.hazmat.primitives.interfaces.DSAPrivateKey`, or - :py:class:`cryptography.hazmat.primitives.interfaces.EllipticCurvePrivateKey` object. When signing with a + containing a PEM-formatted key, or a :class:`cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`, + :class:`cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey`, or + :class:`cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey` object. When signing with a HMAC, this should be a string containing the shared secret. - :type key: - string, bytes, :py:class:`cryptography.hazmat.primitives.interfaces.RSAPrivateKey`, - :py:class:`cryptography.hazmat.primitives.interfaces.DSAPrivateKey`, or - :py:class:`cryptography.hazmat.primitives.interfaces.EllipticCurvePrivateKey` object :param passphrase: Passphrase to use to decrypt the key, if any. :param cert: X.509 certificate to use for signing. This should be a string containing a PEM-formatted certificate, or an - array of strings or OpenSSL.crypto.X509 objects containing the certificate and a chain of intermediate - certificates. - :type cert: string, array of strings, or array of OpenSSL.crypto.X509 objects + array of strings or :class:`OpenSSL.crypto.X509` objects containing the certificate and a chain of + intermediate certificates. :param reference_uri: Custom reference URI or list of reference URIs to incorporate into the signature. When ``method`` is set to ``detached`` or ``enveloped``, reference URIs are set to this value and only the referenced elements are @@ -175,10 +171,9 @@ def sign( :param signature_properties: One or more Elements that are to be included in the SignatureProperies section when using the detached method. - :type signature_properties: :py:class:`lxml.etree.Element` or list of :py:class:`lxml.etree.Element` s :returns: - A :py:class:`lxml.etree.Element` object representing the root of the XML tree containing the signature and + A :class:`lxml.etree._Element` object representing the root of the XML tree containing the signature and the payload data. To specify the location of an enveloped signature within **data**, insert a @@ -192,7 +187,7 @@ def sign( if isinstance(cert, (str, bytes)): cert_chain = list(iterate_pem(cert)) else: - cert_chain = cert + cert_chain = cert # type: ignore input_references = self._preprocess_reference_uri(reference_uri) @@ -235,7 +230,7 @@ def sign( 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]()) + signer = HMAC(key=key, algorithm=digest_algorithm_implementations[self.sign_alg]()) # type: ignore signer.update(signed_info_c14n) signature_value_node.text = b64encode(signer.finalize()).decode() sig_root.append(signature_value_node) @@ -378,7 +373,7 @@ def _build_sig(self, sig_root, references, c14n_inputs, inclusive_ns_prefixes): reference_node = SubElement(signed_info, ds_tag("Reference"), URI=reference.URI) transforms = SubElement(reference_node, ds_tag("Transforms")) if self.construction_method == SignatureConstructionMethod.enveloped: - SubElement(transforms, ds_tag("Transform"), Algorithm=namespaces.ds + "enveloped-signature") + SubElement(transforms, ds_tag("Transform"), Algorithm=SignatureConstructionMethod.enveloped.value) SubElement(transforms, ds_tag("Transform"), Algorithm=reference.c14n_method.value) else: c14n_xform = SubElement(transforms, ds_tag("Transform"), Algorithm=reference.c14n_method.value) diff --git a/signxml/util/__init__.py b/signxml/util/__init__.py index bdfabd8..31933ef 100644 --- a/signxml/util/__init__.py +++ b/signxml/util/__init__.py @@ -13,6 +13,7 @@ from typing import Any, List, Optional from cryptography.hazmat.primitives import hashes, hmac +from lxml.etree import QName from ..exceptions import InvalidCertificate, RedundantCert, SignXMLException @@ -39,23 +40,23 @@ def __getattr__(self, a): def ds_tag(tag): - return "{" + namespaces.ds + "}" + tag + return QName(namespaces.ds, tag) def dsig11_tag(tag): - return "{" + namespaces.dsig11 + "}" + tag + return QName(namespaces.dsig11, tag) def ec_tag(tag): - return "{" + namespaces.ec + "}" + tag + return QName(namespaces.ec, tag) def xades_tag(tag): - return "{" + namespaces.xades + "}" + tag + return QName(namespaces.xades, tag) def xades141_tag(tag): - return "{" + namespaces.xades141 + "}" + tag + return QName(namespaces.xades141, tag) @dataclass diff --git a/signxml/verifier.py b/signxml/verifier.py index 1e23654..c50a21b 100644 --- a/signxml/verifier.py +++ b/signxml/verifier.py @@ -12,7 +12,13 @@ from OpenSSL.crypto import load_certificate from OpenSSL.crypto import verify as openssl_verify -from .algorithms import CanonicalizationMethod, DigestAlgorithm, SignatureMethod, digest_algorithm_implementations +from .algorithms import ( + CanonicalizationMethod, + DigestAlgorithm, + SignatureConstructionMethod, + SignatureMethod, + digest_algorithm_implementations, +) from .exceptions import InvalidCertificate, InvalidDigest, InvalidInput, InvalidSignature from .processor import XMLSignatureProcessor from .util import ( @@ -141,7 +147,7 @@ def _apply_transforms(self, payload, transforms_node, signature, c14n_algorithm: transforms = self._findall(transforms_node, "Transform") for transform in transforms: - if transform.get("Algorithm") == "http://www.w3.org/2000/09/xmldsig#enveloped-signature": + if transform.get("Algorithm") == SignatureConstructionMethod.enveloped.value: _remove_sig(signature, idempotent=True) for transform in transforms: @@ -242,7 +248,7 @@ def verify( :param parser: Custom XML parser instance to use when parsing **data**. The default parser arguments used by SignXML are: ``resolve_entities=False``. See https://lxml.de/FAQ.html#how-do-i-use-lxml-safely-as-a-web-service-endpoint. - :type parser: :py:class:`lxml.etree.XMLParser` compatible parser + :type parser: :class:`lxml.etree.XMLParser` compatible parser :param uri_resolver: Function to use to resolve reference URIs that don't start with "#". The function is called with a single string argument containing the URI to be resolved, and is expected to return a lxml.etree node or string. @@ -259,7 +265,7 @@ def verify( necessary to match the keys, and throws an InvalidInput error instead. Set this to True to bypass the error and validate the signature using X509Data only. - :raises: :py:class:`cryptography.exceptions.InvalidSignature` + :raises: :class:`signxml.exceptions.InvalidSignature` """ self.hmac_key = hmac_key self.require_x509 = require_x509 diff --git a/signxml/xades/xades.py b/signxml/xades/xades.py index c26b9d0..8e73a61 100644 --- a/signxml/xades/xades.py +++ b/signxml/xades/xades.py @@ -323,10 +323,10 @@ def verify( # type: ignore **xml_verifier_args, ) -> List[XAdESVerifyResult]: """ - Verify the XAdES signature supplied in the data and return a list of **VerifyResult** data structures - representing the data signed by the signature, or raise an exception if the signature is not valid. By default, - this requires the signature to be generated using a valid X.509 certificate. To enable other means of signature - validation, set the **require_x509** argument to `False`. + 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 + is a wrapper around :meth:`signxml.XMLVerifier.verify`; see its documentation for more details and arguments it + supports. :param expect_signature_policy: If you need to assert that the verified XAdES signature carries specific data in the @@ -340,6 +340,7 @@ def verify( # type: ignore Parameters to pass to :meth:`signxml.XMLVerifier.verify`. """ self.expect_signature_policy = expect_signature_policy + xml_verifier_args["require_x509"] = True verify_results = super().verify(data, expect_references=expect_references, **xml_verifier_args) if not isinstance(verify_results, list): raise InvalidInput("Expected to find multiple references in signature")