From a311adc0351b2ec843e62f545450590ca648ae6f Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Mon, 5 Oct 2026 08:26:29 -0500 Subject: [PATCH 1/8] chore: group tests and deprecations in the changelog (#651) --- .grenrc.js | 29 +++++++++++++++++++++++++---- package-lock.json | 34 +++++----------------------------- package.json | 2 +- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/.grenrc.js b/.grenrc.js index a67265b9..6c0407be 100644 --- a/.grenrc.js +++ b/.grenrc.js @@ -1,16 +1,35 @@ +// Fixed in one merge from a private fork on each release line, so the commits carrying them +// have no pull request to take a title or labels from. +const signatureBypassAdvisories = { + title: + "Address CVEs: [CVE-2025-29774](https://github.com/node-saml/xml-crypto/security/advisories/GHSA-9p8x-f768-wp2g) and [CVE-2025-29775](https://github.com/node-saml/xml-crypto/security/advisories/GHSA-x3m8-899r-f7c3)", + labels: ["security"], +}; + module.exports = { dataSource: "prs", prefix: "", onlyMilestones: false, ignoreTagsWith: [], - ignoreLabels: [], + // Dependabot adds these next to "dependencies"; they say nothing a reader needs. + ignoreLabels: ["javascript", "github_actions", "java"], + // The master-side copies of changes already released from 6.x, which git cannot tell + // are the same change as the commits that shipped. + ignoreIssuesWith: ["duplicate"], + commitNotes: { + "8ac6118ee7": signatureBypassAdvisories, + "28f92218ec": signatureBypassAdvisories, + "886dc63a8b": signatureBypassAdvisories, + }, tags: "all", groupBy: { - "Major Changes": ["semver-major", "breaking-change"], - "Minor Changes": ["semver-minor", "enhancement", "new-feature"], + "Major Changes": ["breaking-change"], + "Minor Changes": ["enhancement"], + Deprecations: ["deprecation"], Dependencies: ["dependencies"], - "Bug Fixes": ["semver-patch", "bug", "security"], + "Bug Fixes": ["bug", "security"], Documentation: ["documentation"], + Tests: ["tests"], "Technical Tasks": ["chore"], Other: ["..."], }, @@ -41,8 +60,10 @@ module.exports = { const iconMap = { Enhancements: "๐Ÿš€", "Minor Changes": "๐Ÿš€", + Deprecations: "โš ๏ธ", "Bug Fixes": "๐Ÿ›", Documentation: "๐Ÿ“š", + Tests: "๐Ÿงช", "Technical Tasks": "โš™๏ธ", "Major Changes": "๐Ÿ’ฃ", Dependencies: "๐Ÿ”—", diff --git a/package-lock.json b/package-lock.json index 5523e361..3c00a0e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "xpath": "^0.0.33" }, "devDependencies": { - "@cjbarth/github-release-notes": "^5.0.0", + "@cjbarth/github-release-notes": "^5.1.1", "@istanbuljs/nyc-config-typescript": "^1.0.2", "@prettier/plugin-xml": "^3.4.2", "@types/chai": "^4.3.20", @@ -309,9 +309,9 @@ } }, "node_modules/@cjbarth/github-release-notes": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@cjbarth/github-release-notes/-/github-release-notes-5.0.0.tgz", - "integrity": "sha512-S/3HxOeh2MDp6aG2EbRnD4nPTEbyPdcdvoCN9exMhoxHfYFW5anLhB6HveuvLRgs4ngHC4vs5gFwovJugGLHPw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@cjbarth/github-release-notes/-/github-release-notes-5.1.1.tgz", + "integrity": "sha512-iAkpELN/T3Zs0ufRNYy+5UP1NIPjcrtEN/OFXrqZlwhh9jdVY0Psr4gxCbrH535797bZAADozy8eE04ByI5UkA==", "dev": true, "hasInstallScript": true, "license": "ISC", @@ -325,7 +325,7 @@ "object-assign-deep": "^0.4.0", "prettier": "^3.9.6", "require-from-url": "^3.1.3", - "semver-compare": "^1.0.0", + "semver": "^7.8.5", "yaml": "^2.9.0" }, "bin": { @@ -659,16 +659,6 @@ } } }, - "node_modules/@cjbarth/github-release-notes/node_modules/@types/node": { - "version": "26.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.0.tgz", - "integrity": "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "undici-types": "~8.9.0" - } - }, "node_modules/@cjbarth/github-release-notes/node_modules/chardet": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", @@ -9081,13 +9071,6 @@ "node": ">=10" } }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT" - }, "node_modules/semver-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", @@ -10052,13 +10035,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", - "extraneous": true, - "license": "MIT" - }, "node_modules/unique-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", diff --git a/package.json b/package.json index b0e7fdf8..fe8d250a 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "xpath": "^0.0.33" }, "devDependencies": { - "@cjbarth/github-release-notes": "^5.0.0", + "@cjbarth/github-release-notes": "^5.1.1", "@istanbuljs/nyc-config-typescript": "^1.0.2", "@prettier/plugin-xml": "^3.4.2", "@types/chai": "^4.3.20", From f844dedb4bc0e5392ea34eb071c0427f74c3108d Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Tue, 13 Oct 2026 13:06:44 -0500 Subject: [PATCH 2/8] chore: put the lockfile back in sync for npm 8 (#659) --- package-lock.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/package-lock.json b/package-lock.json index 3c00a0e2..63308859 100644 --- a/package-lock.json +++ b/package-lock.json @@ -659,6 +659,17 @@ } } }, + "node_modules/@cjbarth/github-release-notes/node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~8.9.0" + } + }, "node_modules/@cjbarth/github-release-notes/node_modules/chardet": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", @@ -10035,6 +10046,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/unique-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", From 0c38c80993de80d33f6b1f1872e2933f46648553 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Tue, 13 Oct 2026 14:30:49 -0500 Subject: [PATCH 3/8] feat: check private-key data against its label in toPem() and pemToDer() (#658) --- README.md | 10 +++++++++ src/utils.ts | 41 +++++++++++++++++++++++++++++-------- test/utils-tests.spec.ts | 44 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f5309ed9..9346784c 100644 --- a/README.md +++ b/README.md @@ -654,6 +654,16 @@ Rejected, with an error rather than a certificate: certificate cut short, or one with more bytes after it. Node's [`X509Certificate`](https://nodejs.org/api/crypto.html#class-x509certificate) decides whether the data is a certificate, so it is judged as X.509 and not by its base64 alone. +- a `PRIVATE KEY` or `RSA PRIVATE KEY` whose data Node cannot read as a private key under that + label: PKCS #1 data labelled `PRIVATE KEY`, a public key, or base64 of something else. Node's + [`crypto.createPrivateKey()`](https://nodejs.org/api/crypto.html#cryptocreateprivatekeykey) + decides, so the answer follows the Node version: Node 16 refuses PKCS #8 data labelled + `RSA PRIVATE KEY`, and later versions read it. Base64 does not say whether a key is PKCS #8, + which is `PRIVATE KEY`, or PKCS #1, which is `RSA PRIVATE KEY`, so a caller that takes either as + bare base64 tries `PRIVATE KEY` first and `RSA PRIVATE KEY` second. In that order each key comes + out under its own label on every supported Node version. Tried the other way round, PKCS #8 data + comes out labelled `RSA PRIVATE KEY` on a Node version that reads it so, and Node 16 then refuses + that PEM. - a header with no data under it, and a blank line in the middle of a message's data. - a boundary sharing its line with other text. [Figure 1](https://www.rfc-editor.org/rfc/rfc7468#section-3) gives an encapsulation boundary a diff --git a/src/utils.ts b/src/utils.ts index ec5ec0c1..65f5142e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { X509Certificate } from "crypto"; +import { X509Certificate, createPrivateKey } from "crypto"; import * as xpath from "xpath"; import type { NamespacePrefix, PemLabel } from "./types"; import * as isDomNode from "@xmldom/is-dom-node"; @@ -348,12 +348,35 @@ export function normalizePem(pem: string): string { // same bytes whatever line width, line ending or blanks it arrived with. Only the data is handed // to `normalizePem`, which wraps at 64 characters whatever it is given: a boundary broken across // two lines would be a message this parser could no longer read back. -function formatPemMessage(label: string, data: string): string { +function writePemMessage(label: string, data: string): string { + return `-----BEGIN ${label}-----\n${normalizePem(canonicalBase64(data))}-----END ${label}-----\n`; +} + +const PRIVATE_KEY_LABELS = ["PRIVATE KEY", "RSA PRIVATE KEY"]; + +// Base64 does not say whether it holds PKCS #8 or PKCS #1, and the label does, so Node is asked to +// read the data under the label it carries: https://www.rfc-editor.org/rfc/rfc7468#section-10 +function assertPrivateKey(label: string, data: string): void { + try { + createPrivateKey(writePemMessage(label, data)); + } catch { + throw new Error("Invalid PEM format."); + } +} + +function assertEncapsulatedData(label: string, data: string): void { if (label === "CERTIFICATE") { assertX509Certificate(data); } + if (PRIVATE_KEY_LABELS.includes(label)) { + assertPrivateKey(label, data); + } +} - return `-----BEGIN ${label}-----\n${normalizePem(canonicalBase64(data))}-----END ${label}-----\n`; +function formatPemMessage(label: string, data: string): string { + assertEncapsulatedData(label, data); + + return writePemMessage(label, data); } /** @@ -392,7 +415,8 @@ export function pemCertificates(pem: string): string[] { * Returns the decoded bytes of the one PEM message a value holds, whatever its label. * * @param pem The PEM message to decode - * @throws Error if the value is not a single well-formed PEM message + * @throws Error if the value is not a single well-formed PEM message, or if its data is not what + * a `CERTIFICATE`, `PRIVATE KEY` or `RSA PRIVATE KEY` label says it is */ export function pemToDer(pem: string): Buffer { const text = normalizePemInput(pem); @@ -409,9 +433,7 @@ export function pemToDer(pem: string): Buffer { } const [{ label, data }] = messages; - if (label === "CERTIFICATE") { - assertX509Certificate(data); - } + assertEncapsulatedData(label, data); return Buffer.from(data, "base64"); } @@ -451,8 +473,9 @@ export function bareCertificate(value: string): string { * * @param value The certificate or key to return as PEM * @param pemLabel The label to give base64 data, which needs one; ignored when the value is PEM - * @throws Error if the value is neither a well-formed PEM nor base64, or if it is base64 and no - * label, or an unusable one, was given + * @throws Error if the value is neither a well-formed PEM nor base64, if it is base64 and no + * label, or an unusable one, was given, or if the data is not what a `CERTIFICATE`, + * `PRIVATE KEY` or `RSA PRIVATE KEY` label says it is */ export function toPem(value: string | Buffer, pemLabel?: PemLabel): string { const text = normalizePemInput(pemText(value)); diff --git a/test/utils-tests.spec.ts b/test/utils-tests.spec.ts index f37305e5..b081c36e 100644 --- a/test/utils-tests.spec.ts +++ b/test/utils-tests.spec.ts @@ -581,6 +581,50 @@ describe("Utils tests", function () { }); }); + describe("reads a PRIVATE KEY or RSA PRIVATE KEY message's data as a private key", function () { + const key = crypto.createPrivateKey(fs.readFileSync("./test/static/client.pem")); + const pkcs8 = key.export({ type: "pkcs8", format: "der" }); + const pkcs1 = key.export({ type: "pkcs1", format: "der" }); + const publicKey = crypto.createPublicKey(key).export({ type: "spki", format: "der" }); + const noKey = Buffer.from("base64, and no private key"); + const wrap = (label: string, der: Buffer) => + `-----BEGIN ${label}-----\n${der.toString("base64")}\n-----END ${label}-----\n`; + + for (const [structure, der, label] of [ + ["PKCS #8", pkcs8, "PRIVATE KEY"], + ["PKCS #1", pkcs1, "RSA PRIVATE KEY"], + ] as const) { + it(`accepts ${structure} data labelled ${label}, and keeps its octets as given`, function () { + expect(utils.pemToDer(utils.toPem(wrap(label, der)))).to.deep.equal(der); + expect(utils.pemToDer(utils.toPem(der.toString("base64"), label))).to.deep.equal(der); + expect(utils.pemToDer(utils.toPem(der, label))).to.deep.equal(der); + }); + } + + for (const [problem, der, label] of [ + ["PKCS #1 data labelled PRIVATE KEY", pkcs1, "PRIVATE KEY"], + ["a public key labelled PRIVATE KEY", publicKey, "PRIVATE KEY"], + ["base64 that is no key, labelled PRIVATE KEY", noKey, "PRIVATE KEY"], + ["base64 that is no key, labelled RSA PRIVATE KEY", noKey, "RSA PRIVATE KEY"], + ] as const) { + it(`refuses ${problem}, in each function that reads one`, function () { + expect(() => utils.toPem(wrap(label, der))).to.throw("Invalid PEM format."); + expect(() => utils.toPem(der.toString("base64"), label)).to.throw("Invalid PEM format."); + expect(() => utils.toPem(der, label)).to.throw("Invalid PEM format."); + expect(() => utils.pemToDer(wrap(label, der))).to.throw("Invalid PEM format."); + }); + } + + it("refuses a mislabelled key in a value that also holds its certificate", function () { + const certificate = fs.readFileSync("./test/static/client_public.pem", "latin1"); + + expect(() => utils.toPem(wrap("PRIVATE KEY", pkcs1) + certificate)).to.throw( + "Invalid PEM format.", + ); + expect(utils.pemCertificates(wrap("PRIVATE KEY", pkcs1) + certificate)).to.have.lengthOf(1); + }); + }); + describe("pemCertificates", function () { const bundle = fs.readFileSync("./test/static/client_bundle.pem", "latin1"); From 557dfffe7b22c3abe4fc22fca9ce47247c61b9a3 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Tue, 13 Oct 2026 15:59:45 -0500 Subject: [PATCH 4/8] chore: deprecate enveloped-signature after a canonicalization (#649) --- README.md | 18 ++++++++++++++---- src/signed-xml.ts | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9346784c..4f822e50 100644 --- a/README.md +++ b/README.md @@ -71,15 +71,22 @@ string is parsed into a new document and the next transform is applied to that, [reference processing model](https://www.w3.org/TR/xmldsig-core1/#sec-ReferenceProcessingModel) requires. Every built-in canonicalization algorithm returns a string, so: -- `enveloped-signature` after a canonicalization removes the `Signature`, including one inside the - referenced element. -- The result of a `#WithComments` canonicalization followed by `enveloped-signature` is - [canonicalized](#transforms-that-end-in-a-dom-node) without comments. - Exclusive canonicalization after inclusive canonicalization omits inherited namespace declarations that the referenced element does not use. - A custom transform after a canonicalization receives the parsed document. - A transform throws when the string returned by the transform before it is not well-formed XML. +`enveloped-signature` is the exception. XMLDSig applies it only to +[a node-set from its parent XML document](https://www.w3.org/TR/xmldsig-core1/#sec-EnvelopedSignature), +which the parsed document is not, so list `enveloped-signature` before any canonicalization. Applying +it after a transform that returns a string is deprecated, when signing and when verifying, and will +throw in 7.0. Until then it prints a `DeprecationWarning`, and: + +- `enveloped-signature` after a canonicalization removes the `Signature`, including one inside the + referenced element. +- The result of a `#WithComments` canonicalization followed by `enveloped-signature` is + [canonicalized](#transforms-that-end-in-a-dom-node) without comments. + ### Copies of an enveloped signature The `enveloped-signature` transform removes only the `Signature` element being verified, as @@ -125,6 +132,9 @@ deprecated and stay exported. `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). +`enveloped-signature` after a transform that returns a string is deprecated; see +[Transforms that follow a canonicalization](#transforms-that-follow-a-canonicalization). + ## Supported Algorithms ### Canonicalization and Transformation Algorithms diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 841e23c1..d181c6b8 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -90,6 +90,12 @@ const warnValidateElementAgainstReferences = deprecate( "XML_CRYPTO_VALIDATE_ELEMENT_AGAINST_REFERENCES", ); +const warnEnvelopedSignatureAfterCanonicalization = deprecate( + () => {}, + "Applying `enveloped-signature` after a transform that returns a string, such as a canonicalization, is deprecated and will throw in a future version. List `enveloped-signature` before any canonicalization in the `Reference`.", + "XML_CRYPTO_ENVELOPED_SIGNATURE_AFTER_CANONICALIZATION", +); + export class SignedXml { idMode?: "wssecurity"; idAttributes: string[]; @@ -1444,6 +1450,7 @@ export class SignedXml { } let transformedXml: Node | string = canonXml; let transformOptions = options; + let parsedFromOctets = false; // Octets are parsed into a node-set for the next transform, and a node-set left at the end is // converted to octets with C14N: https://www.w3.org/TR/xmldsig-core1/#sec-ReferenceProcessingModel @@ -1458,6 +1465,15 @@ export class SignedXml { defaultNs: "", signatureNode: this.findLoadedSignature(transformedXml) ?? options.signatureNode, }; + parsedFromOctets = true; + } + // enveloped-signature takes only a node-set from its parent document: + // https://www.w3.org/TR/xmldsig-core1/#sec-EnvelopedSignature + if ( + parsedFromOctets && + transformName === "http://www.w3.org/2000/09/xmldsig#enveloped-signature" + ) { + warnEnvelopedSignatureAfterCanonicalization(); } // Each transform takes only the parameters of its own Transform: // https://www.w3.org/TR/xmldsig-core1/#sec-Transforms From 6004896c119b2f39bdd9b30482b6e5767176a706 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Tue, 13 Oct 2026 16:30:20 -0500 Subject: [PATCH 5/8] chore: deprecate renderNs() and processInner() ahead of 7.0 (#656) --- README.md | 4 ++++ src/c14n-canonicalization.ts | 25 ++++++++++++++------- src/exclusive-canonicalization.ts | 17 ++++++++------ src/utils.ts | 37 +++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4f822e50..9e25f6f1 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,10 @@ Calling one prints a `DeprecationWarning` naming its replacement. The three rege `util.deprecate` needs a call to intercept โ€” so TypeScript users see the `@deprecated` tag and JavaScript users get no signal until the names go away. +`renderNs()` and `processInner()` on `C14nCanonicalization` and `ExclusiveCanonicalization` are +deprecated and will be private in 7.0; call `process()` instead. Calling either one from outside +`process()`, or overriding one in a subclass, prints a `DeprecationWarning`. + `toPem`, `pemToDer`, `pemCertificates`, `normalizePem` and `findAncestorNs` are **not** deprecated and stay exported. diff --git a/src/c14n-canonicalization.ts b/src/c14n-canonicalization.ts index 4611ea33..abbe283f 100644 --- a/src/c14n-canonicalization.ts +++ b/src/c14n-canonicalization.ts @@ -85,6 +85,7 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg * @param ancestorNamespaces Import ancestor namespaces if it is specified * @param namespacesInScope The namespaces bound on this node's output ancestors, nearest last. * Without it, a prefix in `prefixesInScope` counts as in scope whatever it is bound to. + * @deprecated Will be private in 7.0. Call {@link process} instead of calling or overriding it. * @api private */ renderNs( @@ -95,6 +96,7 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg ancestorNamespaces: NamespacePrefix[], namespacesInScope?: NamespacePrefix[], ): RenderedNamespace { + utils.warnOutsideProcess(this); let i; let attr; const res: string[] = []; @@ -199,6 +201,7 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg /** * @param node Node + * @deprecated Will be private in 7.0. Call {@link process} instead of calling or overriding it. */ processInner( node, @@ -208,6 +211,7 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg ancestorNamespaces, namespacesInScope?: NamespacePrefix[], ) { + utils.warnOutsideProcess(this); if (isDomNode.isDocumentNode(node)) { const res: string[] = []; for (let i = 0; i < node.childNodes.length; i++) { @@ -220,6 +224,7 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg } if (utils.isCanonicalDocumentChild(child)) { res.push( + /* eslint-disable-next-line deprecation/deprecation */ this.processInner( child, prefixesInScope, @@ -246,6 +251,7 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg if (isDomNode.isElementNode(node)) { let i; let pfxCopy; + /* eslint-disable-next-line deprecation/deprecation */ const ns = this.renderNs( node, prefixesInScope, @@ -259,6 +265,7 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg for (i = 0; i < node.childNodes.length; ++i) { pfxCopy = prefixesInScope.slice(0); res.push( + /* eslint-disable-next-line deprecation/deprecation */ this.processInner( node.childNodes[i], pfxCopy, @@ -355,15 +362,17 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg prefixesInScope.push(ancestorNamespaces[i].prefix); } - const res = this.processInner( - canonicalNode, - prefixesInScope, - defaultNs, - defaultNsForPrefix, - ancestorNamespaces, - ancestorNamespaces.slice(), + return utils.insideProcess(this, C14nCanonicalization.prototype, () => + /* eslint-disable-next-line deprecation/deprecation */ + this.processInner( + canonicalNode, + prefixesInScope, + defaultNs, + defaultNsForPrefix, + ancestorNamespaces, + ancestorNamespaces.slice(), + ), ); - return res; } getAlgorithmName() { diff --git a/src/exclusive-canonicalization.ts b/src/exclusive-canonicalization.ts index 210eb61d..7b61bbf6 100644 --- a/src/exclusive-canonicalization.ts +++ b/src/exclusive-canonicalization.ts @@ -79,6 +79,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati * parents which are a part of the output set * @param {String} defaultNs. The current default namespace * @return {String} + * @deprecated Will be private in 7.0. Call {@link process} instead of calling or overriding it. * @api private */ renderNs( @@ -88,6 +89,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati defaultNsForPrefix, inclusiveNamespacesPrefixList: string[], ) { + utils.warnOutsideProcess(this); let i; let attr; const res: string[] = []; @@ -169,6 +171,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati /** * @param node Node + * @deprecated Will be private in 7.0. Call {@link process} instead of calling or overriding it. */ processInner( node, @@ -177,6 +180,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati defaultNsForPrefix, inclusiveNamespacesPrefixList: string[], ) { + utils.warnOutsideProcess(this); if (isDomNode.isDocumentNode(node)) { const res: string[] = []; for (let i = 0; i < node.childNodes.length; i++) { @@ -189,6 +193,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati } if (utils.isCanonicalDocumentChild(child)) { res.push( + /* eslint-disable-next-line deprecation/deprecation */ this.processInner( child, prefixesInScope, @@ -214,6 +219,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati if (isDomNode.isElementNode(node)) { let i; let pfxCopy; + /* eslint-disable-next-line deprecation/deprecation */ const ns = this.renderNs( node, prefixesInScope, @@ -226,6 +232,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati for (i = 0; i < node.childNodes.length; ++i) { pfxCopy = prefixesInScope.slice(0); res.push( + /* eslint-disable-next-line deprecation/deprecation */ this.processInner( node.childNodes[i], pfxCopy, @@ -347,14 +354,10 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati }); } - const res = this.processInner( - node, - [], - defaultNs, - defaultNsForPrefix, - inclusiveNamespacesPrefixList, + return utils.insideProcess(this, ExclusiveCanonicalization.prototype, () => + /* eslint-disable-next-line deprecation/deprecation */ + this.processInner(node, [], defaultNs, defaultNsForPrefix, inclusiveNamespacesPrefixList), ); - return res; } getAlgorithmName() { diff --git a/src/utils.ts b/src/utils.ts index 65f5142e..b8bfeb67 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,6 +2,7 @@ import { X509Certificate, createPrivateKey } from "crypto"; import * as xpath from "xpath"; import type { NamespacePrefix, PemLabel } from "./types"; import * as isDomNode from "@xmldom/is-dom-node"; +import { deprecate } from "util"; export function isArrayHasLength(array: unknown): array is unknown[] { return Array.isArray(array) && array.length > 0; @@ -643,6 +644,42 @@ export function isPrefixInScope( return binding !== undefined && binding.namespaceURI === namespaceURI; } +const warnCanonicalizationInternals = deprecate( + () => {}, + "`renderNs()` and `processInner()` are deprecated and will be private in version 7.0. Call `process()` instead of calling or overriding them.", + "XML_CRYPTO_CANONICALIZATION_INTERNALS", +); + +const canonicalizationsInProcess = new WeakSet(); + +// process() goes on calling renderNs() and processInner(), so that an override of either still +// takes effect. That leaves an override, and a call from anywhere else, as the uses to warn about. +export function insideProcess(canonicalization: object, ownMethods: object, run: () => T): T { + const isOverridden = (method: string) => + Reflect.get(canonicalization, method) !== Reflect.get(ownMethods, method); + if (["renderNs", "processInner"].some(isOverridden)) { + warnCanonicalizationInternals(); + } + + // A subclass can call process() again from inside it, and the outer call is still running + // when the inner one returns. + const isOutermost = !canonicalizationsInProcess.has(canonicalization); + canonicalizationsInProcess.add(canonicalization); + try { + return run(); + } finally { + if (isOutermost) { + canonicalizationsInProcess.delete(canonicalization); + } + } +} + +export function warnOutsideProcess(canonicalization: object): void { + if (!canonicalizationsInProcess.has(canonicalization)) { + warnCanonicalizationInternals(); + } +} + export function validateDigestValue(digest, expectedDigest) { const buffer = Buffer.from(digest, "base64"); const expectedBuffer = Buffer.from(expectedDigest, "base64"); From b4f1335d65cb9f1f110c44bbae48a5bdf65ce335 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Tue, 13 Oct 2026 18:16:49 -0500 Subject: [PATCH 6/8] chore: warn about input that 7.0 rejects (#660) --- README.md | 24 +++++++++++++++++ src/exclusive-canonicalization.ts | 15 +++++++++++ src/signed-xml.ts | 44 +++++++++++++++++++++++++++++-- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e25f6f1..3e8eefb7 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,30 @@ The error for a value the parser cannot read is `Invalid PEM format.`, in place as that file rather than base64-encoded, which is what made it a message with a body of base64-encoded PEM. Both forms of PEM the parser does read produce the same canonical output. +### Input that 7.0 rejects + +Each of these works in 6.x, prints a `DeprecationWarning`, and is rejected in 7.0. + +When verifying: + +- a `Reference` whose `URI` is neither empty nor `#` followed by an ID. It is resolved as a + same-document ID all the same, though only those two forms are + [same-document references](https://www.w3.org/TR/xmldsig-core1/#sec-ReferenceProcessingModel). +- a `Transform` with no `Algorithm` attribute, which is skipped. The + [schema](https://www.w3.org/TR/xmldsig-core1/#sec-Transforms) requires the attribute. +- an `InclusiveNamespaces` element outside the `http://www.w3.org/2001/10/xml-exc-c14n#` + [namespace](https://www.w3.org/TR/xml-exc-c14n/#sec-Use), whose `PrefixList` is applied all the + same. xml-crypto 6.3.2 and earlier wrote one when signing with `xml-exc-c14n#WithComments` and a + prefix list, so documents signed that way need signing again. + +When signing: + +- a `location.reference` that selects a text node, a comment or a processing instruction, which + `before` and `after` place the `Signature` next to. +- `isEmptyUri: true` on a reference whose `xpath` selects anything but the document element. + `URI=""` identifies [the whole document](https://www.w3.org/TR/xmldsig-core1/#sec-Same-Document), + so the signature this produces does not verify. + ### Deprecated ahead of 7.0 These exports are deprecated and will be removed in 7.0: diff --git a/src/exclusive-canonicalization.ts b/src/exclusive-canonicalization.ts index 7b61bbf6..251bff97 100644 --- a/src/exclusive-canonicalization.ts +++ b/src/exclusive-canonicalization.ts @@ -5,6 +5,20 @@ import type { } from "./types"; import * as utils from "./utils"; import * as isDomNode from "@xmldom/is-dom-node"; +import { deprecate } from "util"; + +const warnInclusiveNamespacesNamespace = deprecate( + () => {}, + "Reading an `InclusiveNamespaces` element that is not in the `http://www.w3.org/2001/10/xml-exc-c14n#` namespace is deprecated, and a signature that has one will not verify in version 7.0. xml-crypto 6.3.2 and earlier wrote one when signing with `xml-exc-c14n#WithComments` and a prefix list, so sign those documents again.", + "XML_CRYPTO_INCLUSIVE_NAMESPACES_NAMESPACE", +); + +// https://www.w3.org/TR/xml-exc-c14n/#sec-Use +export function warnUnlessExclusiveC14nNamespace(inclusiveNamespaces: Element): void { + if (inclusiveNamespaces.namespaceURI !== "http://www.w3.org/2001/10/xml-exc-c14n#") { + warnInclusiveNamespacesNamespace(); + } +} export class ExclusiveCanonicalization implements CanonicalizationOrTransformationAlgorithm { protected includeComments = false; @@ -320,6 +334,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati "InclusiveNamespaces", ); if (inclusiveNamespaces.length !== 0) { + warnUnlessExclusiveC14nNamespace(inclusiveNamespaces[0]); inclusiveNamespacesPrefixList = ( inclusiveNamespaces[0].getAttribute("PrefixList") || "" ).split(" "); diff --git a/src/signed-xml.ts b/src/signed-xml.ts index d181c6b8..bbad4e61 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -64,6 +64,30 @@ function decodeSignatureValue(signature: Node): Buffer { return Buffer.from(findSignatureValue(signature) ?? "", "base64"); } +const warnReferenceUriWithoutFragment = deprecate( + () => {}, + 'Resolving a `Reference` whose `URI` is neither empty nor starts with "#" as a same-document ID is deprecated, and such a signature will not verify in version 7.0. A same-document `URI` is empty, or "#" followed by an ID.', + "XML_CRYPTO_REFERENCE_URI_WITHOUT_FRAGMENT", +); + +const warnTransformWithoutAlgorithm = deprecate( + () => {}, + "Skipping a `Transform` that has no `Algorithm` attribute is deprecated, and a signature that has one will not verify in version 7.0.", + "XML_CRYPTO_TRANSFORM_WITHOUT_ALGORITHM", +); + +const warnLocationReferenceNotElement = deprecate( + () => {}, + "A `location.reference` that selects a node other than an element is deprecated and will throw in version 7.0. Select the element to place the signature in or next to.", + "XML_CRYPTO_LOCATION_REFERENCE_NOT_ELEMENT", +); + +const warnEmptyUriNotDocumentElement = deprecate( + () => {}, + '`isEmptyUri: true` on a reference whose `xpath` selects anything but the document element is deprecated and will throw in version 7.0. `URI=""` identifies the whole document, so the signature this produces does not verify.', + "XML_CRYPTO_EMPTY_URI_NOT_DOCUMENT_ELEMENT", +); + function certificatesToPublish(publicCert: string): string[] { const certificates = utils.pemCertificates(publicCert); if (certificates.length > 0) { @@ -619,6 +643,12 @@ export class SignedXml { const uri = ref.uri?.[0] === "#" ? ref.uri.substring(1) : ref.uri; let elem: xpath.SelectSingleReturnType = null; + // Only an empty URI and a fragment are same-document references: + // https://www.w3.org/TR/xmldsig-core1/#sec-ReferenceProcessingModel + if (uri !== "" && uri === ref.uri) { + warnReferenceUriWithoutFragment(); + } + if (uri === "") { elem = utils.hasDocumentLevelProcessingInstruction(doc) ? doc : xpath.select1("//*", doc); } else if (uri?.indexOf("'") !== -1) { @@ -817,16 +847,19 @@ export class SignedXml { if (transformAttr) { transforms.push(transformAttr.value); + const inclusiveNamespaces = utils.findChildren(transform, "InclusiveNamespaces"); + inclusiveNamespaces.forEach(execC14n.warnUnlessExclusiveC14nNamespace); // PrefixList is NMTOKENS, delimited by XML white space: // https://www.w3.org/TR/xml-exc-c14n/#sec-Use prefixLists.push( - utils - .findChildren(transform, "InclusiveNamespaces") + inclusiveNamespaces .flatMap((namespace) => (namespace.getAttribute("PrefixList") ?? "").split(/[ \t\r\n]+/), ) .filter((value) => value.length > 0), ); + } else { + warnTransformWithoutAlgorithm(); } } } @@ -1105,6 +1138,10 @@ export class SignedXml { } } + if (!isDomNode.isElementNode(referenceNode)) { + warnLocationReferenceNotElement(); + } + const previousSignatureNode = this.signatureNode; this.signatureNode = signatureElem; try { @@ -1277,6 +1314,9 @@ export class SignedXml { let targetUri: string; if (ref.isEmptyUri) { targetUri = ""; + if (node !== node.ownerDocument.documentElement) { + warnEmptyUriNotDocumentElement(); + } } else { const id = this.ensureHasId(node); ref.uri = id; From 8a7e264bad340c051a27b1da0683c04948526850 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Tue, 13 Oct 2026 18:20:08 -0500 Subject: [PATCH 7/8] chore: deprecate signing and verifying a document that has a DOCTYPE (#661) --- README.md | 9 +++++++++ src/signed-xml.ts | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/README.md b/README.md index 3e8eefb7..baba6079 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,15 @@ The `enveloped-signature` transform removes only the `Signature` element being v - `getCanonXml()` finds the loaded signature in the node's document the same way, and removes nothing when the signature is not there. +### Documents with a DOCTYPE + +`computeSignature()` and `checkSignature()` do not process a document type declaration. An entity +it declares is not expanded and a default attribute it declares is not added, though +[Canonical XML](https://www.w3.org/TR/xml-c14n#Terminology) requires both, so the canonical form is +not the one an implementation that processes the declaration computes. Signing or verifying a +document that has a DOCTYPE is deprecated and will throw in 7.0. Until then it prints a +`DeprecationWarning`. + ### Malformed certificates A certificate whose encapsulated data is not base64 is rejected rather than decoded as far as it diff --git a/src/signed-xml.ts b/src/signed-xml.ts index bbad4e61..2a7fee3b 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -108,6 +108,12 @@ const warnOriginalXmlWithIds = deprecate( "XML_CRYPTO_GET_ORIGINAL_XML_WITH_IDS", ); +const warnDocumentTypeDeclaration = deprecate( + () => {}, + "Signing or verifying a document that has a document type declaration (``) is deprecated and will throw in version 7.0. The declaration is not processed, so an entity it declares is not expanded and a default attribute it declares is not added to what is signed or verified.", + "XML_CRYPTO_DOCUMENT_TYPE_DECLARATION", +); + const warnValidateElementAgainstReferences = deprecate( () => {}, "`validateElementAgainstReferences()` is deprecated and will be removed in a future version. After `checkSignature()` succeeds, use the XML that `getSignedReferences()` returns instead of nodes from the original document.", @@ -365,6 +371,9 @@ export class SignedXml { this.signedXml = xml; const doc = new xmldom.DOMParser().parseFromString(xml); + if (doc.doctype != null) { + warnDocumentTypeDeclaration(); + } // Reset the references as only references from our re-parsed signedInfo node can be trusted this.references = []; @@ -1034,6 +1043,9 @@ export class SignedXml { } const doc = new xmldom.DOMParser().parseFromString(xml); + if (doc.doctype != null) { + warnDocumentTypeDeclaration(); + } let xmlNsAttr = "xmlns"; const signatureAttrs: string[] = []; let currentPrefix: string; From 84df7801bdca86ee373825494c46c2a318a60608 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Wed, 14 Oct 2026 07:38:27 -0500 Subject: [PATCH 8/8] feat: add computeSignatureAsync() and checkSignatureAsync(), fix asynchronous verification through a callback, and deprecate the callback form (#657) --- README.md | 123 ++++++++++-- src/index.ts | 17 ++ src/signed-xml.ts | 240 ++++++++++++++++++----- src/types.ts | 21 +- test/async-entry-points.spec.ts | 218 ++++++++++++++++++++ test/signature-integration-tests.spec.ts | 2 + test/signature-unit-tests.spec.ts | 2 + test/types-tests.spec.ts | 3 + 8 files changed, 561 insertions(+), 65 deletions(-) create mode 100644 test/async-entry-points.spec.ts diff --git a/README.md b/README.md index baba6079..497943c3 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,11 @@ deprecated and will be private in 7.0; call `process()` instead. Calling either `toPem`, `pemToDer`, `pemCertificates`, `normalizePem` and `findAncestorNs` are **not** deprecated and stay exported. +The callback form of `computeSignature()` and `checkSignature()` is deprecated, and so are +`createOptionalCallbackFunction` and `ErrorFirstCallback`. `computeSignatureAsync()` and +`checkSignatureAsync()` replace them; see +[Moving from the callback form](#moving-from-the-callback-form). + `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). @@ -425,6 +430,7 @@ To sign xml documents: - `attrs` - a hash of attributes and values `attrName: value` to add to the signature root node - `location` - customize the location of the signature, pass an object with a `reference` key which should contain a XPath expression to a reference node, an `action` key which should contain one of the following values: `append`, `prepend`, `before`, `after` - `existingPrefixes` - A hash of prefixes and namespaces `prefix: namespace` that shouldn't be in the signature because they already exist in the xml +- `computeSignatureAsync(xml, [options])` - `computeSignature` for a signature algorithm that signs asynchronously; returns a promise for the `SignedXml` instance. See [Asynchronous signing and verification](#asynchronous-signing-and-verification) - `getSignedXml()` - returns the original xml document with the signature in it, **must be called only after `computeSignature`** - `getSignatureXml()` - returns just the signature part, **must be called only after `computeSignature`** - `getOriginalXmlWithIds()` - **[deprecated]** returns the original xml with Id attributes added on relevant elements, **must be called only after `computeSignature`**. Use the `location` option of `computeSignature()` to place the signature, then `getSignedXml()`. See [how to specify the location of the signature](#how-to-specify-the-location-of-the-signature). For a detached signature, put an ID attribute the signer recognizes on each referenced element (`wsu:Id` for WS-Security), sign that document, and send it alongside `getSignatureXml()`. Make sure each reference XPath still selects its intended element once those IDs are present, for example by selecting on the ID itself. @@ -445,6 +451,7 @@ To verify xml documents: - `loadSignature(signatureXml)` - loads the signature where: - `signatureXml` - a string or node object (like an [xmldom](https://github.com/xmldom/xmldom) node) containing the xml representation of the signature - `checkSignature(xml)` - validates the given xml document and returns `true` if the validation was successful +- `checkSignatureAsync(xml)` - `checkSignature` for a signature algorithm that verifies asynchronously; returns a promise for `true` if the validation was successful - `getSignedReferences()` - returns the canonical XML of each reference, only after `checkSignature` succeeds - `validateElementAgainstReferences(elemOrXpath, doc)` - **[deprecated]** after `checkSignature` succeeds, use the XML that `getSignedReferences()` returns instead of nodes from the original document @@ -572,9 +579,9 @@ signXml(xml, "//*[local-name(.)='book']", "client.pem", "result.xml"); You can always look at the actual code as a sample. -## Asynchronous signing +## 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. Register it under the URI of the algorithm it implements, which is the `SignatureMethod` a verifier reads. +If the private key is not stored locally, and you wish to use a signing server or Hardware Security Module (HSM) to sign documents, the signature algorithm has to wait for its answer. Give a custom signature algorithm a `getSignatureAsync()` method that returns a promise, and sign with `computeSignatureAsync()`. Register the algorithm under the URI of the algorithm it implements, which is the `SignatureMethod` a verifier reads. ```javascript const { SignedXml } = require("xml-crypto"); @@ -582,11 +589,18 @@ const crypto = require("crypto"); const fs = require("fs"); class AsyncRsaSha256 { - getSignature(signedInfo, privateKey, callback) { + async getSignatureAsync(signedInfo, privateKey) { // Do some asynchronous things here, such as calling a signing server. const signer = crypto.createSign("RSA-SHA256"); signer.update(signedInfo); - callback(null, signer.sign(privateKey, "base64")); + return signer.sign(privateKey, "base64"); + } + + async verifySignatureAsync(material, key, signatureValue) { + // Do some asynchronous things here, too. + const verifier = crypto.createVerify("RSA-SHA256"); + verifier.update(material); + return verifier.verify(key, signatureValue, "base64"); } getAlgorithmName() { @@ -594,28 +608,103 @@ class AsyncRsaSha256 { } } -const xml = "Harry Potter"; +async function sign(xml) { + 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#"], + }); -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#"], + await sig.computeSignatureAsync(xml); + return sig; +} + +sign("Harry Potter").then((sig) => + fs.writeFileSync("signed.xml", sig.getSignedXml()), +); +``` + +`computeSignatureAsync()` takes what `computeSignature()` takes and resolves with the `SignedXml` instance. `checkSignatureAsync()` does the same for `checkSignature()`: it waits for the algorithm's `verifySignatureAsync()` and resolves with `true` for a valid signature. + +```javascript +async function verify(xml, signature) { + const sig = new SignedXml({ publicCert: fs.readFileSync("client_public.pem") }); + sig.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] = AsyncRsaSha256; + sig.loadSignature(signature); + + if (!(await sig.checkSignatureAsync(xml))) { + throw new Error("Invalid signature"); + } + return sig.getSignedReferences(); +} +``` + +Both call `getSignature()` or `verifySignature()` on an algorithm that has no `Async` method, so they work with the built-in algorithms too. The `SignatureAlgorithm` type requires `getSignature()` and `verifySignature()`; an algorithm that only works asynchronously can have them throw. + +### Moving from the callback form + +The callback form of `computeSignature()` and `checkSignature()` is deprecated and will be removed in 7.0. Passing a callback prints a `DeprecationWarning`. + +With a built-in algorithm, or any other that returns its answer, call them without the callback: + +```javascript +// Before +sig.computeSignature(xml, (err) => { + if (err) { + throw err; + } + fs.writeFileSync("signed.xml", sig.getSignedXml()); }); + +// After +sig.computeSignature(xml); +fs.writeFileSync("signed.xml", sig.getSignedXml()); +``` + +With an algorithm that answers through a callback, return a promise from an `Async` method instead and call the `Async` entry point: + +```javascript +// Before +class AsyncRsaSha256 { + getSignature(signedInfo, privateKey, callback) { + signingServer.sign(signedInfo, callback); + } + + verifySignature(material, key, signatureValue, callback) { + signingServer.verify(material, signatureValue, callback); + } +} + sig.computeSignature(xml, (err) => { if (err) { - console.error(err); - return; + throw err; } fs.writeFileSync("signed.xml", sig.getSignedXml()); }); + +// After +class AsyncRsaSha256 { + getSignatureAsync(signedInfo, privateKey) { + return signingServer.sign(signedInfo); + } + + verifySignatureAsync(material, key, signatureValue) { + return signingServer.verify(material, signatureValue); + } +} + +await sig.computeSignatureAsync(xml); +fs.writeFileSync("signed.xml", sig.getSignedXml()); ``` +`createOptionalCallbackFunction` and `ErrorFirstCallback` serve only the callback form. An algorithm needs neither once its methods return their answer or a promise for it. + ## 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): diff --git a/src/index.ts b/src/index.ts index 2260806c..df5d0d30 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import { deprecate } from "util"; +import * as types from "./types"; import * as utils from "./utils"; export { C14nCanonicalization, C14nCanonicalizationWithComments } from "./c14n-canonicalization"; @@ -22,6 +23,22 @@ export { findAncestorNs, normalizePem, pemCertificates, pemToDer, toPem } from " * own calls is what made #497 unpleasant. */ +/** + * @deprecated Will be removed in 7.0. Write a signature algorithm's methods without a callback, + * and give it `getSignatureAsync()` and `verifySignatureAsync()` if it works asynchronously. + */ +export const createOptionalCallbackFunction = deprecate( + types.createOptionalCallbackFunction, + "`createOptionalCallbackFunction()` is deprecated and will be removed in version 7.0. Write a signature algorithm's methods without a callback, and give it `getSignatureAsync()` and `verifySignatureAsync()` if it works asynchronously.", + "XML_CRYPTO_CREATE_OPTIONAL_CALLBACK_FUNCTION", +); + +/** + * @deprecated Will be removed in 7.0, with the callback form of `computeSignature()` and + * `checkSignature()`. `computeSignatureAsync()` and `checkSignatureAsync()` return a promise. + */ +export type ErrorFirstCallback = types.ErrorFirstCallback; + /** * @deprecated Will be removed in 7.0. This is an internal predicate with no replacement; use * `Array.isArray(x) && x.length > 0`. diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 2a7fee3b..a6735089 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -28,6 +28,7 @@ import * as signatureAlgorithms from "./signature-algorithms"; import * as utils from "./utils"; type SigningReferenceTarget = { node: Element; digestValue?: string }; +type UnsignedSignature = { doc: Document; insertSignatureValue: () => void }; type C14nOptionsWithXmlAttributes = CanonicalizationOrTransformationAlgorithmProcessOptions & { ancestorXmlAttributes?: utils.InheritedXmlAttribute[]; }; @@ -102,6 +103,12 @@ function certificatesToPublish(publicCert: string): string[] { } } +const warnCallbackForm = deprecate( + () => {}, + "The callback form of `computeSignature()` and `checkSignature()` is deprecated and will be removed in version 7.0. Call them without a callback. For a signature algorithm that works asynchronously, implement `getSignatureAsync()` and `verifySignatureAsync()` on it and call `computeSignatureAsync()` and `checkSignatureAsync()`, which return a promise.", + "XML_CRYPTO_CALLBACK_FORM", +); + const warnOriginalXmlWithIds = deprecate( () => {}, "`getOriginalXmlWithIds()` is deprecated and will be removed in a future version. Use the `location` option of `computeSignature()` to place the signature, then `getSignedXml()`. For a detached signature, put an ID attribute the signer recognizes on each referenced element (`wsu:Id` for WS-Security), sign that document, and send it alongside `getSignatureXml()`.", @@ -348,11 +355,14 @@ export class SignedXml { */ checkSignature(xml: string): boolean; /** - * Validates the signature of the provided XML document synchronously using the configured key info provider. + * Validates the signature of the provided XML document using the configured key info provider. + * The signature algorithm is given a callback to answer through. * * @param xml The XML document containing the signature to be validated. - * @param callback Called with the validation result before `checkSignature` returns. + * @param callback Called with the validation result. * @throws Error if the last parameter is provided and is not a function, or if no key info resolver is provided. + * @deprecated Will be removed in 7.0. Call `checkSignature(xml)`, or `checkSignatureAsync(xml)` + * for a signature algorithm that verifies asynchronously. */ checkSignature(xml: string, callback: (error: Error | null, isValid?: boolean) => void): void; checkSignature( @@ -362,7 +372,93 @@ export class SignedXml { if (callback != null && typeof callback !== "function") { throw new Error("Last parameter must be a callback function"); } + if (callback) { + warnCallbackForm(); + } + + const pending = this.validateReferences(xml, callback); + if (!pending) { + return pending; + } + const { signer, key, unverifiedSignedInfoCanon, accept } = pending; + + if (!callback) { + if (signer.verifySignature(unverifiedSignedInfoCanon, key, this.signatureValue) !== true) { + throw this.invalidSignature(); + } + accept(); + return true; + } + + let answered = false; + const answer = (error: Error | null, isValid?: boolean) => { + if (answered) { + return; + } + answered = true; + if (error) { + callback(error); + } else if (isValid !== true) { + callback(this.invalidSignature()); + } else { + accept(); + callback(null, true); + } + }; + /* eslint-disable-next-line deprecation/deprecation */ + const returned: unknown = signer.verifySignature( + unverifiedSignedInfoCanon, + key, + this.signatureValue, + answer, + ); + // An algorithm that verifies synchronously can ignore the callback and return its verdict. + if (typeof returned === "boolean") { + answer(null, returned); + } + } + /** + * Validates the signature of the provided XML document using the configured key info provider, + * waiting for a signature algorithm that implements `verifySignatureAsync()`. An algorithm + * without it is called as `checkSignature(xml)` calls it. + * + * @param xml The XML document containing the signature to be validated. + * @returns a promise for `true` if the signature is valid, and for `false` if a reference does + * not validate + * @throws Error rejects with it if the signature value is incorrect, or if no key info resolver + * is provided. + */ + async checkSignatureAsync(xml: string): Promise { + const pending = this.validateReferences(xml); + if (!pending) { + return false; + } + const { signer, key, unverifiedSignedInfoCanon, accept } = pending; + + const isValid = + typeof signer.verifySignatureAsync === "function" + ? await signer.verifySignatureAsync(unverifiedSignedInfoCanon, key, this.signatureValue) + : signer.verifySignature(unverifiedSignedInfoCanon, key, this.signatureValue); + if (isValid !== true) { + throw this.invalidSignature(); + } + accept(); + return true; + } + + private invalidSignature(): Error { + // TODO: add this breaking change here later on for even more security: `this.references = [];` + return new Error(`invalid signature: the signature value ${this.signatureValue} is incorrect`); + } + + // Core validation up to the signature over `SignedInfo`, which is left to the caller because the + // signature algorithm may answer later. Returns `false`, or nothing once the callback has the + // reason, when a reference does not validate. + private validateReferences( + xml: string, + callback?: (error: Error | null, isValid?: boolean) => void, + ) { // Nothing this check has not authenticated may be visible, even if it throws partway. Success // adds to what earlier successful checks on this instance published. const earlierSignedReferences = this.signedReferences; @@ -450,33 +546,16 @@ export class SignedXml { throw new Error("KeyInfo or publicCert or privateKey is required to validate signature"); } - // Check the signature verification to know whether to reset signature value or not. - const sigRes = signer.verifySignature(unverifiedSignedInfoCanon, key, this.signatureValue); - if (sigRes === true) { + // An asynchronous algorithm may answer after another check has replaced `this.references`. + const loadedReferences = this.references; + const accept = () => { this.signedReferences = [...earlierSignedReferences, ...canonReferences]; - this.references.forEach((ref, index) => { + loadedReferences.forEach((ref, index) => { ref.signedReference = canonReferences[index]; }); + }; - if (callback) { - callback(null, true); - } else { - return true; - } - } else { - // TODO: add this breaking change here later on for even more security: `this.references = [];` - - if (callback) { - callback( - new Error(`invalid signature: the signature value ${this.signatureValue} is incorrect`), - ); - return; // return early - } else { - throw new Error( - `invalid signature: the signature value ${this.signatureValue} is incorrect`, - ); - } - } + return { signer, key, unverifiedSignedInfoCanon, accept }; } private getCanonSignedInfoXml(doc: Document) { @@ -564,19 +643,40 @@ export class SignedXml { ); } - private calculateSignatureValue(doc: Document, callback?: ErrorFirstCallback) { + private signingInput(doc: Document) { const signedInfoCanon = this.getCanonSignedInfoXml(doc); const signer = this.findSignatureAlgorithm(this.signatureAlgorithm); if (this.privateKey == null) { throw new Error("Private key is required to compute signature"); } + return { signedInfoCanon, signer, privateKey: this.privateKey }; + } + + private calculateSignatureValue(doc: Document, callback?: ErrorFirstCallback) { + const { signedInfoCanon, signer, privateKey } = this.signingInput(doc); if (typeof callback === "function") { - signer.getSignature(signedInfoCanon, this.privateKey, callback); + /* eslint-disable-next-line deprecation/deprecation */ + signer.getSignature(signedInfoCanon, privateKey, callback); } else { - this.signatureValue = signer.getSignature(signedInfoCanon, this.privateKey); + this.signatureValue = signer.getSignature(signedInfoCanon, privateKey); } } + private async calculateSignatureValueAsync(doc: Document) { + const { signedInfoCanon, signer, privateKey } = this.signingInput(doc); + const signatureValue: unknown = + typeof signer.getSignatureAsync === "function" + ? await signer.getSignatureAsync(signedInfoCanon, privateKey) + : signer.getSignature(signedInfoCanon, privateKey); + // An algorithm written for the callback form returns nothing when it is called without one. + if (typeof signatureValue !== "string") { + throw new Error( + "the signature algorithm returned no signature; an algorithm that signs asynchronously implements getSignatureAsync()", + ); + } + this.signatureValue = signatureValue; + } + private findSignatureAlgorithm(name?: SignatureAlgorithmType) { if (name == null) { throw new Error("signatureAlgorithm is required"); @@ -1000,6 +1100,8 @@ export class SignedXml { * @param callback A callback function to handle the signature computation asynchronously. * @returns void * @throws TypeError If the xml can not be parsed. + * @deprecated Will be removed in 7.0. Call `computeSignature(xml)`, or `computeSignatureAsync(xml)` + * for a signature algorithm that signs asynchronously. */ computeSignature(xml: string, callback: ErrorFirstCallback): void; @@ -1021,6 +1123,8 @@ export class SignedXml { * @param callback A callback function to handle the signature computation asynchronously. * @returns void * @throws TypeError If the xml can not be parsed, or Error if there were invalid options passed. + * @deprecated Will be removed in 7.0. Call `computeSignature(xml, options)`, or + * `computeSignatureAsync(xml, options)` for a signature algorithm that signs asynchronously. */ computeSignature( xml: string, @@ -1041,7 +1145,66 @@ export class SignedXml { callback = callbackParam as ErrorFirstCallback; options = (options ?? {}) as ComputeSignatureOptions; } + if (typeof callback === "function") { + warnCallbackForm(); + } + + const unsigned = this.prepareSignature(xml, options, callback); + if (!unsigned) { + return; + } + + if (typeof callback === "function") { + // Asynchronous flow + this.calculateSignatureValue(unsigned.doc, (err, signature) => { + if (err) { + callback(err); + } else { + this.signatureValue = signature || ""; + unsigned.insertSignatureValue(); + callback(null, this); + } + }); + } else { + // Synchronous flow + this.calculateSignatureValue(unsigned.doc); + unsigned.insertSignatureValue(); + } + } + /** + * Compute the signature of the given XML (using the already defined settings), waiting for a + * signature algorithm that implements `getSignatureAsync()`. An algorithm without it is called + * as `computeSignature(xml)` calls it. + * + * @param xml The XML to compute the signature for. + * @param options An object containing options for the signature computation. + * @returns a promise for this instance + * @throws TypeError rejects with it if the xml can not be parsed, or with an Error if there were + * invalid options passed. + */ + async computeSignatureAsync( + xml: string, + options: ComputeSignatureOptions = {}, + ): Promise { + const unsigned = this.prepareSignature(xml, options); + await this.calculateSignatureValueAsync(unsigned.doc); + unsigned.insertSignatureValue(); + return this; + } + + // Everything but the signature value. Returns nothing once the callback has the reason it stopped. + private prepareSignature(xml: string, options: ComputeSignatureOptions): UnsignedSignature; + private prepareSignature( + xml: string, + options: ComputeSignatureOptions, + callback: ErrorFirstCallback, + ): UnsignedSignature | undefined; + private prepareSignature( + xml: string, + options: ComputeSignatureOptions, + callback?: ErrorFirstCallback, + ) { const doc = new xmldom.DOMParser().parseFromString(xml); if (doc.doctype != null) { warnDocumentTypeDeclaration(); @@ -1198,26 +1361,13 @@ export class SignedXml { } const signedInfoNode = signedInfoNodes[0]; - if (typeof callback === "function") { - // Asynchronous flow - this.calculateSignatureValue(doc, (err, signature) => { - if (err) { - callback(err); - } else { - this.signatureValue = signature || ""; - signatureElem.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling); - this.signatureXml = this.serialize(signatureElem); - this.signedXml = this.serialize(doc); - callback(null, this); - } - }); - } else { - // Synchronous flow - this.calculateSignatureValue(doc); + const insertSignatureValue = () => { signatureElem.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling); this.signatureXml = this.serialize(signatureElem); this.signedXml = this.serialize(doc); - } + }; + + return { doc, insertSignatureValue }; } // The signer parses its input, so a carriage return reaches the DOM only through a character diff --git a/src/types.ts b/src/types.ts index 05ce63c9..87e622f2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -201,26 +201,41 @@ export interface SignatureAlgorithm { * Sign the given string using the given key */ getSignature(signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string; + /** @deprecated Will be removed in 7.0. Implement `getSignatureAsync()` instead. */ getSignature( signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike, callback?: ErrorFirstCallback, ): void; + /** + * Sign the given string using the given key, for an algorithm that has to wait for the + * signature. `SignedXml.computeSignatureAsync` calls it in place of `getSignature`. + */ + getSignatureAsync?(signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): Promise; /** * Verify the given signature of the given string using key * * @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. - */ + /** @deprecated Will be removed in 7.0. Implement `verifySignatureAsync()` instead. */ verifySignature( material: string, key: crypto.KeyLike, signatureValue: string, callback?: ErrorFirstCallback, ): void; + /** + * Verify the given signature of the given string using key, for an algorithm that has to wait + * for the verdict. `SignedXml.checkSignatureAsync` calls it in place of `verifySignature`. + * + * @param key a public cert, public key, or private key can be passed here + */ + verifySignatureAsync?( + material: string, + key: crypto.KeyLike, + signatureValue: string, + ): Promise; getAlgorithmName(): SignatureAlgorithmType; } diff --git a/test/async-entry-points.spec.ts b/test/async-entry-points.spec.ts new file mode 100644 index 00000000..486a0876 --- /dev/null +++ b/test/async-entry-points.spec.ts @@ -0,0 +1,218 @@ +import { SignedXml, type SignatureAlgorithm } from "../src/index"; +import * as assert from "assert"; +import * as crypto from "crypto"; +import * as fs from "fs"; +import { expect } from "chai"; + +const RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; +const EXCLUSIVE_C14N = "http://www.w3.org/2001/10/xml-exc-c14n#"; +const xml = "trusted"; + +const elsewhere = () => new Promise((resolve) => setImmediate(resolve)); + +class RemoteRsaSha256 implements SignatureAlgorithm { + getAlgorithmName = () => RSA_SHA256; + + getSignature(): never { + throw new Error("this algorithm signs only asynchronously"); + } + + verifySignature(): never { + throw new Error("this algorithm verifies only asynchronously"); + } + + async getSignatureAsync(signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike) { + await elsewhere(); + return crypto.createSign("RSA-SHA256").update(signedInfo).sign(privateKey, "base64"); + } + + async verifySignatureAsync(material: string, key: crypto.KeyLike, signatureValue: string) { + await elsewhere(); + return crypto.createVerify("RSA-SHA256").update(material).verify(key, signatureValue, "base64"); + } +} + +function createSigner(algorithm?: new () => SignatureAlgorithm): SignedXml { + const sig = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: EXCLUSIVE_C14N, + signatureAlgorithm: RSA_SHA256, + }); + if (algorithm) { + sig.SignatureAlgorithms[RSA_SHA256] = algorithm; + } + sig.addReference({ + xpath: "/*", + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature", EXCLUSIVE_C14N], + }); + return sig; +} + +function createVerifier(signatureXml: string, algorithm?: new () => SignatureAlgorithm): SignedXml { + const sig = new SignedXml({ publicCert: fs.readFileSync("./test/static/client_public.pem") }); + if (algorithm) { + sig.SignatureAlgorithms[RSA_SHA256] = algorithm; + } + sig.loadSignature(signatureXml); + return sig; +} + +describe("computeSignatureAsync()", function () { + function signedSynchronously(): string { + const sig = createSigner(); + sig.computeSignature(xml); + return sig.getSignedXml(); + } + + it("signs through the algorithm's getSignatureAsync()", async function () { + const sig = createSigner(RemoteRsaSha256); + + await sig.computeSignatureAsync(xml); + + expect(sig.getSignedXml()).to.equal(signedSynchronously()); + }); + + it("signs through getSignature() when the algorithm has no getSignatureAsync()", async function () { + const sig = createSigner(); + + await sig.computeSignatureAsync(xml); + + expect(sig.getSignedXml()).to.equal(signedSynchronously()); + }); + + it("resolves with the instance", async function () { + const sig = createSigner(RemoteRsaSha256); + + expect(await sig.computeSignatureAsync(xml)).to.equal(sig); + }); + + it("takes the options computeSignature() takes", async function () { + const sig = createSigner(RemoteRsaSha256); + + await sig.computeSignatureAsync(xml, { prefix: "ds" }); + + expect(sig.getSignatureXml()).to.match(/^ { + throw new Error("signing server unavailable"); + } + } + const sig = createSigner(Unavailable); + + await assert.rejects(sig.computeSignatureAsync(xml), { message: "signing server unavailable" }); + expect(sig.getSignedXml()).to.equal(""); + }); + + it("rejects an algorithm that answers only through a callback", async function () { + class AnswersThroughCallback implements SignatureAlgorithm { + getAlgorithmName = () => RSA_SHA256; + + verifySignature(): never { + throw new Error("this algorithm only signs"); + } + + getSignature = function ( + signedInfo: crypto.BinaryLike, + privateKey: crypto.KeyLike, + callback?: (error: Error | null, signature?: string) => void, + ) { + const signature = crypto.createSign("RSA-SHA256").update(signedInfo); + callback?.(null, signature.sign(privateKey, "base64")); + } as unknown as SignatureAlgorithm["getSignature"]; + } + const sig = createSigner(AnswersThroughCallback); + + await assert.rejects(sig.computeSignatureAsync(xml), { + message: + "the signature algorithm returned no signature; an algorithm that signs asynchronously implements getSignatureAsync()", + }); + expect(sig.getSignedXml()).to.equal(""); + }); +}); + +describe("checkSignatureAsync()", function () { + const signer = createSigner(); + signer.computeSignature(xml); + const signedXml = signer.getSignedXml(); + const signatureXml = signer.getSignatureXml(); + const signedContent = 'trusted'; + + // The references still match their digests; only the signature over SignedInfo does not verify. + const withAnotherSignatureValue = (signed: string) => + signed.replace( + /()(.)/, + (match, open: string, first: string) => open + (first === "A" ? "B" : "A"), + ); + + it("accepts a valid signature, publishing what it signs only once the algorithm answers", async function () { + const sig = createVerifier(signatureXml, RemoteRsaSha256); + + const verdict = sig.checkSignatureAsync(signedXml); + + expect(sig.getSignedReferences()).to.deep.equal([]); + expect(await verdict).to.be.true; + expect(sig.getSignedReferences()).to.deep.equal([signedContent]); + }); + + it("verifies through verifySignature() when the algorithm has no verifySignatureAsync()", async function () { + const sig = createVerifier(signatureXml); + + expect(await sig.checkSignatureAsync(signedXml)).to.be.true; + expect(sig.getSignedReferences()).to.deep.equal([signedContent]); + }); + + it("rejects a signature value that does not verify, publishing nothing", async function () { + const sig = createVerifier(withAnotherSignatureValue(signatureXml), RemoteRsaSha256); + + await assert.rejects(sig.checkSignatureAsync(withAnotherSignatureValue(signedXml)), { + message: /^invalid signature: the signature value /, + }); + expect(sig.getSignedReferences()).to.deep.equal([]); + }); + + it("rejects a verdict other than true, publishing nothing", async function () { + class AnswersWithText extends RemoteRsaSha256 { + async verifySignatureAsync() { + return "true" as unknown as boolean; + } + } + const sig = createVerifier(signatureXml, AnswersWithText); + + await assert.rejects(sig.checkSignatureAsync(signedXml), { + message: /^invalid signature: the signature value /, + }); + expect(sig.getSignedReferences()).to.deep.equal([]); + }); + + it("rejects when the algorithm does, publishing nothing", async function () { + class Unreachable extends RemoteRsaSha256 { + async verifySignatureAsync(): Promise { + throw new Error("HSM unreachable"); + } + } + const sig = createVerifier(signatureXml, Unreachable); + + await assert.rejects(sig.checkSignatureAsync(signedXml), { message: "HSM unreachable" }); + expect(sig.getSignedReferences()).to.deep.equal([]); + }); + + it("resolves false, as checkSignature() returns false, for content that does not match its digest", async function () { + const sig = createVerifier(signatureXml, RemoteRsaSha256); + + expect(await sig.checkSignatureAsync(signedXml.replace("trusted", "tampered"))).to.be.false; + expect(sig.getSignedReferences()).to.deep.equal([]); + }); +}); diff --git a/test/signature-integration-tests.spec.ts b/test/signature-integration-tests.spec.ts index 50b8ef93..6b1fddaf 100644 --- a/test/signature-integration-tests.spec.ts +++ b/test/signature-integration-tests.spec.ts @@ -1005,6 +1005,7 @@ describe("Signature integration tests", function () { detached === "input IDs" ? 'trusted' : "trusted"; if (useCallback) { await new Promise((resolve, reject) => { + /* eslint-disable-next-line deprecation/deprecation */ signer.computeSignature(xml, (err) => (err ? reject(err) : resolve())); }); } else { @@ -1325,6 +1326,7 @@ describe("Signature integration tests", function () { const xml = "a b c"; if (useCallback) { await new Promise((resolve, reject) => { + /* eslint-disable-next-line deprecation/deprecation */ signer.computeSignature(xml, (err) => (err ? reject(err) : resolve())); }); } else { diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index f1006b8e..80465d97 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -857,6 +857,7 @@ describe("Signature unit tests", function () { return true; }; + /* eslint-disable-next-line deprecation/deprecation */ getSignature = createOptionalCallbackFunction( (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike) => { const signer = crypto.createSign("RSA-SHA1"); @@ -895,6 +896,7 @@ describe("Signature unit tests", function () { }); sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + /* eslint-disable-next-line deprecation/deprecation */ sig.computeSignature(xml, function () { const signedXml = sig.getSignedXml(); const expected = diff --git a/test/types-tests.spec.ts b/test/types-tests.spec.ts index 037b4e47..15a2d4e6 100644 --- a/test/types-tests.spec.ts +++ b/test/types-tests.spec.ts @@ -22,11 +22,13 @@ describe("Callback invocation", function () { const sig = createSigner(fs.readFileSync("./test/static/client.pem")); const errorsSeen: (string | null)[] = []; + /* eslint-disable-next-line deprecation/deprecation */ const callback: ErrorFirstCallback = (err) => { errorsSeen.push(err ? err.message : null); throw new Error("Error Thrown"); }; + /* eslint-disable-next-line deprecation/deprecation */ expect(() => sig.computeSignature(xml, callback)).to.throw("Error Thrown"); expect(errorsSeen).to.deep.equal([null]); }); @@ -35,6 +37,7 @@ describe("Callback invocation", function () { const sig = createSigner(fs.readFileSync("./test/static/client_public.pem")); const outcomes: string[] = []; + /* eslint-disable-next-line deprecation/deprecation */ sig.computeSignature(xml, (err) => outcomes.push(err ? "error" : "success")); expect(outcomes).to.deep.equal(["error"]);