From 0b09b1a7058f382e52299fa12695076b82975d62 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Mon, 14 Sep 2026 12:52:26 -0500 Subject: [PATCH 01/11] docs: remove descriptions of earlier behavior from the README The Upgrading notes narrated what earlier releases did, the verification example still used the xpath export removed in 4.0, the KeyInfo docs still told readers to set getKeyInfoContent themselves, and two example headings had been "coming soon" for years. Co-Authored-By: Claude Opus 5 --- README.md | 66 ++++++++++++++++++++----------------------------------- 1 file changed, 24 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 4fe3d65..161969d 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ ### Canonicalization output Inclusive canonicalization (`http://www.w3.org/TR/2001/REC-xml-c14n-20010315` and its -`#WithComments` variant) now renders namespace declarations as the +`#WithComments` variant) renders namespace declarations as the [C14N specification](https://www.w3.org/TR/2001/REC-xml-c14n-20010315#ProcessingModel) requires. -Earlier releases rendered some documents incorrectly, for example when: +Its output differs from 6.1.x when: - a prefixed element in the signed content declares a default namespace, as in `` @@ -35,32 +35,27 @@ and its `#WithComments` variant) when the redeclared prefix is listed in the For such documents 6.2.0 and later compute a different digest than 6.1.x and earlier, so a signature created by one will not verify with the other. Upgrade signers and verifiers that -exchange these documents together. Documents signed in these shapes by other conforming -implementations, which 6.1.x rejected, now verify. +exchange these documents together. ### Transforms that end in a DOM node -When the last transform of a `Reference` returns a DOM `Node`, 6.2.0 and later convert it to octets -with inclusive canonicalization, as the +When the last transform of a `Reference` returns a DOM `Node`, it is converted to octets with +inclusive canonicalization, as the [reference processing model](https://www.w3.org/TR/xmldsig-core1/#sec-ReferenceProcessingModel) requires. A `SignedInfo` canonicalization algorithm that returns a `Node` is converted the same way. -Earlier releases serialized both with xmldom instead. - -- A reference whose only transform is `enveloped-signature` now gets a signature that verifies. - Verification with the built-in algorithms already canonicalized this case and is unchanged, so - 6.1.x verifies these signatures too, except for documents affected by the - [canonicalization output](#canonicalization-output) changes. -- `getCanonXml()` returns canonical XML for such transform lists, for example `` rather than - ``. -- A custom transform or canonicalization algorithm whose `process()` returns a `Node` now produces a + +- A reference whose only transform is `enveloped-signature` gets a signature that 6.1.x also + verifies, except for documents affected by the [canonicalization output](#canonicalization-output) + changes. +- `getCanonXml()` returns canonical XML for such transform lists, for example ``, which + differs from 6.1.x. +- A custom transform or canonicalization algorithm whose `process()` returns a `Node` produces a different digest or signature than 6.1.x, so a signature created by one will not verify with the other. Upgrade signers and verifiers that use it together. ### Deprecated ahead of 7.0 -The package used to re-export everything in its internal `utils` module, so helpers written for -`signed-xml.ts` became public API by accident. These are deprecated as of 6.2.0 and will be -removed in 7.0: +These exports are deprecated and will be removed in 7.0: | Deprecated | Instead | | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -77,12 +72,8 @@ JavaScript users get no signal until the names go away. `derToPem`, `pemToDer`, `normalizePem` and `findAncestorNs` are **not** deprecated and stay exported. -The `.getReferences()` AND the `.references` APIs are deprecated. -Please do not attempt to access them. The content in them should be treated as unsigned. - -Instead, we strongly encourage users to migrate to the `.getSignedReferences()` API. See the [Verifying XML document](#verifying-xml-documents) section -We understand that this may take a lot of efforts to migrate, feel free to ask for help. -This will help prevent future XML signature wrapping attacks. +`getReferences()` and `references` are deprecated, and their content is not signed. Use +`getSignedReferences()` instead, as shown in [Verifying Xml documents](#verifying-xml-documents). ## Supported Algorithms @@ -174,7 +165,7 @@ The result will be: Note: -If you set the `publicCert` and the `getKeyInfoContent` properties, a `` element with the public certificate will be generated in the signature: +If you set `publicCert`, a `` element with the public certificate will be generated in the signature: ```xml @@ -190,7 +181,7 @@ If you set the `publicCert` and the `getKeyInfoContent` properties, a ` ``` -For `getKeyInfoContent`, a default implementation `SignedXml.getKeyInfoContent` is available. +The default `getKeyInfoContent`, `SignedXml.getKeyInfoContent`, generates this element. To customize this see [customizing algorithms](#customizing-algorithms) for an example. @@ -212,16 +203,16 @@ new SignedXml({ }); ``` -You can use any dom parser you want in your code (or none, depending on your usage). This sample uses [xmldom](https://github.com/xmldom/xmldom), so you should install it first: +You can use any dom parser you want in your code (or none, depending on your usage). This sample uses [xmldom](https://github.com/xmldom/xmldom) and [xpath](https://github.com/goto100/xpath), so you should install them first: ```shell -npm install @xmldom/xmldom +npm install @xmldom/xmldom xpath ``` Example: ```javascript -var select = require("xml-crypto").xpath, +var xpath = require("xpath"), dom = require("@xmldom/xmldom").DOMParser, SignedXml = require("xml-crypto").SignedXml, fs = require("fs"); @@ -234,10 +225,10 @@ var doc = new dom().parseFromString(xml); // i.e. BAD: parseAssertion(doc), // good: see below -var signature = select( - doc, +var signature = xpath.select1( "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", -)[0]; + doc, +); var sig = new SignedXml({ publicCert: fs.readFileSync("client_public.pem") }); sig.loadSignature(signature); try { @@ -300,11 +291,6 @@ You might find it difficult to guess such transforms, but there are typical tran ## API -### xpath - -See [xpath.js](https://github.com/yaronn/xpath.js) for usage. Note that this is actually using -[another library](https://github.com/goto100/xpath) as the underlying implementation. - ### SignedXml The `SignedXml` constructor provides an abstraction for sign and verify xml documents. The object is constructed using `new SignedXml(options?: SignedXmlOptions)` where the possible options are: @@ -376,7 +362,7 @@ Now define the extension point you want to implement. You can choose one or more To determine the inclusion and contents of a `` element, the function `this.getKeyInfoContent()` is called. There is a default implementation of this. If you wish to change -this implementation, provide your own function assigned to the property `this.getKeyInfoContent`. If you prefer to use the default implementation, assign `SignedXml.getKeyInfoContent` to `this.getKeyInfoContent` If +this implementation, provide your own function assigned to the property `this.getKeyInfoContent`. If there are no attributes and no contents to the `` element, it won't be included in the generated XML. @@ -552,8 +538,6 @@ Then you could use the result as is for the purpose of signing. For the purpose ## Examples -### how to sign a root node (_coming soon_) - ### how to add a prefix for the signature Use the `prefix` option when calling `computeSignature` to add a prefix to the signature. @@ -642,8 +626,6 @@ sig.computeSignature(xml); fs.writeFileSync("signed.xml", sig.getSignedXml()); ``` -### more examples (_coming soon_) - ## Development The testing framework we use is [Mocha](https://github.com/mochajs/mocha) with [Chai](https://github.com/chaijs/chai) as the assertion framework. From e795ec5ea65635956eeb105d1b0fa9438d038da8 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Mon, 14 Sep 2026 12:57:06 -0500 Subject: [PATCH 02/11] docs: correct the location, custom algorithm and sponsor sections The location actions named a referenceNode property that never existed and described append and before loosely. The custom algorithm example registered its algorithms on an undefined variable under names that did not match getAlgorithmName(), selected an element the document lacks, overwrote the custom algorithms with built-in ones before signing, and split its XML string into a separate statement. The README also had two Sponsors sections. Co-Authored-By: Claude Opus 5 --- README.md | 64 +++++++++++++++++++++---------------------------------- 1 file changed, 24 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 161969d..1b5964a 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ ![stytchauth](https://github.com/stytchauth.png?size=30) [stytchauth](https://github.com/stytchauth) +![Short-io logo](https://github.com/Short-io.png?size=30) [Short-io](https://github.com/Short-io) + +![RideAmigosCorp logo](https://github.com/RideAmigosCorp.png?size=30) [RideAmigosCorp](https://github.com/RideAmigosCorp) + ## Upgrading ### Canonicalization output @@ -429,50 +433,35 @@ function MyCanonicalization() { } ``` -Now you need to register the new algorithms: - -```javascript -/*register all the custom algorithms*/ - -signedXml.CanonicalizationAlgorithms["http://MyTransformation"] = MyTransformation; -signedXml.CanonicalizationAlgorithms["http://MyCanonicalization"] = MyCanonicalization; -signedXml.HashAlgorithms["http://myDigestAlgorithm"] = MyDigest; -signedXml.SignatureAlgorithms["http://mySigningAlgorithm"] = MySignatureAlgorithm; -``` - -Now do the signing. Note how we configure the signature to use the above algorithms: +Now register the new algorithms on a `SignedXml` instance, under the names their `getAlgorithmName()` +returns, and configure the instance to use them: ```javascript function signXml(xml, xpath, key, dest) { - var options = { + var sig = new SignedXml({ publicCert: fs.readFileSync("my_public_cert.pem", "latin1"), privateKey: fs.readFileSync(key), /*configure the signature object to use the custom algorithms*/ - signatureAlgorithm: "http://mySignatureAlgorithm", - canonicalizationAlgorithm: "http://MyCanonicalization", - }; - - var sig = new SignedXml(options); - - sig.addReference({ - xpath: "//*[local-name(.)='x']", - transforms: ["http://MyTransformation"], - digestAlgorithm: "http://myDigestAlgorithm", + signatureAlgorithm: "http://mySigningAlgorithm", + canonicalizationAlgorithm: "http://myCanonicalization", }); + /*register all the custom algorithms*/ + sig.CanonicalizationAlgorithms["http://myTransformation"] = MyTransformation; + sig.CanonicalizationAlgorithms["http://myCanonicalization"] = MyCanonicalization; + sig.HashAlgorithms["http://myDigestAlgorithm"] = MyDigest; + sig.SignatureAlgorithms["http://mySigningAlgorithm"] = MySignatureAlgorithm; + sig.addReference({ xpath, - transforms: ["http://MyTransformation"], + transforms: ["http://myTransformation"], digestAlgorithm: "http://myDigestAlgorithm", }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; sig.computeSignature(xml); fs.writeFileSync(dest, sig.getSignedXml()); } -var xml = "" + "" + "Harry Potter" + ""; -(""); +var xml = "" + "" + "Harry Potter" + "" + ""; signXml(xml, "//*[local-name(.)='book']", "client.pem", "result.xml"); ``` @@ -563,13 +552,14 @@ sig.computeSignature(xml, { ### how to specify the location of the signature -Use the `location` option when calling `computeSignature` to move the signature around. -Set `action` to one of the following: +Use the `location` option when calling `computeSignature` to move the signature around. Set +`reference` to an XPath expression that selects a node (default `/*`, the document element), and +`action` to one of the following: -- append(default) - append to the end of the xml document -- prepend - prepend to the xml document -- before - prepend to a specific node (use the `referenceNode` property) -- after - append to specific node (use the `referenceNode` property) +- `append` (default) - insert the signature as the last child of the `reference` node +- `prepend` - insert the signature as the first child of the `reference` node +- `before` - insert the signature just before the `reference` node +- `after` - insert the signature just after the `reference` node ```javascript const SignedXml = require("xml-crypto").SignedXml; @@ -636,12 +626,6 @@ To run tests use: npm test ``` -## Sponsors - -![Short-io logo](https://github.com/Short-io.png?size=30) [Short-io](https://github.com/Short-io) - -![RideAmigosCorp logo](https://github.com/RideAmigosCorp.png?size=30) [RideAmigosCorp](https://github.com/RideAmigosCorp) - ## License This project is licensed under the [MIT License](http://opensource.org/licenses/MIT). See the [LICENSE](LICENSE) file for more info. From 39d739d1d53f587dcb858ce01215ab54e5ae6a50 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Mon, 14 Sep 2026 13:09:38 -0500 Subject: [PATCH 03/11] docs: say that a node-returning transform can change the digest Co-Authored-By: Claude Opus 5 --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1b5964a..ec4de90 100644 --- a/README.md +++ b/README.md @@ -51,11 +51,11 @@ requires. A `SignedInfo` canonicalization algorithm that returns a `Node` is con - A reference whose only transform is `enveloped-signature` gets a signature that 6.1.x also verifies, except for documents affected by the [canonicalization output](#canonicalization-output) changes. -- `getCanonXml()` returns canonical XML for such transform lists, for example ``, which - differs from 6.1.x. -- A custom transform or canonicalization algorithm whose `process()` returns a `Node` produces a - different digest or signature than 6.1.x, so a signature created by one will not verify with the - other. Upgrade signers and verifiers that use it together. +- `getCanonXml()` returns canonical XML for such transform lists, for example ``, which can + differ from 6.1.x. +- A custom transform or canonicalization algorithm whose `process()` returns a `Node` can produce a + different digest or signature than 6.1.x, and then a signature created by one will not verify with + the other. Upgrade signers and verifiers that use it together. ### Deprecated ahead of 7.0 From 2daa5069398817877d684557ff6a49cd0aeb40f4 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Mon, 14 Sep 2026 13:40:45 -0500 Subject: [PATCH 04/11] docs: state canonicalization behavior without comparing versions Co-Authored-By: Claude Opus 5 --- README.md | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ec4de90..a99169b 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ Inclusive canonicalization (`http://www.w3.org/TR/2001/REC-xml-c14n-20010315` and its `#WithComments` variant) renders namespace declarations as the -[C14N specification](https://www.w3.org/TR/2001/REC-xml-c14n-20010315#ProcessingModel) requires. -Its output differs from 6.1.x when: +[C14N specification](https://www.w3.org/TR/2001/REC-xml-c14n-20010315#ProcessingModel) requires, +including when: - a prefixed element in the signed content declares a default namespace, as in `` @@ -33,29 +33,17 @@ Its output differs from 6.1.x when: clears it with `xmlns=""` - the signed element redeclares a prefix that an ancestor binds, after declaring another namespace -The last case also changes exclusive canonicalization (`http://www.w3.org/2001/10/xml-exc-c14n#` -and its `#WithComments` variant) when the redeclared prefix is listed in the -`InclusiveNamespaces` `PrefixList`. Exclusive canonicalization is otherwise unaffected. - -For such documents 6.2.0 and later compute a different digest than 6.1.x and earlier, so a -signature created by one will not verify with the other. Upgrade signers and verifiers that -exchange these documents together. +Exclusive canonicalization (`http://www.w3.org/2001/10/xml-exc-c14n#` and its `#WithComments` +variant) renders the last case the same way when the redeclared prefix is listed in the +`InclusiveNamespaces` `PrefixList`. ### Transforms that end in a DOM node When the last transform of a `Reference` returns a DOM `Node`, it is converted to octets with inclusive canonicalization, as the [reference processing model](https://www.w3.org/TR/xmldsig-core1/#sec-ReferenceProcessingModel) -requires. A `SignedInfo` canonicalization algorithm that returns a `Node` is converted the same way. - -- A reference whose only transform is `enveloped-signature` gets a signature that 6.1.x also - verifies, except for documents affected by the [canonicalization output](#canonicalization-output) - changes. -- `getCanonXml()` returns canonical XML for such transform lists, for example ``, which can - differ from 6.1.x. -- A custom transform or canonicalization algorithm whose `process()` returns a `Node` can produce a - different digest or signature than 6.1.x, and then a signature created by one will not verify with - the other. Upgrade signers and verifiers that use it together. +requires. A `SignedInfo` canonicalization algorithm that returns a `Node` is converted the same way, +and `getCanonXml()` returns canonical XML for such transform lists, for example ``. ### Deprecated ahead of 7.0 From 692551e56b04fbe84a4c2e76b736e6cec2f7cc6a Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Mon, 14 Sep 2026 16:32:42 -0500 Subject: [PATCH 05/11] docs: correct the KeyInfo content and getReferences wording Co-Authored-By: Claude Opus 5 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0b117a4..09c0363 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ JavaScript users get no signal until the names go away. `derToPem`, `pemToDer`, `normalizePem` and `findAncestorNs` are **not** deprecated and stay exported. -`getReferences()` and `references` are deprecated, and their content is not signed. Use +`getReferences()` and `references` are deprecated. Do not use them to obtain signed XML; use `getSignedReferences()` instead, as shown in [Verifying Xml documents](#verifying-xml-documents). ## Supported Algorithms @@ -211,7 +211,7 @@ If you set `publicCert`, a `` element with the public certifi ``` -The default `getKeyInfoContent`, `SignedXml.getKeyInfoContent`, generates this element. +The default `SignedXml.getKeyInfoContent` generates the `` content of this element. To customize this see [customizing algorithms](#customizing-algorithms) for an example. From 0fe2a9cee1460abf45aca5d8464b4f175125738b Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Mon, 14 Sep 2026 16:52:08 -0500 Subject: [PATCH 06/11] docs: modernize the README samples and sign them with SHA-256 Use const, destructured require and classes, which the supported Node versions all have and the algorithm JSDoc already asks for. Pass the MIME type @xmldom/xmldom 0.9 requires to parseFromString, throw an Error instead of a string, and let checkSignature errors propagate rather than logging them. The async example registered its algorithm under a URI its getAlgorithmName() did not return and used undefined crypto and opts. It now registers under the RSA-SHA256 URI it implements. Co-Authored-By: Claude Opus 5 --- README.md | 212 +++++++++++++++++++++++++++++------------------------- 1 file changed, 115 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 09c0363..006514f 100644 --- a/README.md +++ b/README.md @@ -152,19 +152,21 @@ When signing a xml document you can pass the following options to the `SignedXml Use this code: ```javascript -var SignedXml = require("xml-crypto").SignedXml, - fs = require("fs"); +const { SignedXml } = require("xml-crypto"); +const fs = require("fs"); -var xml = "" + "" + "Harry Potter" + "" + ""; +const xml = "Harry Potter"; -var sig = new SignedXml({ privateKey: fs.readFileSync("client.pem") }); +const sig = new SignedXml({ + privateKey: fs.readFileSync("client.pem"), + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", +}); sig.addReference({ xpath: "//*[local-name(.)='book']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); -sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; -sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; sig.computeSignature(xml); fs.writeFileSync("signed.xml", sig.getSignedXml()); ``` @@ -179,16 +181,16 @@ The result will be: - + - - cdiS43aFDQMnb3X8yaIUej3+z9Q= + + 9d/ciWlVZkaJnJ3KBB5WY1H2Y8WRXPB2DquM0goT8jY= - vhWzpQyIYuncHUZV9W...[long base64 removed]... + uxmxGw2O3B6ylkhEXOaZ...[long base64 removed]... ``` @@ -202,7 +204,7 @@ If you set `publicCert`, a `` element with the public certifi ...[signature info removed]... - vhWzpQyIYuncHUZV9W...[long base64 removed]... + uxmxGw2O3B6ylkhEXOaZ...[long base64 removed]... MIIGYjCCBJagACCBN...[long base64 removed]... @@ -242,46 +244,42 @@ npm install @xmldom/xmldom xpath Example: ```javascript -var xpath = require("xpath"), - dom = require("@xmldom/xmldom").DOMParser, - SignedXml = require("xml-crypto").SignedXml, - fs = require("fs"); +const { DOMParser } = require("@xmldom/xmldom"); +const xpath = require("xpath"); +const { SignedXml } = require("xml-crypto"); +const fs = require("fs"); -var xml = fs.readFileSync("signed.xml").toString(); -var doc = new dom().parseFromString(xml); +const xml = fs.readFileSync("signed.xml", "utf8"); +const doc = new DOMParser().parseFromString(xml, "text/xml"); // DO NOT attempt to parse whatever data object you have here in `doc` // and then use it to verify the signature. This can lead to security issues. // i.e. BAD: parseAssertion(doc), // good: see below -var signature = xpath.select1( +const signature = xpath.select1( "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc, ); -var sig = new SignedXml({ publicCert: fs.readFileSync("client_public.pem") }); +const sig = new SignedXml({ publicCert: fs.readFileSync("client_public.pem") }); sig.loadSignature(signature); -try { - var res = sig.checkSignature(xml); -} catch (ex) { - console.log(ex); -} +const res = sig.checkSignature(xml); ``` In order to protect from some attacks we must check the content we want to use is the one that has been signed: ```javascript if (!res) { - throw "Invalid Signature"; + throw new Error("Invalid signature"); } // good: The XML Signature has been verified, meaning some subset of XML is verified. -var signedBytes = sig.getSignedReferences(); +const signedBytes = sig.getSignedReferences(); -var authenticatedDoc = new dom().parseFromString(signedBytes[0]); // Take the first signed reference +const authenticatedDoc = new DOMParser().parseFromString(signedBytes[0], "text/xml"); // Take the first signed reference // It is now safe to load SAML, obtain the assertion XML, or do whatever else is needed. // Be sure to only use authenticated data. -let signedAssertionNode = extractAssertion(authenticatedDoc); -let parsedAssertion = parseAssertion(signedAssertionNode); +const signedAssertionNode = extractAssertion(authenticatedDoc); +const parsedAssertion = parseAssertion(signedAssertionNode); return parsedAssertion; // This the correctly verified signed Assertion @@ -303,13 +301,12 @@ which makes XML developers confused and then leads to incorrect implementation f If you keep failing verification, it is worth trying to guess such a hidden transform and specify it to the option as below: ```javascript -var options = { +const sig = new SignedXml({ implicitTransforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"], publicCert: fs.readFileSync("client_public.pem"), -}; -var sig = new SignedXml(options); +}); sig.loadSignature(signature); -var res = sig.checkSignature(xml); +const res = sig.checkSignature(xml); ``` You might find it difficult to guess such transforms, but there are typical transforms you can try. @@ -384,8 +381,8 @@ The following sample shows how to sign a message using custom algorithms. First import some modules: ```javascript -var SignedXml = require("xml-crypto").SignedXml, - fs = require("fs"); +const { SignedXml } = require("xml-crypto"); +const fs = require("fs"); ``` Now define the extension point you want to implement. You can choose one or more. @@ -401,61 +398,61 @@ To specify custom attributes on ``, add the properties to the `.keyIn A custom hash algorithm is used to calculate digests. Implement it if you want a hash other than the built-in methods. ```javascript -function MyDigest() { - this.getHash = function (xml) { +class MyDigest { + getHash(xml) { return "the base64 hash representation of the given xml string"; - }; + } - this.getAlgorithmName = function () { + getAlgorithmName() { return "http://myDigestAlgorithm"; - }; + } } ``` A custom signing algorithm. ```javascript -function MySignatureAlgorithm() { - /*sign the given SignedInfo using the key. return base64 signature value*/ - this.getSignature = function (signedInfo, privateKey) { +class MySignatureAlgorithm { + // Sign the given SignedInfo using the key. Return the base64 signature value. + getSignature(signedInfo, privateKey) { return "signature of signedInfo as base64..."; - }; + } - this.getAlgorithmName = function () { + getAlgorithmName() { return "http://mySigningAlgorithm"; - }; + } } ``` Custom transformation algorithm. ```javascript -function MyTransformation() { - /*given a node (from the xmldom module) return its canonical representation (as string)*/ - this.process = function (node) { - //you should apply your transformation before returning +class MyTransformation { + // Given a node (from the xmldom module), return its canonical representation as a string. + process(node) { + // You should apply your transformation before returning. return node.toString(); - }; + } - this.getAlgorithmName = function () { + getAlgorithmName() { return "http://myTransformation"; - }; + } } ``` Custom canonicalization is actually the same as custom transformation. It is applied on the SignedInfo rather than on references. ```javascript -function MyCanonicalization() { - /*given a node (from the xmldom module) return its canonical representation (as string)*/ - this.process = function (node) { - //you should apply your transformation before returning +class MyCanonicalization { + // Given a node (from the xmldom module), return its canonical representation as a string. + process(node) { + // You should apply your transformation before returning. return "< x/>"; - }; + } - this.getAlgorithmName = function () { + getAlgorithmName() { return "http://myCanonicalization"; - }; + } } ``` @@ -464,15 +461,15 @@ returns, and configure the instance to use them: ```javascript function signXml(xml, xpath, key, dest) { - var sig = new SignedXml({ + const sig = new SignedXml({ publicCert: fs.readFileSync("my_public_cert.pem", "latin1"), privateKey: fs.readFileSync(key), - /*configure the signature object to use the custom algorithms*/ + // Configure the signature object to use the custom algorithms. signatureAlgorithm: "http://mySigningAlgorithm", canonicalizationAlgorithm: "http://myCanonicalization", }); - /*register all the custom algorithms*/ + // Register all the custom algorithms. sig.CanonicalizationAlgorithms["http://myTransformation"] = MyTransformation; sig.CanonicalizationAlgorithms["http://myCanonicalization"] = MyCanonicalization; sig.HashAlgorithms["http://myDigestAlgorithm"] = MyDigest; @@ -487,7 +484,7 @@ function signXml(xml, xpath, key, dest) { fs.writeFileSync(dest, sig.getSignedXml()); } -var xml = "" + "" + "Harry Potter" + "" + ""; +const xml = "Harry Potter"; signXml(xml, "//*[local-name(.)='book']", "client.pem", "result.xml"); ``` @@ -496,28 +493,45 @@ You can always look at the actual code as a sample. ## Asynchronous signing and verification -If the private key is not stored locally, and you wish to use a signing server or Hardware Security Module (HSM) to sign documents, you can create a custom signing algorithm that uses an asynchronous callback. +If the private key is not stored locally, and you wish to use a signing server or Hardware Security Module (HSM) to sign documents, you can create a custom signing algorithm that uses an asynchronous callback. Register it under the URI of the algorithm it implements, which is the `SignatureMethod` a verifier reads. ```javascript -function AsyncSignatureAlgorithm() { - this.getSignature = function (signedInfo, privateKey, callback) { - var signer = crypto.createSign("RSA-SHA1"); +const { SignedXml } = require("xml-crypto"); +const crypto = require("crypto"); +const fs = require("fs"); + +class AsyncRsaSha256 { + getSignature(signedInfo, privateKey, callback) { + // Do some asynchronous things here, such as calling a signing server. + const signer = crypto.createSign("RSA-SHA256"); signer.update(signedInfo); - var res = signer.sign(privateKey, "base64"); - //Do some asynchronous things here - callback(null, res); - }; - this.getAlgorithmName = function () { - return "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; - }; + callback(null, signer.sign(privateKey, "base64")); + } + + getAlgorithmName() { + return "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; + } } -var sig = new SignedXml({ signatureAlgorithm: "http://asyncSignatureAlgorithm" }); -sig.SignatureAlgorithms["http://asyncSignatureAlgorithm"] = AsyncSignatureAlgorithm; -sig.signatureAlgorithm = "http://asyncSignatureAlgorithm"; -sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; -sig.computeSignature(xml, opts, function (err) { - var signedResponse = sig.getSignedXml(); +const xml = "Harry Potter"; + +const sig = new SignedXml({ + privateKey: fs.readFileSync("client.pem"), + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", +}); +sig.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] = AsyncRsaSha256; +sig.addReference({ + xpath: "//*[local-name(.)='book']", + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], +}); +sig.computeSignature(xml, (err) => { + if (err) { + console.error(err); + return; + } + fs.writeFileSync("signed.xml", sig.getSignedXml()); }); ``` @@ -558,19 +572,21 @@ Then you could use the result as is for the purpose of signing. For the purpose Use the `prefix` option when calling `computeSignature` to add a prefix to the signature. ```javascript -var SignedXml = require("xml-crypto").SignedXml, - fs = require("fs"); +const { SignedXml } = require("xml-crypto"); +const fs = require("fs"); -var xml = "" + "" + "Harry Potter" + "" + ""; +const xml = "Harry Potter"; -var sig = new SignedXml({ privateKey: fs.readFileSync("client.pem") }); +const sig = new SignedXml({ + privateKey: fs.readFileSync("client.pem"), + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", +}); sig.addReference({ xpath: "//*[local-name(.)='book']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); -sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; -sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; sig.computeSignature(xml, { prefix: "ds", }); @@ -588,19 +604,21 @@ Use the `location` option when calling `computeSignature` to move the signature - `after` - insert the signature just after the `reference` node ```javascript -const SignedXml = require("xml-crypto").SignedXml; +const { SignedXml } = require("xml-crypto"); const fs = require("fs"); -const xml = "" + "" + "Harry Potter" + "" + ""; +const xml = "Harry Potter"; -const sig = new SignedXml({ privateKey: fs.readFileSync("client.pem") }); +const sig = new SignedXml({ + privateKey: fs.readFileSync("client.pem"), + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", +}); sig.addReference({ xpath: "//*[local-name(.)='book']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); -sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; -sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; sig.computeSignature(xml, { location: { reference: "//*[local-name(.)='book']", action: "after" }, // This will place the signature after the book element }); @@ -611,10 +629,10 @@ sig.computeSignature(xml, { Use the `objects` option when creating a SignedXml instance to add custom Objects to the signature. ```javascript -const SignedXml = require("xml-crypto").SignedXml; +const { SignedXml } = require("xml-crypto"); const fs = require("fs"); -const xml = "" + "" + "Harry Potter" + "" + ""; +const xml = "Harry Potter"; const sig = new SignedXml({ privateKey: fs.readFileSync("client.pem"), From b6f99a8b47493fb6793037da86fda49e6517f76f Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Mon, 14 Sep 2026 17:05:14 -0500 Subject: [PATCH 07/11] docs: limit the async section to signing and qualify the KeyInfo note checkSignature calls verifySignature without a callback, so its callback overload only reports a synchronous result. The default KeyInfo content includes only PEM certificate blocks from publicCert. Co-Authored-By: Claude Opus 5 --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 006514f..df507ac 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ The result will be: Note: -If you set `publicCert`, a `` element with the public certificate will be generated in the signature: +If `publicCert` contains an X.509 certificate, the default `SignedXml.getKeyInfoContent` includes it in a `` element: ```xml @@ -213,8 +213,6 @@ If you set `publicCert`, a `` element with the public certifi ``` -The default `SignedXml.getKeyInfoContent` generates the `` content of this element. - To customize this see [customizing algorithms](#customizing-algorithms) for an example. ## Verifying Xml documents @@ -491,7 +489,7 @@ signXml(xml, "//*[local-name(.)='book']", "client.pem", "result.xml"); You can always look at the actual code as a sample. -## Asynchronous signing and verification +## Asynchronous signing If the private key is not stored locally, and you wish to use a signing server or Hardware Security Module (HSM) to sign documents, you can create a custom signing algorithm that uses an asynchronous callback. Register it under the URI of the algorithm it implements, which is the `SignatureMethod` a verifier reads. @@ -535,8 +533,6 @@ sig.computeSignature(xml, (err) => { }); ``` -The function `sig.checkSignature` may also use a callback if asynchronous verification is needed. - ## X.509 / Key formats Xml-Crypto internally relies on node's crypto module. This means pem encoded certificates are supported. So to sign an xml use key.pem that looks like this (only the beginning of the key content is shown): From 53e6cb81d694326c66b370ee364a5b7054bcab42 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Mon, 14 Sep 2026 17:19:58 -0500 Subject: [PATCH 08/11] docs: say that checkSignature verifies synchronously checkSignature calls verifySignature without a callback, so its callback receives a result computed synchronously and the callback overload of SignatureAlgorithm.verifySignature is never called. Co-Authored-By: Claude Opus 5 --- src/signed-xml.ts | 2 +- src/types.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 953d23e..3493f9c 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -272,7 +272,7 @@ export class SignedXml { * Validates the signature of the provided XML document synchronously using the configured key info provider. * * @param xml The XML document containing the signature to be validated. - * @param callback Callback function to handle the validation result asynchronously. + * @param callback Called with the validation result before `checkSignature` returns. * @throws Error if the last parameter is provided and is not a function, or if no key info resolver is provided. */ checkSignature(xml: string, callback: (error: Error | null, isValid?: boolean) => void): void; diff --git a/src/types.ts b/src/types.ts index 322273f..2dc4105 100644 --- a/src/types.ts +++ b/src/types.ts @@ -193,6 +193,9 @@ export interface SignatureAlgorithm { * @param key a public cert, public key, or private key can be passed here */ verifySignature(material: string, key: crypto.KeyLike, signatureValue: string): boolean; + /** + * `SignedXml.checkSignature` calls only the synchronous form. + */ verifySignature( material: string, key: crypto.KeyLike, From a3f35f420a55cf6154a8d1c3a8432198b6e55a55 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Mon, 14 Sep 2026 17:27:16 -0500 Subject: [PATCH 09/11] docs: return the node from the custom canonicalization example "< x/>" is not well-formed XML. Co-Authored-By: Claude Opus 5 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index df507ac..99150ce 100644 --- a/README.md +++ b/README.md @@ -444,8 +444,8 @@ Custom canonicalization is actually the same as custom transformation. It is app class MyCanonicalization { // Given a node (from the xmldom module), return its canonical representation as a string. process(node) { - // You should apply your transformation before returning. - return "< x/>"; + // You should apply your canonicalization before returning. + return node.toString(); } getAlgorithmName() { From d2201757ffa3632e8207ba1530e19e5802c7e972 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Mon, 14 Sep 2026 17:44:30 -0500 Subject: [PATCH 10/11] docs: list only current sponsors stytchauth's sponsorship has ended. Co-Authored-By: Claude Opus 5 --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 99150ce..0ef7f0c 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,6 @@ ![workos](https://github.com/workos.png?size=30) [workos](https://github.com/workos) -![stytchauth](https://github.com/stytchauth.png?size=30) [stytchauth](https://github.com/stytchauth) - ![Short-io logo](https://github.com/Short-io.png?size=30) [Short-io](https://github.com/Short-io) ![RideAmigosCorp logo](https://github.com/RideAmigosCorp.png?size=30) [RideAmigosCorp](https://github.com/RideAmigosCorp) From c54aa14721d2b50fe031e4e52d4e587af5ceb356 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Mon, 14 Sep 2026 17:49:36 -0500 Subject: [PATCH 11/11] docs: keep sponsor placement by tier and list past sponsors Only tiers from $100 a month place a logo above the fold, so the $25 sponsors return to the section at the bottom. stytchauth moves to a new Past sponsors section below them. Co-Authored-By: Claude Opus 5 --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0ef7f0c..c162ab8 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,6 @@ ![workos](https://github.com/workos.png?size=30) [workos](https://github.com/workos) -![Short-io logo](https://github.com/Short-io.png?size=30) [Short-io](https://github.com/Short-io) - -![RideAmigosCorp logo](https://github.com/RideAmigosCorp.png?size=30) [RideAmigosCorp](https://github.com/RideAmigosCorp) - ## Upgrading ### Canonicalization output @@ -664,6 +660,16 @@ To run tests use: npm test ``` +## Sponsors + +![Short-io logo](https://github.com/Short-io.png?size=30) [Short-io](https://github.com/Short-io) + +![RideAmigosCorp logo](https://github.com/RideAmigosCorp.png?size=30) [RideAmigosCorp](https://github.com/RideAmigosCorp) + +## Past sponsors + +![stytchauth](https://github.com/stytchauth.png?size=30) [stytchauth](https://github.com/stytchauth) + ## License This project is licensed under the [MIT License](http://opensource.org/licenses/MIT). See the [LICENSE](LICENSE) file for more info.