Skip to content

Commit 65eeb0e

Browse files
authored
fix: respect namespace config for default namespace C14N (#283)
Fixes #275 ## Problem When `signer.namespaces = {None: namespaces.ds}`, the C14N output contained spurious `xmlns=""` undeclarations on elements inside `<Reference>`, changing the signature. ## Root Cause `ds_tag()` always returns `QName(namespaces.ds, tag)`, creating elements with explicit namespace regardless of how `signer.namespaces` is configured. When lxml canonicalizes these elements with C14N 1.0, it adds `xmlns=""` to undeclare the namespace on nested elements. ## Solution Added `_ds_tag()` method that respects namespace configuration - returns `QName(None, tag)` when default namespace is configured, allowing elements to inherit from nsmap context. ## Test Added `test_default_namespace_c14n_no_xmlns_undeclarations` to verify: - Round-trip signing/verification works with default namespace - No `xmlns=""` undeclarations in SignedInfo C14N output
1 parent 42bc7bf commit 65eeb0e

2 files changed

Lines changed: 68 additions & 27 deletions

File tree

signxml/signer.py

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from cryptography.hazmat.primitives.asymmetric.padding import MGF1, PSS, PKCS1v15
88
from cryptography.hazmat.primitives.hmac import HMAC
99
from cryptography.hazmat.primitives.serialization import Encoding, load_pem_private_key
10-
from lxml.etree import Element, SubElement, _Element
10+
from lxml.etree import Element, QName, SubElement, _Element
1111

1212
from .algorithms import (
1313
CanonicalizationMethod,
@@ -117,6 +117,20 @@ def __init__(
117117
self._parser = None
118118
self.signature_annotators = [self._add_key_info]
119119

120+
def _ds_tag(self, tag):
121+
"""
122+
Create a QName for the ds namespace, respecting the configured namespace mapping.
123+
124+
When the default namespace is set to the ds namespace ({None: namespaces.ds}),
125+
elements should be created without an explicit namespace so they inherit from
126+
the nsmap context. This avoids spurious xmlns="" undeclarations in C14N output.
127+
128+
See https://github.com/XML-Security/signxml/issues/275
129+
"""
130+
if None in self.namespaces and self.namespaces[None] == namespaces.ds:
131+
return QName(None, tag)
132+
return ds_tag(tag)
133+
120134
def check_deprecated_methods(self):
121135
if "SHA1" in self.sign_alg.name or "SHA1" in self.digest_alg.name:
122136
msg = "SHA1-based algorithms are not supported in the default configuration because they are not secure"
@@ -304,19 +318,19 @@ def _add_key_info(self, sig_root, signing_settings: SigningSettings):
304318
if self.sign_alg.name.startswith("HMAC_"):
305319
return
306320
if signing_settings.key_info is None:
307-
key_info = SubElement(sig_root, ds_tag("KeyInfo"))
321+
key_info = SubElement(sig_root, self._ds_tag("KeyInfo"))
308322
if signing_settings.key_name is not None:
309-
keyname = SubElement(key_info, ds_tag("KeyName"))
323+
keyname = SubElement(key_info, self._ds_tag("KeyName"))
310324
keyname.text = signing_settings.key_name
311325

312326
if signing_settings.cert_chain is None or signing_settings.always_add_key_value:
313327
self._serialize_key_value(signing_settings.key, key_info)
314328

315329
if signing_settings.cert_chain is not None:
316330
assert len(signing_settings.cert_chain) > 0
317-
x509_data = SubElement(key_info, ds_tag("X509Data"))
331+
x509_data = SubElement(key_info, self._ds_tag("X509Data"))
318332
for cert in signing_settings.cert_chain:
319-
x509_certificate = SubElement(x509_data, ds_tag("X509Certificate"))
333+
x509_certificate = SubElement(x509_data, self._ds_tag("X509Certificate"))
320334
if isinstance(cert, (str, bytes)):
321335
x509_certificate.text = strip_pem_header(cert)
322336
else:
@@ -333,7 +347,7 @@ def _get_c14n_inputs_from_references(self, doc_root, references: List[SignatureR
333347
return c14n_inputs, new_references
334348

335349
def _unpack(self, data, references: List[SignatureReference]):
336-
sig_root = Element(ds_tag("Signature"), nsmap=self.namespaces)
350+
sig_root = Element(self._ds_tag("Signature"), nsmap=self.namespaces)
337351
if self.construction_method == SignatureConstructionMethod.enveloped:
338352
if isinstance(data, (str, bytes)):
339353
raise InvalidInput("When using enveloped signature, **data** must be an XML element")
@@ -376,7 +390,7 @@ def _unpack(self, data, references: List[SignatureReference]):
376390
c14n_inputs = [self.get_root(data)]
377391
elif self.construction_method == SignatureConstructionMethod.enveloping:
378392
doc_root = sig_root
379-
c14n_inputs = [Element(ds_tag("Object"), nsmap=self.namespaces, Id="object")]
393+
c14n_inputs = [Element(self._ds_tag("Object"), nsmap=self.namespaces, Id="object")]
380394
if isinstance(data, (str, bytes)):
381395
c14n_inputs[0].text = data
382396
else:
@@ -389,14 +403,14 @@ def _build_transforms_for_reference(
389403
):
390404
assert reference.c14n_method is not None
391405
if self.construction_method == SignatureConstructionMethod.enveloped:
392-
SubElement(transforms_node, ds_tag("Transform"), Algorithm=SignatureConstructionMethod.enveloped.value)
406+
SubElement(transforms_node, self._ds_tag("Transform"), Algorithm=SignatureConstructionMethod.enveloped.value)
393407
if not exclude_c14n_transform_element:
394-
SubElement(transforms_node, ds_tag("Transform"), Algorithm=reference.c14n_method.value)
408+
SubElement(transforms_node, self._ds_tag("Transform"), Algorithm=reference.c14n_method.value)
395409
else:
396410
if not exclude_c14n_transform_element:
397411
c14n_xform = SubElement(
398412
transforms_node,
399-
ds_tag("Transform"),
413+
self._ds_tag("Transform"),
400414
Algorithm=reference.c14n_method.value,
401415
)
402416
if reference.inclusive_ns_prefixes:
@@ -407,41 +421,41 @@ def _build_transforms_for_reference(
407421
def _build_sig(
408422
self, sig_root, references, c14n_inputs, inclusive_ns_prefixes, exclude_c14n_transform_element=False
409423
):
410-
signed_info = SubElement(sig_root, ds_tag("SignedInfo"), nsmap=self.namespaces)
411-
sig_c14n_method = SubElement(signed_info, ds_tag("CanonicalizationMethod"), Algorithm=self.c14n_alg.value)
424+
signed_info = SubElement(sig_root, self._ds_tag("SignedInfo"), nsmap=self.namespaces)
425+
sig_c14n_method = SubElement(signed_info, self._ds_tag("CanonicalizationMethod"), Algorithm=self.c14n_alg.value)
412426
if inclusive_ns_prefixes:
413427
SubElement(sig_c14n_method, ec_tag("InclusiveNamespaces"), PrefixList=" ".join(inclusive_ns_prefixes))
414428

415-
SubElement(signed_info, ds_tag("SignatureMethod"), Algorithm=self.sign_alg.value)
429+
SubElement(signed_info, self._ds_tag("SignatureMethod"), Algorithm=self.sign_alg.value)
416430
for i, reference in enumerate(references):
417431
if reference.c14n_method is None:
418432
reference = replace(reference, c14n_method=self.c14n_alg)
419433
if reference.inclusive_ns_prefixes is None:
420434
reference = replace(reference, inclusive_ns_prefixes=inclusive_ns_prefixes)
421-
reference_node = SubElement(signed_info, ds_tag("Reference"), URI=reference.URI)
422-
transforms = SubElement(reference_node, ds_tag("Transforms"))
435+
reference_node = SubElement(signed_info, self._ds_tag("Reference"), URI=reference.URI)
436+
transforms = SubElement(reference_node, self._ds_tag("Transforms"))
423437
self._build_transforms_for_reference(
424438
transforms_node=transforms,
425439
reference=reference,
426440
exclude_c14n_transform_element=exclude_c14n_transform_element,
427441
)
428-
SubElement(reference_node, ds_tag("DigestMethod"), Algorithm=self.digest_alg.value)
429-
digest_value = SubElement(reference_node, ds_tag("DigestValue"))
442+
SubElement(reference_node, self._ds_tag("DigestMethod"), Algorithm=self.digest_alg.value)
443+
digest_value = SubElement(reference_node, self._ds_tag("DigestValue"))
430444
payload_c14n = self._c14n(
431445
c14n_inputs[i], algorithm=reference.c14n_method, inclusive_ns_prefixes=reference.inclusive_ns_prefixes
432446
)
433447
digest = self._get_digest(payload_c14n, algorithm=self.digest_alg)
434448
digest_value.text = b64encode(digest).decode()
435-
signature_value = SubElement(sig_root, ds_tag("SignatureValue"))
449+
signature_value = SubElement(sig_root, self._ds_tag("SignatureValue"))
436450
return signed_info, signature_value
437451

438452
def _build_signature_properties(self, signature_properties):
439453
# FIXME: make this use the annotator API
440-
obj = Element(ds_tag("Object"), attrib={"Id": "prop"}, nsmap=self.namespaces)
441-
signature_properties_el = Element(ds_tag("SignatureProperties"))
454+
obj = Element(self._ds_tag("Object"), attrib={"Id": "prop"}, nsmap=self.namespaces)
455+
signature_properties_el = Element(self._ds_tag("SignatureProperties"))
442456
for i, el in enumerate(signature_properties):
443457
signature_property = Element(
444-
ds_tag("SignatureProperty"),
458+
self._ds_tag("SignatureProperty"),
445459
attrib={
446460
"Id": el.attrib.pop("Id", f"sigprop{i}"),
447461
"Target": el.attrib.pop("Target", f"#sigproptarget{i}"),
@@ -456,17 +470,17 @@ def _serialize_key_value(self, key, key_info_node):
456470
"""
457471
Add the public components of the key to the signature (see https://www.w3.org/TR/xmldsig-core2/#sec-KeyValue).
458472
"""
459-
key_value = SubElement(key_info_node, ds_tag("KeyValue"))
473+
key_value = SubElement(key_info_node, self._ds_tag("KeyValue"))
460474
if self.sign_alg.name.startswith("RSA_") or self.sign_alg.name.startswith("SHA"):
461-
rsa_key_value = SubElement(key_value, ds_tag("RSAKeyValue"))
462-
modulus = SubElement(rsa_key_value, ds_tag("Modulus"))
475+
rsa_key_value = SubElement(key_value, self._ds_tag("RSAKeyValue"))
476+
modulus = SubElement(rsa_key_value, self._ds_tag("Modulus"))
463477
modulus.text = b64encode(long_to_bytes(key.public_key().public_numbers().n)).decode()
464-
exponent = SubElement(rsa_key_value, ds_tag("Exponent"))
478+
exponent = SubElement(rsa_key_value, self._ds_tag("Exponent"))
465479
exponent.text = b64encode(long_to_bytes(key.public_key().public_numbers().e)).decode()
466480
elif self.sign_alg.name.startswith("DSA_"):
467-
dsa_key_value = SubElement(key_value, ds_tag("DSAKeyValue"))
481+
dsa_key_value = SubElement(key_value, self._ds_tag("DSAKeyValue"))
468482
for field in "p", "q", "g", "y":
469-
e = SubElement(dsa_key_value, ds_tag(field.upper()))
483+
e = SubElement(dsa_key_value, self._ds_tag(field.upper()))
470484

471485
if field == "y":
472486
key_params = key.public_key().public_numbers()

test/test.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,33 @@ def test_changing_signature_namespace_prefix_to_default(self):
479479
expected_match = f'<Signature xmlns="{namespaces["ds"]}">'
480480
self.assertTrue(re.search(expected_match.encode("ascii"), signed_data))
481481

482+
def test_default_namespace_c14n_no_xmlns_undeclarations(self):
483+
"""
484+
Test that using default namespace doesn't produce xmlns="" undeclarations in C14N.
485+
486+
When signer.namespaces = {None: ds_namespace}, the SignedInfo C14N should not
487+
contain xmlns="" on child elements. This regression was reported in issue #275.
488+
"""
489+
data = etree.parse(self.example_xml_files[0]).getroot()
490+
signer = XMLSigner()
491+
signer.namespaces = {None: namespaces["ds"]}
492+
signed = signer.sign(data, key=self.keys["rsa"])
493+
494+
# Verify signature round-trips correctly
495+
XMLVerifier().verify(signed, x509_cert=self.certs["example"])
496+
497+
# Extract SignedInfo and canonicalize it
498+
signed_info = signed.find(".//{http://www.w3.org/2000/09/xmldsig#}SignedInfo")
499+
c14n_output = etree.tostring(signed_info, method="c14n").decode()
500+
501+
# SignedInfo should have xmlns declaration, but child elements should NOT have xmlns=""
502+
# Count occurrences: should be exactly 1 (on SignedInfo itself)
503+
xmlns_count = c14n_output.count('xmlns="')
504+
self.assertEqual(xmlns_count, 1, f"Expected 1 xmlns declaration, found {xmlns_count}. C14N output: {c14n_output}")
505+
506+
# Specifically verify no xmlns="" undeclarations
507+
self.assertNotIn('xmlns=""', c14n_output, f"Found xmlns='' undeclaration in C14N output: {c14n_output}")
508+
482509
def test_elementtree_compat(self):
483510
data = stdlibElementTree.parse(self.example_xml_files[0]).getroot()
484511
signer = XMLSigner()

0 commit comments

Comments
 (0)