Skip to content

SignXML Verifier Fails When XML Signature Uses Default Namespace Without ds: Prefix #275

@kalaLokia

Description

@kalaLokia

I'm working on generating a signed XML document where the element (and related elements like , , etc.) must not include the ds: prefix. This is because the legacy application I’m integrating with expects the XML Digital Signature block to be present without any namespace prefixes—only the Signature tag and children in the correct namespace (i.e., http://www.w3.org/2000/09/xmldsig#), but without the ds: prefix.

I’m using signxml to generate the signature. Signing works fine when I override the namespace as:

signer.namespaces = {None: signxml.namespaces.ds}

This gives me the desired XML structure (no ds:), and the output looks valid. However, when I try to verify the signature using signxml.XMLVerifier().verify(...), it fails. The same test passes if I don't override the namespace and let SignXML add the ds: prefix.

Test Result

self = <signxml.verifier.XMLVerifier object at 0xffffadce0c50>, element = <Element {http://www.w3.org/2000/09/xmldsig#}Reference at 0xffffacd2e9c0>, query = 'DigestMethod'
require = True, xpath = ''

    def _find(self, element, query, require=True, xpath=""):
        namespace = "ds"
        if ":" in query:
            namespace, _, query = query.partition(":")
        result = element.find(f"{xpath}{namespace}:{query}", namespaces=namespaces)
    
        if require and result is None:
>           raise InvalidInput(f"Expected to find XML element {query} in {element.tag}")
E           signxml.exceptions.InvalidInput: Expected to find XML element DigestMethod in {http://www.w3.org/2000/09/xmldsig#}Reference

/py/lib/python3.12/site-packages/signxml/processor.py:98: InvalidInput
================================================================================ short test summary info ================================================================================
FAILED test_signxml.py::test_signed_without_ns_success - signxml.exceptions.InvalidInput: Expected to find XML element DigestMethod in {http://www.w3.org/2000/09/xmldsig#}Reference
============================================================================== 1 failed, 1 passed in 0.21s ==============================================================================

Test Code

"""Test Certificate SetUp

.. code-block:: bash

openssl genrsa -out private.key 4096
openssl req -new -key private.key -out request.csr
openssl x509 -req -days 365 -in request.csr -signkey private.key -out certificate.crt


"""

from xml.etree.ElementTree import Element, SubElement, tostring

import lxml
import signxml
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization

PRIVATE_KEY_PATH = "/app/private.key"
CERTIFICATE_PATH = "/app/certificate.crt"


def load_cert_pub(cert_path: str):
    with open(cert_path, "rb") as cert_file:
        cert_data = cert_file.read()
    cert = x509.load_pem_x509_certificate(cert_data, default_backend())
    return cert, cert.public_key()


def load_private_key(key_path: str, password=None):
    with open(key_path, "rb") as key_file:
        key_data = key_file.read()
    key = serialization.load_pem_private_key(
        key_data, password=password, backend=default_backend()
    )
    return key


def create_sample_xml():
    root = Element("ROOT")

    # Section Data
    data_sec = SubElement(root, "DATA")

    pid_elem = SubElement(data_sec, "ID")
    pid_elem.text = "encrypted_some_data_data"

    session_key_elem = SubElement(data_sec, "SESSION_KEY")
    session_key_elem.text = "encrypted_session_key"

    return tostring(root, encoding="unicode")


private_key = load_private_key(PRIVATE_KEY_PATH)
cert, public_key = load_cert_pub(CERTIFICATE_PATH)


def test_signed_with_default_ns_success():
    xml_str = create_sample_xml()
    root = lxml.etree.fromstring(xml_str.encode("utf-8"))

    signer = signxml.XMLSigner(
        method=signxml.methods.enveloped,
        signature_algorithm="rsa-sha256",
        digest_algorithm="sha256",
        c14n_algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
    )

    signed_root = signer.sign(
        root,
        key=private_key,
        exclude_c14n_transform_element=True,
    )

    signed_xml_str = lxml.etree.tostring(signed_root, encoding="utf-8")

    # Verifying
    root = lxml.etree.fromstring(signed_xml_str)

    result = signxml.XMLVerifier().verify(root, x509_cert=cert)

    # No exceptions, so this is okay...


def test_signed_without_ns_success():
    # This test is failing...
    xml_str = create_sample_xml()
    root = lxml.etree.fromstring(xml_str.encode("utf-8"))

    signer = signxml.XMLSigner(
        method=signxml.methods.enveloped,
        signature_algorithm="rsa-sha256",
        digest_algorithm="sha256",
        c14n_algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
    )

    signer.namespaces = {None: signxml.namespaces.ds}
    # signer.signature_annotators = []  # remove key info from payload

    signed_root = signer.sign(
        root,
        key=private_key,
        exclude_c14n_transform_element=True,
    )

    signed_xml_str = lxml.etree.tostring(signed_root, encoding="utf-8")

    # Verifying
    root = lxml.etree.fromstring(signed_xml_str)
    # This going to raise:
    # signxml.exceptions.InvalidInput: Expected to find XML element DigestMethod in {http://www.w3.org/2000/09/xmldsig#}Reference
    result = signxml.XMLVerifier().verify(root, x509_cert=cert)

Side Note:
I’m new to XML digital signatures, and trying to get this working based on the requirements of the external (legacy) application. If I’m missing something or doing in a wrong way, I'd appreciate your help on the correct approach.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions