Skip to content

Commit 7c60d4a

Browse files
committed
Document XAdES module, clean up docs and namespace
1 parent 026cacf commit 7c60d4a

File tree

13 files changed

+465
-377
lines changed

13 files changed

+465
-377
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
max-parallel: 8
1010
matrix:
1111
os: [ubuntu-20.04, ubuntu-22.04, macos-10.15]
12-
python-version: ["3.7", "3.8", "3.9", "3.10"]
12+
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
1313
steps:
1414
- uses: actions/checkout@v2
1515
- name: Set up Python ${{ matrix.python-version }}

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Installation
2727
------------
2828
::
2929

30-
pip3 install signxml
30+
pip install signxml
3131

3232
Note: SignXML depends on `lxml <https://github.com/lxml/lxml>`_ and `cryptography
3333
<https://github.com/pyca/cryptography>`_, which in turn depend on `OpenSSL <https://www.openssl.org/>`_, `LibXML

docs/index.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ API documentation
77
:members:
88
:imported-members:
99
:undoc-members:
10+
:exclude-members: XMLSignatureProcessor
11+
12+
XAdES API documentation
13+
=======================
14+
15+
.. automodule:: signxml.xades
16+
:members:
17+
:imported-members:
18+
:undoc-members:
1019

1120
Change log
1221
==========

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ line-length = 120
33
[tool.isort]
44
profile = "black"
55
line_length = 120
6+
skip = ["signxml/__init__.py", "signxml/xades/__init__.py"]

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ universal=1
33
[flake8]
44
max-line-length=120
55
extend-ignore=E203
6+
per-file-ignores=
7+
signxml/__init__.py:F401
8+
signxml/xades/__init__.py:F401
69
[coverage:run]
710
omit =
811
signxml/__pyinstaller/*

signxml/__init__.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
# isort: skip_file
2-
from .signer import XMLSigner, XMLSignatureReference # noqa:F401
3-
from .verifier import XMLVerifier, VerifyResult # noqa:F401
4-
from .algorithms import DigestAlgorithm, SignatureMethod, CanonicalizationMethod, SignatureType # noqa:F401
5-
from .exceptions import InvalidCertificate, InvalidDigest, InvalidInput, InvalidSignature # noqa:F401
6-
from .processor import XMLSignatureProcessor # noqa:F401
7-
from .util import SigningSettings, namespaces # noqa:F401
1+
"""
2+
Use :class:`signxml.XMLSigner` and :class:`signxml.XMLVerifier` to sign and verify XML Signatures, respectively.
3+
See `SignXML documentation <#synopsis>`_ for examples.
4+
"""
85

9-
methods = SignatureType
6+
from .signer import XMLSigner, XMLSignatureReference
7+
from .verifier import XMLVerifier, VerifyResult
8+
from .algorithms import DigestAlgorithm, SignatureMethod, CanonicalizationMethod, SignatureConstructionMethod
9+
from .exceptions import InvalidCertificate, InvalidDigest, InvalidInput, InvalidSignature
10+
from .processor import XMLSignatureProcessor
11+
from .util import namespaces
12+
13+
methods = SignatureConstructionMethod

signxml/algorithms.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
from enum import Enum, auto
2+
from typing import Callable
23

34
from cryptography.hazmat.primitives import hashes
45

56
from .exceptions import InvalidInput
67

78

8-
class SignatureType(Enum):
9+
class SignatureConstructionMethod(Enum):
910
"""
10-
An enumeration of structural signature types supported by SignXML.
11+
An enumeration of signature construction methods supported by SignXML, used to specify the method when signing.
12+
See the list of signature types under `XML Signature Syntax and Processing Version 2.0, Definitions
13+
<http://www.w3.org/TR/xmldsig-core2/#sec-Definitions>`_.
1114
"""
1215

1316
enveloped = auto()
1417
"""
1518
The signature is over the XML content that contains the signature as an element. The content provides the root
16-
XML document element.
19+
XML document element. This is the most common XML signature type in modern applications.
1720
"""
1821

1922
enveloping = auto()
@@ -63,14 +66,18 @@ class DigestAlgorithm(FragmentLookupMixin, InvalidInputErrorMixin, Enum):
6366
SHA3_512 = "http://www.w3.org/2007/05/xmldsig-more#sha3-512"
6467

6568
@property
66-
def implementation(self):
69+
def implementation(self) -> Callable:
70+
"""
71+
The cryptography callable that implements the specified algorithm.
72+
"""
6773
return digest_algorithm_implementations[self]
6874

6975

7076
# TODO: check if padding errors are fixed by using padding=MGF1
7177
class SignatureMethod(FragmentLookupMixin, InvalidInputErrorMixin, Enum):
7278
"""
73-
An enumeration of signature methods supported by SignXML. See RFC 9231 for details.
79+
An enumeration of signature methods (also referred to as signature algorithms) supported by SignXML. See RFC 9231
80+
for details.
7481
"""
7582

7683
DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1"
@@ -101,7 +108,8 @@ class SignatureMethod(FragmentLookupMixin, InvalidInputErrorMixin, Enum):
101108

102109
class CanonicalizationMethod(InvalidInputErrorMixin, Enum):
103110
"""
104-
An enumeration of XML canonicalization methods supported by SignXML. See RFC 9231 for details.
111+
An enumeration of XML canonicalization methods (also referred to as canonicalization algorithms) supported by
112+
SignXML. See RFC 9231 for details.
105113
"""
106114

107115
CANONICAL_XML_1_0 = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"

signxml/processor.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,26 +33,26 @@ def parser(self):
3333
return self._default_parser
3434
return self._parser
3535

36-
def fromstring(self, xml_string, **kwargs):
36+
def _fromstring(self, xml_string, **kwargs):
3737
xml_node = etree.fromstring(xml_string, parser=self.parser, **kwargs)
3838
for entity in xml_node.iter(etree.Entity):
3939
raise InvalidInput("Entities are not supported in XML input")
4040
return xml_node
4141

42-
def tostring(self, xml_node, **kwargs):
42+
def _tostring(self, xml_node, **kwargs):
4343
return etree.tostring(xml_node, **kwargs)
4444

4545
def get_root(self, data):
4646
if isinstance(data, (str, bytes)):
47-
return self.fromstring(data)
47+
return self._fromstring(data)
4848
elif isinstance(data, stdlibElementTree.Element):
4949
# TODO: add debug level logging statement re: performance impact here
50-
return self.fromstring(stdlibElementTree.tostring(data, encoding="utf-8"))
50+
return self._fromstring(stdlibElementTree.tostring(data, encoding="utf-8"))
5151
else:
5252
# HACK: deep copy won't keep root's namespaces resulting in an invalid digest
5353
# We use a copy so we can modify the tree
5454
# TODO: turn this off for xmlenc
55-
return self.fromstring(etree.tostring(data))
55+
return self._fromstring(etree.tostring(data))
5656

5757

5858
class XMLSignatureProcessor(XMLProcessor):

signxml/signer.py

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from .algorithms import (
1313
CanonicalizationMethod,
1414
DigestAlgorithm,
15+
SignatureConstructionMethod,
1516
SignatureMethod,
16-
SignatureType,
1717
digest_algorithm_implementations,
1818
)
1919
from .exceptions import InvalidInput
@@ -59,9 +59,8 @@ class XMLSigner(XMLSignatureProcessor):
5959
pieces of data.
6060
6161
:param method:
62-
``signxml.methods.enveloped``, ``signxml.methods.enveloping``, or ``signxml.methods.detached``. See the list
63-
of signature types under `XML Signature Syntax and Processing Version 2.0, Definitions
64-
<http://www.w3.org/TR/xmldsig-core2/#sec-Definitions>`_.
62+
``signxml.methods.enveloped``, ``signxml.methods.enveloping``, or ``signxml.methods.detached``. See
63+
:class:`SignatureConstructionMethod` for details.
6564
:param signature_algorithm:
6665
Algorithm that will be used to generate the signature, composed of the signature algorithm and the digest
6766
algorithm, separated by a hyphen. All algorithm IDs listed under the `Algorithm Identifiers and
@@ -72,16 +71,32 @@ class XMLSigner(XMLSignatureProcessor):
7271
<http://www.w3.org/TR/xmldsig-core1/#sec-AlgID>`_ section of the XML Signature 1.1 standard are supported.
7372
"""
7473

74+
signature_annotators: List
75+
"""
76+
A list of callables that will be called at signature creation time to annotate the content to be signed before
77+
signing. You can use this to register a custom signature decorator as follows:
78+
79+
.. code-block:: python
80+
81+
def my_annotator(sig_root, signing_settings):
82+
...
83+
sig_root.append(my_custom_node)
84+
85+
signer = XMLSigner()
86+
signer.signature_annotators.append(my_annotator)
87+
signed = signer.sign(data, ...)
88+
"""
89+
7590
def __init__(
7691
self,
77-
method: SignatureType = SignatureType.enveloped,
92+
method: SignatureConstructionMethod = SignatureConstructionMethod.enveloped,
7893
signature_algorithm: Union[SignatureMethod, str] = SignatureMethod.RSA_SHA256,
7994
digest_algorithm: Union[DigestAlgorithm, str] = DigestAlgorithm.SHA256,
8095
c14n_algorithm=CanonicalizationMethod.CANONICAL_XML_1_1,
8196
):
82-
if method is None or method not in SignatureType:
83-
raise InvalidInput(f"Unknown signature method {method}")
84-
self.signature_type = method
97+
if method is None or method not in SignatureConstructionMethod:
98+
raise InvalidInput(f"Unknown signature construction method {method}")
99+
self.construction_method = method
85100
if isinstance(signature_algorithm, str) and "#" not in signature_algorithm:
86101
self.sign_alg = SignatureMethod.from_fragment(signature_algorithm)
87102
else:
@@ -108,7 +123,7 @@ def sign(
108123
always_add_key_value: bool = False,
109124
inclusive_ns_prefixes: Optional[List[str]] = None,
110125
signature_properties=None,
111-
):
126+
) -> _Element:
112127
"""
113128
Sign the data and return the root element of the resulting XML tree.
114129
@@ -168,7 +183,7 @@ def sign(
168183
169184
To specify the location of an enveloped signature within **data**, insert a
170185
``<ds:Signature Id="placeholder"></ds:Signature>`` element in **data** (where
171-
"ds" is the "http://www.w3.org/2000/09/xmldsig#" namespace). This element will
186+
"ds" is the ``http://www.w3.org/2000/09/xmldsig#`` namespace). This element will
172187
be replaced by the generated signature, and excised when generating the digest.
173188
"""
174189
if id_attribute is not None:
@@ -199,7 +214,7 @@ def sign(
199214

200215
sig_root, doc_root, c14n_inputs, references = self._unpack(data, input_references)
201216

202-
if self.signature_type == SignatureType.detached and signature_properties is not None:
217+
if self.construction_method == SignatureConstructionMethod.detached and signature_properties is not None:
203218
references.append(XMLSignatureReference(URI="#prop"))
204219
if signature_properties is not None and not isinstance(signature_properties, list):
205220
signature_properties = [signature_properties]
@@ -246,14 +261,14 @@ def sign(
246261
else:
247262
raise NotImplementedError()
248263

249-
if self.signature_type == SignatureType.enveloping:
264+
if self.construction_method == SignatureConstructionMethod.enveloping:
250265
for c14n_input in c14n_inputs:
251266
doc_root.append(c14n_input)
252267

253-
if self.signature_type == SignatureType.detached and signature_properties is not None:
268+
if self.construction_method == SignatureConstructionMethod.detached and signature_properties is not None:
254269
sig_root.append(signature_properties_el)
255270

256-
return doc_root if self.signature_type == SignatureType.enveloped else sig_root
271+
return doc_root if self.construction_method == SignatureConstructionMethod.enveloped else sig_root
257272

258273
def _preprocess_reference_uri(self, reference_uris):
259274
if reference_uris is None:
@@ -298,7 +313,7 @@ def _get_c14n_inputs_from_references(self, doc_root, references: List[XMLSignatu
298313

299314
def _unpack(self, data, references: List[XMLSignatureReference]):
300315
sig_root = Element(ds_tag("Signature"), nsmap=self.namespaces)
301-
if self.signature_type == SignatureType.enveloped:
316+
if self.construction_method == SignatureConstructionMethod.enveloped:
302317
if isinstance(data, (str, bytes)):
303318
raise InvalidInput("When using enveloped signature, **data** must be an XML element")
304319
doc_root = self.get_root(data)
@@ -328,7 +343,7 @@ def _unpack(self, data, references: List[XMLSignatureReference]):
328343
payload_id = c14n_input.get("Id", c14n_input.get("ID"))
329344
uri = "#{}".format(payload_id) if payload_id is not None else ""
330345
references.append(XMLSignatureReference(URI=uri))
331-
elif self.signature_type == SignatureType.detached:
346+
elif self.construction_method == SignatureConstructionMethod.detached:
332347
doc_root = self.get_root(data)
333348
if references is None:
334349
uri = "#{}".format(data.get("Id", data.get("ID", "object")))
@@ -338,7 +353,7 @@ def _unpack(self, data, references: List[XMLSignatureReference]):
338353
c14n_inputs, references = self._get_c14n_inputs_from_references(doc_root, references)
339354
except InvalidInput: # Dummy reference URI
340355
c14n_inputs = [self.get_root(data)]
341-
elif self.signature_type == SignatureType.enveloping:
356+
elif self.construction_method == SignatureConstructionMethod.enveloping:
342357
doc_root = sig_root
343358
c14n_inputs = [Element(ds_tag("Object"), nsmap=self.namespaces, Id="object")]
344359
if isinstance(data, (str, bytes)):
@@ -362,7 +377,7 @@ def _build_sig(self, sig_root, references, c14n_inputs, inclusive_ns_prefixes):
362377
reference.inclusive_ns_prefixes = inclusive_ns_prefixes
363378
reference_node = SubElement(signed_info, ds_tag("Reference"), URI=reference.URI)
364379
transforms = SubElement(reference_node, ds_tag("Transforms"))
365-
if self.signature_type == SignatureType.enveloped:
380+
if self.construction_method == SignatureConstructionMethod.enveloped:
366381
SubElement(transforms, ds_tag("Transform"), Algorithm=namespaces.ds + "enveloped-signature")
367382
SubElement(transforms, ds_tag("Transform"), Algorithm=reference.c14n_method.value)
368383
else:

signxml/verifier.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ class VerifyResult:
4848

4949
class XMLVerifier(XMLSignatureProcessor):
5050
"""
51-
Create a new XML Signature Verifier object, which can be used to hold configuration information and verify multiple
52-
pieces of data.
51+
Create a new XML Signature Verifier object, which can be used to verify multiple pieces of data.
5352
"""
5453

5554
def _get_signature(self, root):
@@ -184,7 +183,7 @@ def verify(
184183
ignore_ambiguous_key_info: bool = False,
185184
) -> Union[VerifyResult, List[VerifyResult]]:
186185
"""
187-
Verify the XML signature supplied in the data and return a list of **VerifyResult** data structures
186+
Verify the XML signature supplied in the data and return a list of :class:`VerifyResult` data structures
188187
representing the data signed by the signature, or raise an exception if the signature is not valid. By default,
189188
this requires the signature to be generated using a valid X.509 certificate. To enable other means of signature
190189
validation, set the **require_x509** argument to `False`.
@@ -277,7 +276,7 @@ def verify(
277276
signature_ref = self._get_signature(root)
278277

279278
# HACK: deep copy won't keep root's namespaces
280-
signature = self.fromstring(self.tostring(signature_ref))
279+
signature = self._fromstring(self._tostring(signature_ref))
281280

282281
if validate_schema:
283282
self.validate_schema(signature)
@@ -346,7 +345,7 @@ def verify(
346345
# If both X509Data and KeyValue are present, match one against the other and raise an error on mismatch
347346
if key_value is not None:
348347
if (
349-
self.check_key_value_matches_cert_public_key(key_value, signing_cert.get_pubkey(), signature_alg)
348+
self._check_key_value_matches_cert_public_key(key_value, signing_cert.get_pubkey(), signature_alg)
350349
is False
351350
):
352351
if ignore_ambiguous_key_info is False:
@@ -360,7 +359,7 @@ def verify(
360359
# mismatch
361360
if der_encoded_key_value is not None:
362361
if (
363-
self.check_der_key_value_matches_cert_public_key(
362+
self._check_der_key_value_matches_cert_public_key(
364363
der_encoded_key_value, signing_cert.get_pubkey(), signature_alg
365364
)
366365
is False
@@ -397,7 +396,7 @@ def verify(
397396

398397
verify_results: List[VerifyResult] = []
399398
for reference in self._findall(signed_info, "Reference"):
400-
copied_root = self.fromstring(self.tostring(root))
399+
copied_root = self._fromstring(self._tostring(root))
401400
copied_signature_ref = self._get_signature(copied_root)
402401
transforms = self._find(reference, "Transforms", require=False)
403402
digest_alg = self._find(reference, "DigestMethod").get("Algorithm")
@@ -409,7 +408,7 @@ def verify(
409408

410409
# We return the signed XML (and only that) to ensure no access to unsigned data happens
411410
try:
412-
payload_c14n_xml = self.fromstring(payload_c14n)
411+
payload_c14n_xml = self._fromstring(payload_c14n)
413412
except etree.XMLSyntaxError:
414413
payload_c14n_xml = None
415414
verify_results.append(VerifyResult(payload_c14n, payload_c14n_xml, signature))
@@ -430,7 +429,7 @@ def validate_schema(self, signature):
430429
last_exception = e
431430
raise last_exception # type: ignore
432431

433-
def check_key_value_matches_cert_public_key(self, key_value, public_key, signature_alg: SignatureMethod):
432+
def _check_key_value_matches_cert_public_key(self, key_value, public_key, signature_alg: SignatureMethod):
434433
if signature_alg.name.startswith("ECDSA_") and isinstance(
435434
public_key.to_cryptography_key(), ec.EllipticCurvePublicKey
436435
):
@@ -472,7 +471,7 @@ def check_key_value_matches_cert_public_key(self, key_value, public_key, signatu
472471

473472
raise NotImplementedError()
474473

475-
def check_der_key_value_matches_cert_public_key(self, der_encoded_key_value, public_key, signature_alg):
474+
def _check_der_key_value_matches_cert_public_key(self, der_encoded_key_value, public_key, signature_alg):
476475
# TODO: Add a test case for this functionality
477476
der_public_key = load_der_public_key(b64decode(der_encoded_key_value.text))
478477

0 commit comments

Comments
 (0)