XML in Production · Security and Integration
The Signed Message That Failed Verification
A payment message signed by the sender failed verification after a gateway pretty-printed it. Byte-level hashing of XML broke on harmless formatting changes.
Scenario
Example team: a payments integration signs each XML instruction by hashing the raw bytes and sending the hash in a header. A new API gateway "normalises" bodies: it reorders attributes alphabetically and reformats empty elements.
What you see in production
All signature checks fail after the gateway rollout; payments queue up. Turning verification off "temporarily" is proposed in the incident channel.
The naive code
signature = hmac(key, raw_body_bytes) # any change in formatting changes the hash
Reproduce it
import hashlib
from lxml import etree
sent = b'<payment currency="EUR" amount="250.00"><payee iban="DE89370400440532013000"></payee></payment>'
received = b'<payment amount="250.00" currency="EUR">\n<payee iban="DE89370400440532013000"/></payment>'
tampered = b'<payment amount="2500.00" currency="EUR"><payee iban="DE89370400440532013000"/></payment>'
def raw(data):
return hashlib.sha256(data).hexdigest()[:16]
def canonical(data):
doc = etree.fromstring(data, etree.XMLParser(remove_blank_text=True))
return hashlib.sha256(etree.tostring(doc, method="c14n")).hexdigest()[:16]
for name, data in (("sent", sent), ("reformatted", received), ("tampered", tampered)):
print(f"{name:12} raw={raw(data)} c14n={canonical(data)}")
print(etree.tostring(etree.fromstring(received, etree.XMLParser(remove_blank_text=True)), method="c14n").decode())
Output
sent raw=7b690a9fa9831752 c14n=6a8f0ac430108c3d reformatted raw=5e1e3e1ee90473a4 c14n=6a8f0ac430108c3d tampered raw=5e99d60490dfce4c c14n=c4f226587a5153c6 <payment amount="250.00" currency="EUR"><payee iban="DE89370400440532013000"></payee></payment>
Root cause
XML has many byte representations of the same information: attribute order, quote style, <a></a> vs <a/>, whitespace inside tags, namespace declarations. Hashing raw bytes makes any intermediary that re-serialises XML break the signature. Hashing after parsing without a defined form is worse: different libraries serialise differently.
The fix
- Use Canonical XML (C14N, or Exclusive C14N for signed fragments) before hashing: a single defined serialisation.
- For real interoperability use XML Signature (XMLDSig) through a maintained library (xmlsec/python-xmlsec, Apache Santuario, .NET
SignedXml), which specifies canonicalisation and references. - Verify the signed element is the one you process. "Signature wrapping" attacks move the signed element and insert an unsigned one where the application reads; look up signed content by the reference ID, never by a separate XPath.
- Don't let gateways rewrite signed bodies; mark them as pass-through.
Trade-offs
- Canonicalisation costs CPU and must match on both sides (C14N 1.0 vs 1.1 vs exclusive, with or without comments).
- Insignificant whitespace in mixed content can be significant; agree whether to strip it.
Detect it in production
- Signature failure rates per sender and per intermediary.
- Alerts when a body's bytes change between the edge and the application (compare hashes in logs).
Key points
- XML signatures need canonical XML, not raw bytes.
- Use a vetted XMLDSig library, and process exactly the element that was signed.
- Never "fix" verification failures by turning verification off.
Practice questions
Why do XML signatures use canonicalization?
Answer
The same XML can be serialised in many ways (attribute order, empty-element syntax, whitespace in tags, namespace declarations). C14N defines one byte representation, so harmless re-serialisation doesn't change the digest while real changes do.
What is an XML signature wrapping attack?
Answer
The attacker keeps the validly signed element but moves it, and inserts an unsigned element where the application reads its data. If verification and processing locate elements differently, the forged data is used. Process only the element referenced by the signature.
Open in the app to mark lessons complete, take notes and track your progress.