Skip to content

Commit 026cacf

Browse files
committed
Complete migration of structured inputs to dataclasses
1 parent af3207f commit 026cacf

File tree

6 files changed

+173
-95
lines changed

6 files changed

+173
-95
lines changed

README.rst

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,17 +175,22 @@ SignXML supports signing and verifying documents using `XAdES <https://en.wikipe
175175

176176
.. code-block:: python
177177
178-
from signxml.xades import XAdESSigner, XAdESVerifier, XAdESVerifyResult, digest_algorithms
179-
signature_policy = {
180-
"Identifier": "MyPolicyIdentifier",
181-
"Description": "Hello XAdES",
182-
"DigestMethod": digest_algorithms.SHA256,
183-
"DigestValue": "Ohixl6upD6av8N7pEvDABhEL6hM=",
184-
}
178+
from signxml.xades import (XAdESSigner, XAdESVerifier, XAdESVerifyResult,
179+
XAdESSignaturePolicy, XAdESDataObjectFormat, DigestAlgorithm)
180+
signature_policy = XAdESSignaturePolicy(
181+
Identifier="MyPolicyIdentifier",
182+
Description="Hello XAdES",
183+
DigestMethod=DigestAlgorithm.SHA256,
184+
DigestValue="Ohixl6upD6av8N7pEvDABhEL6hM=",
185+
)
186+
data_object_format = XAdESDataObjectFormat(
187+
Description="My XAdES signature",
188+
MimeType="text/xml",
189+
)
185190
signer = XAdESSigner(
186191
signature_policy=signature_policy,
187192
claimed_roles=["signer"],
188-
data_object_format={"Description": "My XAdES signature", "MimeType": "text/xml"},
193+
data_object_format=data_object_format,
189194
c14n_algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
190195
)
191196
signed_doc = signer.sign(doc, key=private_key, cert=certificate)
@@ -217,6 +222,7 @@ Links
217222
* `W3C Working Group Note: XML Signature Best Practices <http://www.w3.org/TR/xmldsig-bestpractices/>`_
218223
* `XML-Signature Interoperability <http://www.w3.org/Signature/2001/04/05-xmldsig-interop.html>`_
219224
* `W3C Working Group Note: Test Cases for C14N 1.1 and XMLDSig Interoperability <http://www.w3.org/TR/xmldsig2ed-tests/>`_
225+
* `RFC 9231: Additional XML Security Uniform Resource Identifiers (URIs) <https://www.rfc-editor.org/rfc/rfc9231.html>`_
220226
* `XMLSec: Related links <https://www.aleksey.com/xmlsec/related.html>`_
221227
* `OWASP SAML Security Cheat Sheet <https://www.owasp.org/index.php/SAML_Security_Cheat_Sheet>`_
222228
* `Okta Developer Docs: SAML <https://developer.okta.com/standards/SAML/>`_

signxml/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# isort: skip_file
2-
from .signer import XMLSigner # noqa:F401
2+
from .signer import XMLSigner, XMLSignatureReference # noqa:F401
33
from .verifier import XMLVerifier, VerifyResult # noqa:F401
44
from .algorithms import DigestAlgorithm, SignatureMethod, CanonicalizationMethod, SignatureType # noqa:F401
55
from .exceptions import InvalidCertificate, InvalidDigest, InvalidInput, InvalidSignature # noqa:F401

signxml/algorithms.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
class SignatureType(Enum):
99
"""
10-
An enumeration of the structural type of signature supported by SignXML.
10+
An enumeration of structural signature types supported by SignXML.
1111
"""
1212

1313
enveloped = auto()
@@ -100,6 +100,10 @@ class SignatureMethod(FragmentLookupMixin, InvalidInputErrorMixin, Enum):
100100

101101

102102
class CanonicalizationMethod(InvalidInputErrorMixin, Enum):
103+
"""
104+
An enumeration of XML canonicalization methods supported by SignXML. See RFC 9231 for details.
105+
"""
106+
103107
CANONICAL_XML_1_0 = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
104108
CANONICAL_XML_1_0_WITH_COMMENTS = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
105109
CANONICAL_XML_1_1 = "http://www.w3.org/2006/12/xmlc14n11#"

signxml/signer.py

Lines changed: 90 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from base64 import b64encode
2+
from dataclasses import dataclass
23
from typing import List, Optional, Union
34

45
from cryptography.hazmat.primitives.asymmetric import ec, utils
@@ -32,6 +33,26 @@
3233
)
3334

3435

