SignXML: Python XML Signature and XAdES Library

repository·main·Indexed 41 days ago

https://github.com/xml-security/signxml

A Python implementation of the W3C XML Signature (XMLDSig) Syntax and Processing Version 1.1 standard. It provides payload security for protocols such as SAML 2.0, XAdES, EBICS, and WS-Security. The library includes tools for signing and verifying XML elements via XMLSigner and XMLVerifier, supports X.509 certificate chains, and offers configurable digest, signature, and canonicalization methods.

Tokens
8.5K
Snippets
27
Records
33
Agent score
99%

What's inside SignXML

  1. Overview of SignXML features

    main

    SignXML is a Python implementation of the W3C XML Signature Syntax and Processing Version 1.1 standard. Key features include:

    • Security: Uses a libxml2-based XML parser configured to defend against common XML attacks during signature verification.
    • X.509 Support: Extensions for signing and verifying X.509 certificate chains, including hostname/CN validation.
    • XAdES: Support for signing and verifying XAdES signatures.
    • Canonicalization: Support for exclusive XML canonicalization with inclusive prefixes (InclusiveNamespaces PrefixList), which is required for compatibility with certain SAML implementations.
    • Compatibility: Supports modern Python versions (3.9-3.13+) and PyPy.
    • Dependencies: Built on lxml and cryptography.
    • Secure Defaults: Provides a simple interface with ergonomic defaults that avoid network calls, XSLT, or XPath transforms.
  2. Secure XML Verification with SignatureConfiguration

    main

    To prevent attacks like SAML signature wrapping, follow the "See what is signed" rule: only trust the data returned by XMLVerifier.verify().signed_xml.

    Additionally, you should assert the expected location of the signature within the document using SignatureConfiguration. The location parameter describes the expected parent path for the ds:Signature element. SignXML appends ds:Signature to this path internally.

    Example for a SAML Response containing a signed Assertion child:

    from signxml import XMLVerifier, SignatureConfiguration
    
    config = SignatureConfiguration(
        location="./{urn:oasis:names:tc:SAML:2.0:assertion}Assertion/"
    )
    XMLVerifier().verify(response_xml, x509_cert=cert, expect_config=config)
    from signxml import XMLVerifier, SignatureConfiguration
    config = SignatureConfiguration(location="./")
    XMLVerifier(...).verify(..., expect_config=config)
  3. Basic XML Signing and Verification

    main

    SignXML uses the lxml ElementTree API to handle XML data. You can sign an XML element using XMLSigner and verify it using XMLVerifier. To ensure a self-sufficient test environment, you can generate a test certificate and key using OpenSSL:

    openssl req -x509 -nodes -subj "/CN=test" -days 1 -newkey rsa -keyout privkey.pem -out cert.pem -addext "keyUsage = digitalSignature"

    When verifying, it is critical to specify the trusted certificate using the x509_cert keyword argument to prevent trusting any arbitrary valid signature.

    from lxml import etree
    from signxml import XMLSigner, XMLVerifier
    
    data_to_sign = "<Test/>"
    cert = open("cert.pem").read()
    key = open("privkey.pem").read()
    root = etree.fromstring(data_to_sign)
    
    # Signing
    signed_root = XMLSigner().sign(root, key=key, cert=cert)
    
    # Verifying
    verified_data = XMLVerifier().verify(signed_root, x509_cert=cert).signed_xml
  4. Verifying SAML Assertions

    main

    To verify SAML assertions, decode the assertion body (e.g., from Base64) and pass the certificate extracted from the SAML metadata to XMLVerifier.verify().

    Signing SAML Assertions: The SAML schema requires the signature to be placed between <Issuer> and <Subject>. To comply with the schema, insert a signature placeholder tag at that location before calling XMLSigner: <ds:Signature Id="placeholder"></ds:Signature>.

    from lxml import etree
    from base64 import b64decode
    from signxml import XMLVerifier
    
    # Assuming cert is extracted from metadata.xml
    with open("metadata.xml", "rb") as fh:
        cert = etree.parse(fh).find("//ds:X509Certificate").text
    
    assertion_data = XMLVerifier().verify(b64decode(assertion_body), x509_cert=cert).signed_xml
  5. How to verify HMAC signatures

    main

    To verify an HMAC-based XML signature, you must set require_x509=False in your SignatureConfiguration and provide the shared secret via the hmac_key parameter in verify().

    Note: When hmac_key is provided, SignXML automatically adjusts the expected signature methods to include HMAC algorithms.

    from signxml import XMLVerifier, SignatureConfiguration
    
    # 1. Configure to not require X.509
    config = SignatureConfiguration(require_x509=False)
    
    # 2. Provide the shared secret via hmac_key
    verifier = XMLVerifier()
    result = verifier.verify(data, hmac_key=b'my_shared_secret', expect_config=config)
  6. Configure Namespace Prefixes and Whitespace

    main

    Namespace Prefixes

    If your application requires specific namespace prefixes (e.g., making http://www.w3.org/2000/09/xmldsig# the default unprefixed namespace instead of ds:), set the namespaces attribute on the XMLSigner instance.

    signer = signxml.XMLSigner(...)
    signer.namespaces = {None: signxml.namespaces.ds}
    signed_root = signer.sign(...)

    Whitespace

    Whitespace is significant for XML canonicalization. Do not pretty-print the XML after generating a signature, as this can invalidate the signature.

  7. Select a Signature Construction Method

    main

    When signing XML, you must specify a SignatureConstructionMethod to define how the signature relates to the XML content. The most common method is enveloped, where the signature is an element within the XML it signs.

    Supported methods:

    • enveloped: The signature is over the XML content that contains the signature as an element. Most common in modern applications.
    • enveloping: The signature is over content found within an Object element of the signature itself.
    • detached: The signature is over content external to the Signature element (e.g., separate data objects or sibling elements).
    from signxml import SignatureConstructionMethod
    
    # Example usage (contextual)
    method = SignatureConstructionMethod.enveloped
  8. How to verify signatures with expired or future certificates

    main

    By default, XMLVerifier uses the current system time to validate the notBefore and notAfter periods of X.509 certificates. If you need to verify a signature that was created in the past using a certificate that is now expired, or a signature that will be valid in the future, use the verification_time parameter in SignatureConfiguration.

    from signxml import XMLVerifier, SignatureConfiguration
    from datetime import datetime, timezone, timedelta
    
    # Set verification time to 1 year ago to validate an expired certificate
    past_time = datetime.now(timezone.utc) - timedelta(days=365)
    config = SignatureConfiguration(verification_time=past_time)
    
    verifier = XMLVerifier()
    result = verifier.verify(data, expect_config=config)
  9. Verify SHA1-based signatures

    main

    Because SHA1-based algorithms are insecure, SignXML disables them by default. To verify a signature that uses a SHA1 algorithm (e.g., RSA_SHA1), you must explicitly include the expected algorithms in your SignatureConfiguration when calling XMLVerifier().verify().

    from signxml import XMLVerifier, SignatureConfiguration, SignatureMethod, DigestAlgorithm
    
    # To verify a legacy SHA1 signature:
    verifier = XMLVerifier()
    verifier.verify(
        xml_data,
        expect_config=SignatureConfiguration(
            signature_methods=[SignatureMethod.RSA_SHA1],
            digest_algorithms=[DigestAlgorithm.SHA1]
        )
    )
  10. Configure XML Signature Construction Methods

    main

    The XML Signature specification supports three methods: enveloped (default), detached, and enveloping. Use the method argument in XMLSigner.sign() to switch types.

    For detached signatures:

    • SignXML uses the Id or ID attribute of the root to generate a relative URI (<Reference URI="#value").
    • You can override this by passing reference_uri to sign().
    • To verify a detached signature referring to an external entity, pass a callable resolver to XMLVerifier.verify(..., uri_resolver=...).
    import signxml
    # Detached signature example
    signed_root = XMLSigner(method=signxml.methods.detached).sign(root, key=key, cert=cert)
  11. Use XAdES for Advanced Electronic Signatures

    main

    SignXML supports XAdES (XML Advanced Electronic Signatures) for attaching metadata to XML Signature objects. This involves using XAdESSigner and XAdESVerifier along with XAdESSignaturePolicy and XAdESDataObjectFormat.

    When verifying, you can check the number of expected references and the signature policy, and access parsed XAdES properties via the signed_properties attribute of the XAdESVerifyResult.

    from signxml import DigestAlgorithm
    from signxml.xades import (
        XAdESSigner, XAdESVerifier, XAdESVerifyResult, 
        XAdESSignaturePolicy, XAdESDataObjectFormat
    )
    
    signature_policy = XAdESSignaturePolicy(
        Identifier="MyPolicyIdentifier",
        Description="Hello XAdES",
        DigestMethod=DigestAlgorithm.SHA256,
        DigestValue="Ohixl6upD6av8N7pEvDABhEL6hM=",
    )
    
    data_object_format = XAdESDataObjectFormat(
        Description="My XAdES signature",
        MimeType="text/xml",
    )
    
    signer = XAdESSigner(
        signature_policy=signature_policy,
        claimed_roles=["signer"],
        data_object_format=data_object_format,
        c14n_algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
    )
    signed_doc = signer.sign(doc, key=private_key, cert=certificate)
    
    # Verification
    verifier = XAdESVerifier()
    verify_results = verifier.verify(
        signed_doc, 
        x509_cert=certificate, 
        expect_references=3, 
        expect_signature_policy=signature_policy
    )
    for verify_result in verify_results:
        if isinstance(verify_result, XAdESVerifyResult):
            props = verify_result.signed_properties