xml-crypto
xml-crypto is a Node.js implementation of XML Digital Signature. It can canonicalize selected XML nodes, hash references, create RSA or optional HMAC signatures, embed KeyInfo certificates, locate XMLDSig Signature elements, and verify signed references against a trusted key. It is the low-level cryptographic layer used by SAML tooling, not a SAML validator, certificate trust store, XML Encryption implementation, schema validator, or safe authenticated-object parser by itself.
Use xml-crypto only when XMLDSig interoperability is unavoidable and your code can enforce signer trust, signature count, expected references, and signed-byte consumption. It is actively security-patched, but its easy-looking API sits on a history of critical configuration and wrapping failures, so a protocol library is safer when one fits.
Use it if
- You must interoperate with XMLDSig in SAML, WS-Security, SOAP, or a partner protocol and can test exact canonicalization profiles
- You can pin trusted X.509 certificates or implement explicit signer authorization outside the XML document
- You will treat getSignedReferences() output as the only authenticated XML and parse application data from those bytes
- You need custom canonicalization, digest, signature, or transform hooks and have protocol-level security expertise
- You need XML Encryption: despite the npm description and keywords mentioning encryption, version 6.1.2 exports signature and canonicalization APIs, not an encrypt/decrypt API
- You only need SAML login: @node-saml/node-saml or samlify adds protocol validation, timestamps, audience checks, and assertion handling that this low-level package deliberately does not provide
- You plan to trust a certificate embedded by the sender without validating it against your own trust policy; a 2024 critical advisory showed how that configuration allowed attackers to re-sign malicious XML
- You need RSA-PSS today: the README lists RSA-SHA256 with MGF1, but the 6.1.2 package registers RSA-SHA1, RSA-SHA256, and RSA-SHA512 only, with no PSS implementation in its shipped signature algorithms
- You require HMAC: the only built-in option is HMAC-SHA1, it is disabled by default due to key-confusion history, and 6.1.2 compares its Base64 result with ordinary string equality; the constant-time comparison fix exists on main but is not in the latest npm release
Setup reality
Install xml-crypto on Node 16 or newer. It is JavaScript-only, but pulls @xmldom/xmldom, @xmldom/is-dom-node, and xpath; declare @xmldom/xmldom directly if your application imports DOMParser instead of relying on dependency hoisting. Signing needs a PEM private key, an explicit signature algorithm, an explicit canonicalization algorithm, and at least one reference with XPath, one or more transforms, and a digest algorithm. A PFX file must be converted outside the library, commonly with OpenSSL. Prefer RSA-SHA256 or RSA-SHA512 over the README's older SHA1 examples unless a fixed partner profile leaves no choice. Canonicalization and namespaces are the main interoperability tax: whitespace, inherited namespaces, comments, prefix lists, and signature placement can all change the signed bytes. Verification is not one call. Parse the untrusted document, locate the XMLDSig Signature node, enforce the signature count your protocol expects, construct SignedXml with a pinned publicCert, load that signature separately, and pass the original XML string to checkSignature. The method can return false for a bad reference and throw for malformed input, missing keys, unsupported algorithms, duplicate IDs, or a bad SignatureValue, so handle both. A true result authenticates only referenced subsets, never the surrounding document. Call getSignedReferences(), verify the expected count and semantic target, reparse those canonical XML strings, and read application fields only there. getReferences(), its objects, and getValidatedNode() are deprecated and explicitly unsafe for this purpose. Version 6.0.1 patched two critical 2025 bypasses involving multiple SignedInfo nodes and DigestValue comments; keep 6.1.2 or newer pinned and audit advisories. The package ships TypeScript declarations beside lib even though package.json has no types field.
Patterns
Sign one element with RSA-SHA256sign-element
const fs = require('node:fs');
const { SignedXml } = require('xml-crypto');
const signer = new SignedXml({
privateKey: fs.readFileSync('signing-key.pem'),
signatureAlgorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
canonicalizationAlgorithm: 'http://www.w3.org/2001/10/xml-exc-c14n#',
});
signer.addReference({
xpath: "//*[local-name(.)='Order']",
transforms: ['http://www.w3.org/2001/10/xml-exc-c14n#'],
digestAlgorithm: 'http://www.w3.org/2001/04/xmlenc#sha256',
});
signer.computeSignature(xml);
const signedXml = signer.getSignedXml();Use SHA256 or stronger when the protocol permits. addReference adds an Id when needed, and getSignedXml is valid only after computeSignature.
Include the public certificate in KeyInfoembed-certificate
const signer = new SignedXml({
privateKey: fs.readFileSync('signing-key.pem'),
publicCert: fs.readFileSync('signing-cert.pem'),
signatureAlgorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
canonicalizationAlgorithm: 'http://www.w3.org/2001/10/xml-exc-c14n#',
getKeyInfoContent: SignedXml.getKeyInfoContent,
});Embedding a certificate helps recipients identify a key, but it does not establish trust. Verifiers must pin or authorize that signer independently.
Insert the signature next to a protocol nodeplace-signature
signer.computeSignature(xml, {
prefix: 'ds',
location: {
reference: "//*[local-name(.)='Header']",
action: 'after',
},
});The action must be append, prepend, before, or after. Signature location is protocol-specific and can affect enveloped transforms and interoperability.
Verify exactly one signature with a pinned certificateverify-pinned-certificate
const { DOMParser } = require('@xmldom/xmldom');
const { SignedXml } = require('xml-crypto');
const doc = new DOMParser().parseFromString(xml, 'text/xml');
const verifier = new SignedXml({
publicCert: fs.readFileSync('trusted-signer.pem'),
getCertFromKeyInfo: () => null,
});
const signatures = verifier.findSignatures(doc);
if (signatures.length !== 1) throw new Error('Expected one XML signature');
verifier.loadSignature(signatures[0]);
if (!verifier.checkSignature(xml)) throw new Error('Invalid XML signature');Passing getCertFromKeyInfo: () => null makes the trust decision explicit and prevents a document-provided certificate from replacing the pinned certificate.
Parse only authenticated reference bytesconsume-signed-reference
const signed = verifier.getSignedReferences();
if (signed.length !== 1) throw new Error('Expected one signed reference');
const authenticatedDoc = new DOMParser().parseFromString(signed[0], 'text/xml');
const order = authenticatedDoc.documentElement;
processVerifiedOrder(order);Do not read security decisions from the original doc after verification. A true signature result authenticates referenced subsets, not every sibling or wrapper around them.
Handle false results and thrown errorshandle-verification-failure
let valid = false;
try {
valid = verifier.checkSignature(xml);
} catch (error) {
auditVerificationFailure(error);
}
if (!valid) {
throw new Error('XML signature rejected');
}A reference digest mismatch can return false, while malformed XMLDSig, duplicate ids, missing keys, unsupported algorithms, and invalid SignatureValue paths can throw. Handle both outcomes as rejection.
Resolve namespaces in a signing XPathsign-namespaced-node
signer.addReference({
xpath: '//app:Order',
transforms: ['http://www.w3.org/2001/10/xml-exc-c14n#'],
digestAlgorithm: 'http://www.w3.org/2001/04/xmlenc#sha256',
});
signer.computeSignature(xml, {
existingPrefixes: { app: 'urn:example:orders' },
});Prefix bindings are not inferred for your XPath. existingPrefixes must match the document namespace and is also used to avoid redundant declarations in generated XML.
Enforce the protocol's signed-reference countverify-expected-references
if (!verifier.checkSignature(xml)) throw new Error('Invalid signature');
const signedReferences = verifier.getSignedReferences();
if (signedReferences.length !== expectedReferenceCount) {
throw new Error('Unexpected signed-reference count');
}Cryptographic validity does not prove that the signer covered every element your protocol requires. Check count and meaning, then parse each authenticated reference.
Recognize a protocol-specific id attributeuse-custom-id-attribute
const verifier = new SignedXml({
publicCert: trustedCert,
idAttribute: 'AssertionID',
getCertFromKeyInfo: () => null,
});The custom attribute is checked before built-in Id, ID, and id names. Duplicate matching ids cause verification to throw as a wrapping-attack defense.
Generate WS-Security style idsuse-ws-security-id
const signer = new SignedXml({
idMode: 'wssecurity',
privateKey,
signatureAlgorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
canonicalizationAlgorithm: 'http://www.w3.org/2001/10/xml-exc-c14n#',
});idMode changes generated references to the WS-Security Id namespace. It does not implement SOAP or WS-Security policy around the signature.
Retrieve only the Signature XMLreturn-detached-signature
signer.computeSignature(xml);
const signatureXml = signer.getSignatureXml();
const originalWithIds = signer.getOriginalXmlWithIds();A detached transport often needs both the signature and the exact document with generated Id attributes. Calling these methods before computeSignature returns no usable result.
Match an undocumented canonicalization transformadd-implicit-transform
const verifier = new SignedXml({
publicCert: trustedCert,
getCertFromKeyInfo: () => null,
implicitTransforms: [
'http://www.w3.org/2001/10/xml-exc-c14n#',
],
});Use this only after confirming a partner's actual signing pipeline. Guessing transforms until verification passes can hide protocol defects and makes interoperability behavior harder to audit.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| xmldsigjs | npm | Use a TypeScript XMLDSig implementation built on Web Crypto when browser support or CryptoKey-based code is required |
| @node-saml/node-saml | npm | Use a higher-level Node SAML implementation when signatures are only one part of assertion and response validation |
| samlify | npm | Use a SAML service-provider and identity-provider toolkit when metadata, bindings, schemas, and protocol checks matter |
| xml-encryption | npm | Use an encryption-focused package when the requirement is XML Encryption rather than XML Digital Signature |