Skip to content

Add options to exclude the C14N Transform element in signatures #274

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions signxml/signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def sign(
always_add_key_value: bool = False,
inclusive_ns_prefixes: Optional[List[str]] = None,
signature_properties: Optional[Union[_Element, List[_Element]]] = None,
exclude_c14n_transform_element: bool = False,
) -> _Element:
"""
Sign the data and return the root element of the resulting XML tree.
Expand Down Expand Up @@ -183,6 +184,11 @@ def sign(
:param signature_properties:
One or more Elements that are to be included in the SignatureProperies section when using the detached
method.
:param exclude_c14n_transform_element:
When set to True, the c14n algorithm is not added to the signature's Transforms element. By default,
all transforms including canonicalization are added to the Transforms element, and in most cases this
is the correct approach. Only set this to True if you need to comply with a specification where the
c14n algorithm is fixed, and the specification forbids mentioning the algorithm.

:returns:
A :class:`lxml.etree._Element` object representing the root of the XML tree containing the signature and
Expand Down Expand Up @@ -235,6 +241,7 @@ def sign(
references=references,
c14n_inputs=c14n_inputs,
inclusive_ns_prefixes=inclusive_ns_prefixes,
exclude_c14n_transform_element=exclude_c14n_transform_element,
)

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

def _build_transforms_for_reference(self, *, transforms_node: _Element, reference: SignatureReference):
def _build_transforms_for_reference(
self, *, transforms_node: _Element, reference: SignatureReference, exclude_c14n_transform_element: bool = True
):
assert reference.c14n_method is not None
if self.construction_method == SignatureConstructionMethod.enveloped:
SubElement(transforms_node, ds_tag("Transform"), Algorithm=SignatureConstructionMethod.enveloped.value)
SubElement(transforms_node, ds_tag("Transform"), Algorithm=reference.c14n_method.value)
if not exclude_c14n_transform_element:
SubElement(transforms_node, ds_tag("Transform"), Algorithm=reference.c14n_method.value)
else:
c14n_xform = SubElement(
transforms_node,
ds_tag("Transform"),
Algorithm=reference.c14n_method.value,
)
if not exclude_c14n_transform_element:
c14n_xform = SubElement(
transforms_node,
ds_tag("Transform"),
Algorithm=reference.c14n_method.value,
)
if reference.inclusive_ns_prefixes:
SubElement(
c14n_xform, ec_tag("InclusiveNamespaces"), PrefixList=" ".join(reference.inclusive_ns_prefixes)
)

def _build_sig(self, sig_root, references, c14n_inputs, inclusive_ns_prefixes):
def _build_sig(
self, sig_root, references, c14n_inputs, inclusive_ns_prefixes, exclude_c14n_transform_element=False
):
signed_info = SubElement(sig_root, ds_tag("SignedInfo"), nsmap=self.namespaces)
sig_c14n_method = SubElement(signed_info, ds_tag("CanonicalizationMethod"), Algorithm=self.c14n_alg.value)
if inclusive_ns_prefixes:
Expand All @@ -407,7 +420,11 @@ def _build_sig(self, sig_root, references, c14n_inputs, inclusive_ns_prefixes):
reference = replace(reference, inclusive_ns_prefixes=inclusive_ns_prefixes)
reference_node = SubElement(signed_info, ds_tag("Reference"), URI=reference.URI)
transforms = SubElement(reference_node, ds_tag("Transforms"))
self._build_transforms_for_reference(transforms_node=transforms, reference=reference)
self._build_transforms_for_reference(
transforms_node=transforms,
reference=reference,
exclude_c14n_transform_element=exclude_c14n_transform_element,
)
SubElement(reference_node, ds_tag("DigestMethod"), Algorithm=self.digest_alg.value)
digest_value = SubElement(reference_node, ds_tag("DigestValue"))
payload_c14n = self._c14n(
Expand Down
41 changes: 34 additions & 7 deletions signxml/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ class SignatureConfiguration:
and validate the signature using X509Data only.
"""

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
algorithm specified in the Transforms element. In most cases, it should not be necessary to override this
setting; SignedInfo should include every transformation that were applied to the reference before creating
the digests and signature, but there are some uses of XML Signatures where the canonicalization method is
fixed, and not added to the Transforms. This setting can be used to validate signatures in those use-cases.
"""


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

_default_reference_c14n_method = CanonicalizationMethod.CANONICAL_XML_1_0

def _get_signature(self, root):
if root.tag == ds_tag("Signature"):
return root
Expand Down Expand Up @@ -207,7 +214,13 @@ def _get_inclusive_ns_prefixes(self, transform_node):
else:
return inclusive_namespaces.get("PrefixList").split(" ")

def _apply_transforms(self, payload, *, transforms_node: etree._Element, signature: etree._Element):
def _apply_transforms(
self,
payload,
*,
transforms_node: etree._Element,
signature: etree._Element,
):
transforms, c14n_applied = [], False
if transforms_node is not None:
transforms = self._findall(transforms_node, "Transform")
Expand Down Expand Up @@ -239,8 +252,9 @@ def _apply_transforms(self, payload, *, transforms_node: etree._Element, signatu
c14n_applied = True

if not c14n_applied and not isinstance(payload, (str, bytes)):
payload = self._c14n(payload, algorithm=self._default_reference_c14n_method)

# Create a separate copy of the node, see above
payload = self._fromstring(self._tostring(payload))
payload = self._c14n(payload, algorithm=self.config.default_reference_c14n_method)
return payload

def get_cert_chain_verifier(self, ca_pem_file):
Expand Down Expand Up @@ -523,14 +537,27 @@ def verify(

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

def _verify_reference(self, reference, index, root, uri_resolver, c14n_algorithm, signature, signature_key_used):
def _verify_reference(
self,
reference,
index,
root,
uri_resolver,
c14n_algorithm,
signature,
signature_key_used,
):
copied_root = self._fromstring(self._tostring(root))
copied_signature_ref = self._get_signature(copied_root)
transforms = self._find(reference, "Transforms", require=False)
digest_method_alg_name = self._find(reference, "DigestMethod").get("Algorithm")
digest_value = self._find(reference, "DigestValue")
payload = self._resolve_reference(copied_root, reference, uri_resolver=uri_resolver)
payload_c14n = self._apply_transforms(payload, transforms_node=transforms, signature=copied_signature_ref)
payload_c14n = self._apply_transforms(
payload,
transforms_node=transforms,
signature=copied_signature_ref,
)
digest_alg = DigestAlgorithm(digest_method_alg_name)
self.check_digest_alg_expected(digest_alg)

Expand Down
52 changes: 51 additions & 1 deletion test/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ def test_inclusive_namespaces_signing(self):
)

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

def test_include_c14n_transform_element_by_default(self):
cert, key = self.load_example_keys()
doc = etree.fromstring(
'<rDE xmlns="http://example.com/ns1">'
'<DE Id="target"><dDVId>9</dDVId><gOpeDE><!-- comment --><iTipEmi>1</iTipEmi></gOpeDE></DE>'
"</rDE>"
)
root = XMLSigner().sign(doc, cert=cert, key=key, reference_uri="#target")
XMLVerifier().verify(root, x509_cert=cert)
transform_elements = root.findall(
"ds:Signature/ds:SignedInfo/ds:Reference/ds:Transforms/ds:Transform", namespaces=namespaces
)
transform_algorithms = [el.attrib["Algorithm"] for el in transform_elements]
self.assertEqual(len(transform_elements), 2)
self.assertIn("http://www.w3.org/2000/09/xmldsig#enveloped-signature", transform_algorithms)
self.assertIn("http://www.w3.org/2006/12/xml-c14n11", transform_algorithms)

def test_exclude_c14n_transform_element_option(self):
cert, key = self.load_example_keys()
doc = etree.fromstring(
'<rDE xmlns="http://example.com/ns1">'
'<DE Id="target"><dDVId>9</dDVId><gOpeDE><!-- comment --><iTipEmi>1</iTipEmi></gOpeDE></DE>'
"</rDE>"
)
root = XMLSigner(c14n_algorithm=CanonicalizationMethod.CANONICAL_XML_1_0_WITH_COMMENTS).sign(
doc, cert=cert, key=key, reference_uri="#target", exclude_c14n_transform_element=True
)

# The default to use is CANONICAL_XML_1_1 (no comments), and since it's not specified in Transforms,
# verification without specifying the reference canonicalization algorithm should fail.
self.assertRaises(
InvalidDigest,
XMLVerifier().verify,
root,
x509_cert=cert,
)

# However, if we use the right configuration, it should verify correctly
config = SignatureConfiguration(
default_reference_c14n_method=CanonicalizationMethod.CANONICAL_XML_1_0_WITH_COMMENTS
)
XMLVerifier().verify(root, x509_cert=cert, expect_config=config)
transform_elements = root.findall(
"ds:Signature/ds:SignedInfo/ds:Reference/ds:Transforms/ds:Transform", namespaces=namespaces
)
transform_algorithms = [el.attrib["Algorithm"] for el in transform_elements]
self.assertEqual(len(transform_elements), 1)
self.assertIn("http://www.w3.org/2000/09/xmldsig#enveloped-signature", transform_algorithms)
self.assertNotIn("http://www.w3.org/2006/12/xml-c14n11", transform_algorithms)

def test_verify_config(self):
data = etree.parse(self.example_xml_files[0]).getroot()
cert, key = self.load_example_keys()
Expand Down