36+
@dataclass
37+
class XMLSignatureReference:
38+
URI: str
39+
"""
40+
The reference URI, for example ``#elementId`` to refer to an element whose Id attribute is set to ``elementId``.
41+
"""
42+
43+
c14n_method: Optional[CanonicalizationMethod] = None
44+
"""
45+
Use this parameter to set a canonicalization method for the reference value that is distinct from that for the
46+
signature itself.
47+
"""
48+
49+
inclusive_ns_prefixes: Optional[List] = None
50+
"""
51+
When using exclusive XML canonicalization, use this parameter to provide a list of XML namespace prefixes whose
52+
declarations should be preserved when canonicalizing the reference value (**InclusiveNamespaces PrefixList**).
53+
"""
54+
55+
3556
class XMLSigner(XMLSignatureProcessor):
3657
"""
3758
Create a new XML Signature Signer object, which can be used to hold configuration information and sign multiple
@@ -80,13 +101,12 @@ def sign(
80101
key=None,
81102
passphrase: Optional[bytes] = None,
82103
cert=None,
83-
reference_uri: Optional[Union[str, List[str]]] = None,
104+
reference_uri: Optional[Union[str, List[str], List[XMLSignatureReference]]] = None,
84105
key_name: Optional[str] = None,
85106
key_info: Optional[_Element] = None,
86107
id_attribute: Optional[str] = None,
87108
always_add_key_value: bool = False,
88-
payload_inclusive_ns_prefixes: Optional[List[str]] = None,
89-
signature_inclusive_ns_prefixes: Optional[List[str]] = None,
109+
inclusive_ns_prefixes: Optional[List[str]] = None,
90110
signature_properties=None,
91111
):
92112
"""
@@ -113,7 +133,8 @@ def sign(
113133
:param reference_uri:
114134
Custom reference URI or list of reference URIs to incorporate into the signature. When ``method`` is set to
115135
``detached`` or ``enveloped``, reference URIs are set to this value and only the referenced elements are
116-
signed.
136+
signed. To specify extra options specific to each reference URI, pass a list of one or more
137+
XMLSignatureReference objects.
117138
:param key_name: Add a KeyName element in the KeyInfo element that may be used by the signer to communicate a
118139
key identifier to the recipient. Typically, KeyName contains an identifier related to the key pair used to
119140
sign the message.
@@ -129,13 +150,13 @@ def sign(
129150
document is already encoded in the certificate (which is in X509Data), so the verifier must either ignore
130151
KeyValue or make sure it matches what's in the certificate. This parameter is provided for compatibility
131152
purposes only.
132-
:param payload_inclusive_ns_prefixes:
133-
Provide a list of XML namespace prefixes whose declarations should be preserved when canonicalizing the
134-
content referenced by the signature (**InclusiveNamespaces PrefixList**).
135-
:param signature_inclusive_ns_prefixes:
153+
:param inclusive_ns_prefixes:
136154
Provide a list of XML namespace prefixes whose declarations should be preserved when canonicalizing the
137-
signature itself (**InclusiveNamespaces PrefixList**).
138-
:type signature_inclusive_ns_prefixes: list
155+
signature (**InclusiveNamespaces PrefixList**).
156+
157+
To specify this value separately for reference canonicalizaition, pass a list of one or more
158+
XMLSignatureReference objects as the ``reference_uri`` keyword argument, and set the
159+
``inclusive_ns_prefixes`` attribute on those objects.
139160
:param signature_properties:
140161
One or more Elements that are to be included in the SignatureProperies section when using the detached
141162
method.
@@ -158,10 +179,7 @@ def sign(
158179
else:
159180
cert_chain = cert
160181

161-
if isinstance(reference_uri, (str, bytes)):
162-
input_reference_uris = [reference_uri]
163-
else:
164-
input_reference_uris = reference_uri # type: ignore
182+
input_references = self._preprocess_reference_uri(reference_uri)
165183

166184
signing_settings = SigningSettings(
167185
key=None,
@@ -179,28 +197,27 @@ def sign(
179197
else:
180198
signing_settings.key = key
181199

182-
sig_root, doc_root, c14n_inputs, reference_uris = self._unpack(data, input_reference_uris)
200+
sig_root, doc_root, c14n_inputs, references = self._unpack(data, input_references)
183201

184202
if self.signature_type == SignatureType.detached and signature_properties is not None:
185-
reference_uris.append("#prop")
203+
references.append(XMLSignatureReference(URI="#prop"))
186204
if signature_properties is not None and not isinstance(signature_properties, list):
187205
signature_properties = [signature_properties]
188206
signature_properties_el = self._build_signature_properties(signature_properties)
189207
c14n_inputs.append(signature_properties_el)
190208

191209
signed_info_node, signature_value_node = self._build_sig(
192210
sig_root,
193-
reference_uris,
194-
c14n_inputs,
195-
sig_insp=signature_inclusive_ns_prefixes,
196-
payload_insp=payload_inclusive_ns_prefixes,
211+
references=references,
212+
c14n_inputs=c14n_inputs,
213+
inclusive_ns_prefixes=inclusive_ns_prefixes,
197214
)
198215

199216
for signature_annotator in self.signature_annotators:
200217
signature_annotator(sig_root, signing_settings=signing_settings)
201218

202219
signed_info_c14n = self._c14n(
203-
signed_info_node, algorithm=self.c14n_alg, inclusive_ns_prefixes=signature_inclusive_ns_prefixes
220+
signed_info_node, algorithm=self.c14n_alg, inclusive_ns_prefixes=inclusive_ns_prefixes
204221
)
205222
if self.sign_alg.name.startswith("HMAC_"):
206223
signer = HMAC(key=key, algorithm=digest_algorithm_implementations[self.sign_alg]())
@@ -238,6 +255,16 @@ def sign(
238255

239256
return doc_root if self.signature_type == SignatureType.enveloped else sig_root
240257

258+
def _preprocess_reference_uri(self, reference_uris):
259+
if reference_uris is None:
260+
return None
261+
if isinstance(reference_uris, (str, bytes)):
262+
reference_uris = [reference_uris]
263+
references = list(
264+
ref if isinstance(ref, XMLSignatureReference) else XMLSignatureReference(URI=ref) for ref in reference_uris
265+
)
266+
return references
267+
241268
def _add_key_info(self, sig_root, signing_settings: SigningSettings):
242269
if self.sign_alg.name.startswith("HMAC_"):
243270
return
@@ -261,25 +288,24 @@ def _add_key_info(self, sig_root, signing_settings: SigningSettings):
261288
else:
262289
sig_root.append(signing_settings.key_info)
263290

264-
def _get_c14n_inputs_from_reference_uris(self, doc_root, reference_uris):
265-
c14n_inputs, new_reference_uris = [], []
266-
for reference_uri in reference_uris:
267-
if not reference_uri.startswith("#"):
268-
reference_uri = "#" + reference_uri
269-
c14n_inputs.append(self.get_root(self._resolve_reference(doc_root, {"URI": reference_uri})))
270-
new_reference_uris.append(reference_uri)
271-
return c14n_inputs, new_reference_uris
291+
def _get_c14n_inputs_from_references(self, doc_root, references: List[XMLSignatureReference]):
292+
c14n_inputs, new_references = [], []
293+
for reference in references:
294+
uri = reference.URI if reference.URI.startswith("#") else "#" + reference.URI
295+
c14n_inputs.append(self.get_root(self._resolve_reference(doc_root, {"URI": uri})))
296+
new_references.append(XMLSignatureReference(URI=uri, c14n_method=reference.c14n_method))
297+
return c14n_inputs, new_references
272298

273-
def _unpack(self, data, reference_uris):
299+
def _unpack(self, data, references: List[XMLSignatureReference]):
274300
sig_root = Element(ds_tag("Signature"), nsmap=self.namespaces)
275301
if self.signature_type == SignatureType.enveloped:
276302
if isinstance(data, (str, bytes)):
277303
raise InvalidInput("When using enveloped signature, **data** must be an XML element")
278304
doc_root = self.get_root(data)
279305
c14n_inputs = [self.get_root(data)]
280-
if reference_uris is not None:
306+
if references is not None:
281307
# Only sign the referenced element(s)
282-
c14n_inputs, reference_uris = self._get_c14n_inputs_from_reference_uris(doc_root, reference_uris)
308+
c14n_inputs, references = self._get_c14n_inputs_from_references(doc_root, references)
283309

284310
signature_placeholders = self._findall(doc_root, "Signature[@Id='placeholder']", anywhere=True)
285311
if len(signature_placeholders) == 0:
@@ -295,19 +321,21 @@ def _unpack(self, data, reference_uris):
295321
else:
296322
raise InvalidInput("Enveloped signature input contains more than one placeholder")
297323

298-
if reference_uris is None:
324+
if references is None:
299325
# Set default reference URIs based on signed data ID attribute values
300-
reference_uris = []
326+
references = []
301327
for c14n_input in c14n_inputs:
302328
payload_id = c14n_input.get("Id", c14n_input.get("ID"))
303-
reference_uris.append("#{}".format(payload_id) if payload_id is not None else "")
329+
uri = "#{}".format(payload_id) if payload_id is not None else ""
330+
references.append(XMLSignatureReference(URI=uri))
304331
elif self.signature_type == SignatureType.detached:
305332
doc_root = self.get_root(data)
306-
if reference_uris is None:
307-
reference_uris = ["#{}".format(data.get("Id", data.get("ID", "object")))]
333+
if references is None:
334+
uri = "#{}".format(data.get("Id", data.get("ID", "object")))
335+
references = [XMLSignatureReference(URI=uri)]
308336
c14n_inputs = [self.get_root(data)]
309337
try:
310-
c14n_inputs, reference_uris = self._get_c14n_inputs_from_reference_uris(doc_root, reference_uris)
338+
c14n_inputs, references = self._get_c14n_inputs_from_references(doc_root, references)
311339
except InvalidInput: # Dummy reference URI
312340
c14n_inputs = [self.get_root(data)]
313341
elif self.signature_type == SignatureType.enveloping:
@@ -317,30 +345,38 @@ def _unpack(self, data, reference_uris):
317345
c14n_inputs[0].text = data
318346
else:
319347
c14n_inputs[0].append(self.get_root(data))
320-
reference_uris = ["#object"]
321-
return sig_root, doc_root, c14n_inputs, reference_uris
348+
references = [XMLSignatureReference(URI="#object")]
349+
return sig_root, doc_root, c14n_inputs, references
322350

323-
def _build_sig(self, sig_root, reference_uris, c14n_inputs, sig_insp, payload_insp):
351+
def _build_sig(self, sig_root, references, c14n_inputs, inclusive_ns_prefixes):
324352
signed_info = SubElement(sig_root, ds_tag("SignedInfo"), nsmap=self.namespaces)
325353
sig_c14n_method = SubElement(signed_info, ds_tag("CanonicalizationMethod"), Algorithm=self.c14n_alg.value)
326-
if sig_insp:
327-
SubElement(sig_c14n_method, ec_tag("InclusiveNamespaces"), PrefixList=" ".join(sig_insp))
354+
if inclusive_ns_prefixes:
355+
SubElement(sig_c14n_method, ec_tag("InclusiveNamespaces"), PrefixList=" ".join(inclusive_ns_prefixes))
328356

329357
SubElement(signed_info, ds_tag("SignatureMethod"), Algorithm=self.sign_alg.value)
330-
for i, reference_uri in enumerate(reference_uris):
331-
reference = SubElement(signed_info, ds_tag("Reference"), URI=reference_uri)
332-
transforms = SubElement(reference, ds_tag("Transforms"))
358+
for i, reference in enumerate(references):
359+
if reference.c14n_method is None:
360+
reference.c14n_method = self.c14n_alg
361+
if reference.inclusive_ns_prefixes is None:
362+
reference.inclusive_ns_prefixes = inclusive_ns_prefixes
363+
reference_node = SubElement(signed_info, ds_tag("Reference"), URI=reference.URI)
364+
transforms = SubElement(reference_node, ds_tag("Transforms"))
333365
if self.signature_type == SignatureType.enveloped:
334366
SubElement(transforms, ds_tag("Transform"), Algorithm=namespaces.ds + "enveloped-signature")
335-
SubElement(transforms, ds_tag("Transform"), Algorithm=self.c14n_alg.value)
367+
SubElement(transforms, ds_tag("Transform"), Algorithm=reference.c14n_method.value)
336368
else:
337-
c14n_xform = SubElement(transforms, ds_tag("Transform"), Algorithm=self.c14n_alg.value)
338-
if payload_insp:
339-
SubElement(c14n_xform, ec_tag("InclusiveNamespaces"), PrefixList=" ".join(payload_insp))
340-
341-
SubElement(reference, ds_tag("DigestMethod"), Algorithm=self.digest_alg.value)
342-
digest_value = SubElement(reference, ds_tag("DigestValue"))
343-
payload_c14n = self._c14n(c14n_inputs[i], algorithm=self.c14n_alg, inclusive_ns_prefixes=payload_insp)
369+
c14n_xform = SubElement(transforms, ds_tag("Transform"), Algorithm=reference.c14n_method.value)
370+
if reference.inclusive_ns_prefixes:
371+
SubElement(
372+
c14n_xform, ec_tag("InclusiveNamespaces"), PrefixList=" ".join(reference.inclusive_ns_prefixes)
373+
)
374+
375+
SubElement(reference_node, ds_tag("DigestMethod"), Algorithm=self.digest_alg.value)
376+
digest_value = SubElement(reference_node, ds_tag("DigestValue"))
377+
payload_c14n = self._c14n(
378+
c14n_inputs[i], algorithm=reference.c14n_method, inclusive_ns_prefixes=reference.inclusive_ns_prefixes
379+
)
344380
digest = self._get_digest(payload_c14n, algorithm=self.digest_alg)
345381
digest_value.text = b64encode(digest).decode()
346382
signature_value = SubElement(sig_root, ds_tag("SignatureValue"))

0 commit comments

Comments
 (0)