signxml
signxml is a Python library that implements the W3C XML Signature standard (XMLDSig), used for payload security in standards like SAML 2.0, XAdES, EBICS, and WS-Security. It provides features for signing and verifying XML documents, including support for X.509 certificate chains and XAdES signatures. The library is actively maintained with regular releases, supporting modern Python versions (3.9-3.13+).
Warnings
- breaking Version 4.0.0 introduced a major infrastructure change, replacing PyOpenSSL with Cryptography for core certificate and key handling. The `ca_path` parameter for specifying CA certificate stores was removed and replaced by `ca_pem_file`.
- breaking Version 4.0.4 contained critical security fixes addressing HMAC algorithm confusion and timing attacks. Running older versions with HMAC-based signatures is highly insecure.
- breaking As of version 4.4.0, DTD (Document Type Declaration) declarations are forbidden in XML input for enhanced security, preventing XML External Entity (XXE) attacks.
- gotcha signxml relies heavily on `lxml.etree` for its advanced XML parsing, canonicalization, and security features. Using Python's standard `xml.etree.ElementTree` can lead to inconsistent behavior, especially with namespace handling, and may bypass lxml's security protections.
- gotcha For robust security, always follow the 'See what is signed' principle. After `XMLVerifier.verify()`, explicitly use the `signed_xml` attribute of the return value, as this is the actual data covered by the signature. Also, for specific standards like SAML, it's a best practice to assert the expected signature location using `SignatureConfiguration(location='./')` to prevent signature wrapping attacks.
- gotcha The default `XMLVerifier().verify()` behavior trusts any valid X.509 certificate that validates against your system's CA store. For production, you must explicitly establish trust using parameters like `x509_cert` (a pre-shared certificate), `cert_subject_name` (to validate the subject name in the signing certificate), or `ca_pem_file` (a custom CA bundle) to prevent unauthorized signatures.
- gotcha XML canonicalization, a crucial step in XML Signature, is highly sensitive to whitespace. Pretty-printing an XML document *after* it has been signed will almost certainly invalidate its signature.
Install
-
pip install signxml
Imports
- XMLSigner
from signxml import XMLSigner
- XMLVerifier
from signxml import XMLVerifier
- etree
from lxml import etree
Quickstart
from lxml import etree
from signxml import XMLSigner, XMLVerifier, SignatureConfiguration
import os
# --- Setup: Generate test certificate and key (requires OpenSSL) ---
# openssl req -x509 -nodes -subj "/CN=test" -days 1 -newkey rsa -keyout privkey.pem -out cert.pem
# In a real application, you would load these from secure storage.
# Ensure cert.pem and privkey.pem exist in the current directory for this example to run.
if not (os.path.exists('cert.pem') and os.path.exists('privkey.pem')):
print("Please generate 'cert.pem' and 'privkey.pem' using OpenSSL as described in the comments.")
exit()
cert = open("cert.pem").read()
key = open("privkey.pem").read()
# --- Signing an XML document ---
data_to_sign = "<Test><Data>Hello World!</Data></Test>"
root = etree.fromstring(data_to_sign)
signer = XMLSigner()
signed_root = signer.sign(root, key=key, cert=cert)
print("\n--- Signed XML ---")
print(etree.tostring(signed_root, pretty_print=True).decode())
# --- Verifying the signed XML document ---
verifier = XMLVerifier()
try:
# It's crucial to explicitly provide the trusted certificate or CA for verification
# and to ensure the returned data is what was expected to prevent signature wrapping attacks.
verified_data = verifier.verify(signed_root, x509_cert=cert).signed_xml
print("\n--- Verification Successful! ---")
print("Signed data content:", etree.tostring(verified_data, pretty_print=False).decode())
# Optionally, assert the signature location (best practice for SAML etc.)
config = SignatureConfiguration(location='./')
verifier.verify(signed_root, x509_cert=cert, expect_config=config)
print("Signature location asserted successfully.")
except Exception as e:
print(f"\n--- Verification Failed: {e} ---")