Skip to content

Commit 8f46f9d

Browse files
committed
fix: support default namespace in sign/verify round-trip (#275)
1 parent aacb038 commit 8f46f9d

2 files changed

Lines changed: 66 additions & 27 deletions

File tree

signxml/signer.py

Lines changed: 43 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,16 @@ 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(
407+
transforms_node, self._ds_tag("Transform"), Algorithm=SignatureConstructionMethod.enveloped.value
408+
)
393409
if not exclude_c14n_transform_element:
394-
SubElement(transforms_node, ds_tag("Transform"), Algorithm=reference.c14n_method.value)
410+
SubElement(transforms_node, self._ds_tag("Transform"), Algorithm=reference.c14n_method.value)
395411
else:
396412
if not exclude_c14n_transform_element:
397413
c14n_xform = SubElement(
398414
transforms_node,
399-
ds_tag("Transform"),
415+
self._ds_tag("Transform"),
400416
Algorithm=reference.c14n_method.value,
401417
)
402418
if reference.inclusive_ns_prefixes:
@@ -407,41 +423,41 @@ def _build_transforms_for_reference(
407423
def _build_sig(
408424
self, sig_root, references, c14n_inputs, inclusive_ns_prefixes, exclude_c14n_transform_element=False
409425
):
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)
426+
signed_info = SubElement(sig_root, self._ds_tag("SignedInfo"), nsmap=self.namespaces)
427+
sig_c14n_method = SubElement(signed_info, self._ds_tag("CanonicalizationMethod"), Algorithm=self.c14n_alg.value)
412428
if inclusive_ns_prefixes:
413429
SubElement(sig_c14n_method, ec_tag("InclusiveNamespaces"), PrefixList=" ".join(inclusive_ns_prefixes))
414430

415-
SubElement(signed_info, ds_tag("SignatureMethod"), Algorithm=self.sign_alg.value)
431+
SubElement(signed_info, self._ds_tag("SignatureMethod"), Algorithm=self.sign_alg.value)
416432
for i, reference in enumerate(references):
417433
if reference.c14n_method is None:
418434
reference = replace(reference, c14n_method=self.c14n_alg)
419435
if reference.inclusive_ns_prefixes is None:
420436
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"))
437+
reference_node = SubElement(signed_info, self._ds_tag("Reference"), URI=reference.URI)
438+
transforms = SubElement(reference_node, self._ds_tag("Transforms"))
423439
self._build_transforms_for_reference(
424440
transforms_node=transforms,
425441
reference=reference,
426442
exclude_c14n_transform_element=exclude_c14n_transform_element,
427443
)
428-
SubElement(reference_node, ds_tag("DigestMethod"), Algorithm=self.digest_alg.value)
429-
digest_value = SubElement(reference_node, ds_tag("DigestValue"))
444+
SubElement(reference_node, self._ds_tag("DigestMethod"), Algorithm=self.digest_alg.value)
445+
digest_value = SubElement(reference_node, self._ds_tag("DigestValue"))
430446
payload_c14n = self._c14n(
431447
c14n_inputs[i], algorithm=reference.c14n_method, inclusive_ns_prefixes=reference.inclusive_ns_prefixes
432448
)
433449
digest = self._get_digest(payload_c14n, algorithm=self.digest_alg)
434450
digest_value.text = b64encode(digest).decode()
435-
signature_value = SubElement(sig_root, ds_tag("SignatureValue"))
451+
signature_value = SubElement(sig_root, self._ds_tag("SignatureValue"))
436452
return signed_info, signature_value
437453

438454
def _build_signature_properties(self, signature_properties):
439455
# 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"))
456+
obj = Element(self._ds_tag("Object"), attrib={"Id": "prop"}, nsmap=self.namespaces)
457+
signature_properties_el = Element(self._ds_tag("SignatureProperties"))
442458
for i, el in enumerate(signature_properties):
443459
signature_property = Element(
444-
ds_tag("SignatureProperty"),
460+
self._ds_tag("SignatureProperty"),
445461
attrib={
446462
"Id": el.attrib.pop("Id", f"sigprop{i}"),
447463
"Target": el.attrib.pop("Target", f"#sigproptarget{i}"),
@@ -456,17 +472,17 @@ def _serialize_key_value(self, key, key_info_node):
456472
"""
457473
Add the public components of the key to the signature (see https://www.w3.org/TR/xmldsig-core2/#sec-KeyValue).
458474
"""
459-
key_value = SubElement(key_info_node, ds_tag("KeyValue"))
475+
key_value = SubElement(key_info_node, self._ds_tag("KeyValue"))
460476
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"))
477+
rsa_key_value = SubElement(key_value, self._ds_tag("RSAKeyValue"))
478+
modulus = SubElement(rsa_key_value, self._ds_tag("Modulus"))
463479
modulus.text = b64encode(long_to_bytes(key.public_key().public_numbers().n)).decode()
464-
exponent = SubElement(rsa_key_value, ds_tag("Exponent"))
480+
exponent = SubElement(rsa_key_value, self._ds_tag("Exponent"))
465481
exponent.text = b64encode(long_to_bytes(key.public_key().public_numbers().e)).decode()
466482
elif self.sign_alg.name.startswith("DSA_"):
467-
dsa_key_value = SubElement(key_value, ds_tag("DSAKeyValue"))
483+
dsa_key_value = SubElement(key_value, self._ds_tag("DSAKeyValue"))
468484
for field in "p", "q", "g", "y":
469-
e = SubElement(dsa_key_value, ds_tag(field.upper()))
485+
e = SubElement(dsa_key_value, self._ds_tag(field.upper()))
470486

471487
if field == "y":
472488
key_params = key.public_key().public_numbers()

test/test.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,29 @@ 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_sign_verify_default_ns_roundtrip(self):
483+
"""
484+
Test sign/verify round-trip when using default namespace.
485+
486+
See https://github.com/XML-Security/signxml/issues/275
487+
"""
488+
crt, key = self.load_example_keys()
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=key, cert=crt)
493+
494+
signed_info = signed.find(".//SignedInfo")
495+
self.assertIsNotNone(signed_info)
496+
c14n_output = etree.tostring(signed_info, method="c14n").decode()
497+
self.assertNotIn('xmlns=""', c14n_output)
498+
self.assertIn('xmlns="http://www.w3.org/2000/09/xmldsig#"', c14n_output)
499+
500+
signed_data = etree.tostring(signed)
501+
verifier = XMLVerifier()
502+
verifier.excise_empty_xmlns_declarations = True
503+
verifier.verify(signed_data, x509_cert=crt)
504+
482505
def test_elementtree_compat(self):
483506
data = stdlibElementTree.parse(self.example_xml_files[0]).getroot()
484507
signer = XMLSigner()

0 commit comments

Comments
 (0)