mrkeyoor.com_
Sat 08 Aug 21:58 UTC
npmSecurityupdated 08 Aug 2026

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.

Verdict

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.

API stability3/5The core SignedXml workflow, algorithm URI registries, addReference, computeSignature, loadSignature, and checkSignature are long-established, and version 6 ships declarations for its options and extension interfaces. Security work has necessarily changed safe usage: v6 disabled implicit KeyInfo certificate trust, v6.1 introduced getSignedReferences, and v6.1.1 deprecated getReferences and reference access. Source comments also identify possible v7 breaking changes around SHA1 and verification order, so upgrades require security review, not blind semver confidence.
Docs3/5The README is extensive, enumerates algorithms and constructor options, explains signing, verification, implicit transforms, key formats, custom algorithms, async callbacks, signature placement, and most importantly warns readers to use only getSignedReferences output. It loses substantial credit because primary signing examples still choose SHA1, the package description promises encryption it does not expose, RSA-PSS with MGF1 is listed without a shipped implementation, the DOM install guidance is confusing, and secure protocol-level constraints remain spread across warnings rather than one complete hardened example.
Maintenance3/5The project released 6.0.1 promptly in March 2025 for two critical bypasses, then 6.1.0 through 6.1.2 added authenticated-reference handling and failure cleanup. GitHub shows continued commits through November 2025 and a repository push in March 2026. However, npm's latest release remains April 2025, 56 issues and pull requests are open combined, and the constant-time HMAC comparison fix visible on main has not reached the published 6.1.2 package, leaving a meaningful gap between source activity and consumable fixes.
Ecosystem4/5The package recorded 3,817,191 downloads in the measured week and underpins Node SAML implementations, giving it real interoperability exposure across identity systems. It supports the principal XML canonicalization forms, SHA-family digests, RSA signatures, XPath selection, WS-Security ids, PEM certificates, and custom algorithms. The ecosystem is specialized rather than broad, and users still need separate SAML, schema, trust, encryption, and policy layers; its 213 GitHub stars are modest for software handling authentication boundaries.

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
Skip it if

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

PackageRegistryPick it when
xmldsigjsnpmUse a TypeScript XMLDSig implementation built on Web Crypto when browser support or CryptoKey-based code is required
@node-saml/node-samlnpmUse a higher-level Node SAML implementation when signatures are only one part of assertion and response validation
samlifynpmUse a SAML service-provider and identity-provider toolkit when metadata, bindings, schemas, and protocol checks matter
xml-encryptionnpmUse an encryption-focused package when the requirement is XML Encryption rather than XML Digital Signature