Skip to content

Commit f956538

Browse files
authored
Add options to exclude the C14N Transform element in signatures (#274)
This patch adds a flag 'exclude_c14n_transform_element' (boolean, defaults to False) to XMLSigner.sign(). If set to True, the transform is still applied, but it is not added to the Transforms element in ds:SignedInfo/ds:Reference. Conversely, this patch adds an option to XMLVerifier.verify() to specify a default canonicalization algorithm in the case where SignedInfo does not include the c14n Transform information. Rationale: In most cases, the Transforms element should indeed contain every modification made to the source data before creating or verifying the signature, and indeed, the necessity to add a default algorithm in verify() shows why. However, there are specifications that make use of XML Signatures, where the canonicalization algorithm is fixed, and which even go so far as to forbid naming the c14n algorithm in the Transforms element. This patch aims to make it possible to support those specifications.
1 parent 0dfb60b commit f956538

File tree

3 files changed

+111
-17
lines changed

3 files changed

+111
-17
lines changed

signxml/signer.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ def sign(
136136
always_add_key_value: bool = False,
137137
inclusive_ns_prefixes: Optional[List[str]] = None,
138138
signature_properties: Optional[Union[_Element, List[_Element]]] = None,
139+
exclude_c14n_transform_element: bool = False,
139140
) -> _Element:
140141
"""
141142
Sign the data and return the root element of the resulting XML tree.
@@ -183,6 +184,11 @@ def sign(
183184
:param signature_properties:
184185
One or more Elements that are to be included in the SignatureProperies section when using the detached
185186
method.
187+
:param exclude_c14n_transform_element:
188+
When set to True, the c14n algorithm is not added to the signature's Transforms element. By default,
189+
all transforms including canonicalization are added to the Transforms element, and in most cases this
190+
is the correct approach. Only set this to True if you need to comply with a specification where the
191+
c14n algorithm is fixed, and the specification forbids mentioning the algorithm.
186192
187193
:returns:
188194
A :class:`lxml.etree._Element` object representing the root of the XML tree containing the signature and
@@ -235,6 +241,7 @@ def sign(
235241
references=references,
236242
c14n_inputs=c14n_inputs,
237243
inclusive_ns_prefixes=inclusive_ns_prefixes,
244+
exclude_c14n_transform_element=exclude_c14n_transform_element,
238245
)
239246

240247
for signature_annotator in self.signature_annotators:
@@ -377,23 +384,29 @@ def _unpack(self, data, references: List[SignatureReference]):
377384
references = [SignatureReference(URI="#object")]
378385
return sig_root, doc_root, c14n_inputs, references
379386

380-
def _build_transforms_for_reference(self, *, transforms_node: _Element, reference: SignatureReference):
387+
def _build_transforms_for_reference(
388+
self, *, transforms_node: _Element, reference: SignatureReference, exclude_c14n_transform_element: bool = True
389+
):
381390
assert reference.c14n_method is not None
382391
if self.construction_method == SignatureConstructionMethod.enveloped:
383392
SubElement(transforms_node, ds_tag("Transform"), Algorithm=SignatureConstructionMethod.enveloped.value)
384-
SubElement(transforms_node, ds_tag("Transform"), Algorithm=reference.c14n_method.value)
393+
if not exclude_c14n_transform_element:
394+
SubElement(transforms_node, ds_tag("Transform"), Algorithm=reference.c14n_method.value)
385395
else:
386-
c14n_xform = SubElement(
387-
transforms_node,
388-
ds_tag("Transform"),
389-
Algorithm=reference.c14n_method.value,
390-
)
396+
if not exclude_c14n_transform_element:
397+
c14n_xform = SubElement(
398+
transforms_node,
399+
ds_tag("Transform"),
400+
Algorithm=reference.c14n_method.value,
401+
)
391402
if reference.inclusive_ns_prefixes:
392403
SubElement(
393404
c14n_xform, ec_tag("InclusiveNamespaces"), PrefixList=" ".join(reference.inclusive_ns_prefixes)
394405
)
395406

396-
def _build_sig(self, sig_root, references, c14n_inputs, inclusive_ns_prefixes):
407+
def _build_sig(
408+
self, sig_root, references, c14n_inputs, inclusive_ns_prefixes, exclude_c14n_transform_element=False
409+
):
397410
signed_info = SubElement(sig_root, ds_tag("SignedInfo"), nsmap=self.namespaces)
398411
sig_c14n_method = SubElement(signed_info, ds_tag("CanonicalizationMethod"), Algorithm=self.c14n_alg.value)
399412
if inclusive_ns_prefixes:
@@ -407,7 +420,11 @@ def _build_sig(self, sig_root, references, c14n_inputs, inclusive_ns_prefixes):
407420
reference = replace(reference, inclusive_ns_prefixes=inclusive_ns_prefixes)
408421
reference_node = SubElement(signed_info, ds_tag("Reference"), URI=reference.URI)
409422
transforms = SubElement(reference_node, ds_tag("Transforms"))
410-
self._build_transforms_for_reference(transforms_node=transforms, reference=reference)
423+
self._build_transforms_for_reference(
424+
transforms_node=transforms,
425+
reference=reference,
426+
exclude_c14n_transform_element=exclude_c14n_transform_element,
427+
)
411428
SubElement(reference_node, ds_tag("DigestMethod"), Algorithm=self.digest_alg.value)
412429
digest_value = SubElement(reference_node, ds_tag("DigestValue"))
413430
payload_c14n = self._c14n(

signxml/verifier.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ class SignatureConfiguration:
8787
and validate the signature using X509Data only.
8888
"""
8989

90+
default_reference_c14n_method: CanonicalizationMethod = CanonicalizationMethod.CANONICAL_XML_1_1
91+
"""
92+
The default canonicalization method to use for referenced data structures, if there is no canonicalization
93+
algorithm specified in the Transforms element. In most cases, it should not be necessary to override this
94+
setting; SignedInfo should include every transformation that were applied to the reference before creating
95+
the digests and signature, but there are some uses of XML Signatures where the canonicalization method is
96+
fixed, and not added to the Transforms. This setting can be used to validate signatures in those use-cases.
97+
"""
98+
9099

91100
@dataclass(frozen=True)
92101
class VerifyResult:
@@ -116,8 +125,6 @@ class XMLVerifier(XMLSignatureProcessor):
116125
Create a new XML Signature Verifier object, which can be used to verify multiple pieces of data.
117126
"""
118127

119-
_default_reference_c14n_method = CanonicalizationMethod.CANONICAL_XML_1_0
120-
121128
def _get_signature(self, root):
122129
if root.tag == ds_tag("Signature"):
123130
return root
@@ -207,7 +214,13 @@ def _get_inclusive_ns_prefixes(self, transform_node):
207214
else:
208215
return inclusive_namespaces.get("PrefixList").split(" ")
209216

210-
def _apply_transforms(self, payload, *, transforms_node: etree._Element, signature: etree._Element):
217+
def _apply_transforms(
218+
self,
219+
payload,
220+
*,
221+
transforms_node: etree._Element,
222+
signature: etree._Element,
223+
):
211224
transforms, c14n_applied = [], False
212225
if transforms_node is not None:
213226
transforms = self._findall(transforms_node, "Transform")
@@ -239,8 +252,9 @@ def _apply_transforms(self, payload, *, transforms_node: etree._Element, signatu
239252
c14n_applied = True
240253

241254
if not c14n_applied and not isinstance(payload, (str, bytes)):
242-
payload = self._c14n(payload, algorithm=self._default_reference_c14n_method)
243-
255+
# Create a separate copy of the node, see above
256+
payload = self._fromstring(self._tostring(payload))
257+
payload = self._c14n(payload, algorithm=self.config.default_reference_c14n_method)
244258
return payload
245259

246260
def get_cert_chain_verifier(self, ca_pem_file):
@@ -523,14 +537,27 @@ def verify(
523537

524538
return verify_results if self.config.expect_references > 1 else verify_results[0]
525539

526-
def _verify_reference(self, reference, index, root, uri_resolver, c14n_algorithm, signature, signature_key_used):
540+
def _verify_reference(
541+
self,
542+
reference,
543+
index,
544+
root,
545+
uri_resolver,
546+
c14n_algorithm,
547+
signature,
548+
signature_key_used,
549+
):
527550
copied_root = self._fromstring(self._tostring(root))
528551
copied_signature_ref = self._get_signature(copied_root)
529552
transforms = self._find(reference, "Transforms", require=False)
530553
digest_method_alg_name = self._find(reference, "DigestMethod").get("Algorithm")
531554
digest_value = self._find(reference, "DigestValue")
532555
payload = self._resolve_reference(copied_root, reference, uri_resolver=uri_resolver)
533-
payload_c14n = self._apply_transforms(payload, transforms_node=transforms, signature=copied_signature_ref)
556+
payload_c14n = self._apply_transforms(
557+
payload,
558+
transforms_node=transforms,
559+
signature=copied_signature_ref,
560+
)
534561
digest_alg = DigestAlgorithm(digest_method_alg_name)
535562
self.check_digest_alg_expected(digest_alg)
536563

test/test.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ def test_inclusive_namespaces_signing(self):
585585
)
586586

587587
# Test correct default c14n method for payload when c14n transform metadata is omitted
588-
def _build_transforms_for_reference(transforms_node, reference):
588+
def _build_transforms_for_reference(transforms_node, reference, exclude_c14n_transform_element=False):
589589
etree.SubElement(
590590
transforms_node, ds_tag("Transform"), Algorithm=SignatureConstructionMethod.enveloped.value
591591
)
@@ -676,6 +676,56 @@ def test_xmlns_insulation_of_reference_c14n(self):
676676
root = XMLSigner().sign(doc, cert=cert, key=key, reference_uri="#target")
677677
XMLVerifier().verify(root, x509_cert=cert)
678678

679+
def test_include_c14n_transform_element_by_default(self):
680+
cert, key = self.load_example_keys()
681+
doc = etree.fromstring(
682+
'<rDE xmlns="http://example.com/ns1">'
683+
'<DE Id="target"><dDVId>9</dDVId><gOpeDE><!-- comment --><iTipEmi>1</iTipEmi></gOpeDE></DE>'
684+
"</rDE>"
685+
)
686+
root = XMLSigner().sign(doc, cert=cert, key=key, reference_uri="#target")
687+
XMLVerifier().verify(root, x509_cert=cert)
688+
transform_elements = root.findall(
689+
"ds:Signature/ds:SignedInfo/ds:Reference/ds:Transforms/ds:Transform", namespaces=namespaces
690+
)
691+
transform_algorithms = [el.attrib["Algorithm"] for el in transform_elements]
692+
self.assertEqual(len(transform_elements), 2)
693+
self.assertIn("http://www.w3.org/2000/09/xmldsig#enveloped-signature", transform_algorithms)
694+
self.assertIn("http://www.w3.org/2006/12/xml-c14n11", transform_algorithms)
695+
696+
def test_exclude_c14n_transform_element_option(self):
697+
cert, key = self.load_example_keys()
698+
doc = etree.fromstring(
699+
'<rDE xmlns="http://example.com/ns1">'
700+
'<DE Id="target"><dDVId>9</dDVId><gOpeDE><!-- comment --><iTipEmi>1</iTipEmi></gOpeDE></DE>'
701+
"</rDE>"
702+
)
703+
root = XMLSigner(c14n_algorithm=CanonicalizationMethod.CANONICAL_XML_1_0_WITH_COMMENTS).sign(
704+
doc, cert=cert, key=key, reference_uri="#target", exclude_c14n_transform_element=True
705+
)
706+
707+
# The default to use is CANONICAL_XML_1_1 (no comments), and since it's not specified in Transforms,
708+
# verification without specifying the reference canonicalization algorithm should fail.
709+
self.assertRaises(
710+
InvalidDigest,
711+
XMLVerifier().verify,
712+
root,
713+
x509_cert=cert,
714+
)
715+
716+
# However, if we use the right configuration, it should verify correctly
717+
config = SignatureConfiguration(
718+
default_reference_c14n_method=CanonicalizationMethod.CANONICAL_XML_1_0_WITH_COMMENTS
719+
)
720+
XMLVerifier().verify(root, x509_cert=cert, expect_config=config)
721+
transform_elements = root.findall(
722+
"ds:Signature/ds:SignedInfo/ds:Reference/ds:Transforms/ds:Transform", namespaces=namespaces
723+
)
724+
transform_algorithms = [el.attrib["Algorithm"] for el in transform_elements]
725+
self.assertEqual(len(transform_elements), 1)
726+
self.assertIn("http://www.w3.org/2000/09/xmldsig#enveloped-signature", transform_algorithms)
727+
self.assertNotIn("http://www.w3.org/2006/12/xml-c14n11", transform_algorithms)
728+
679729
def test_verify_config(self):
680730
data = etree.parse(self.example_xml_files[0]).getroot()
681731
cert, key = self.load_example_keys()

0 commit comments

Comments
 (0)