From ac33ab81206c44a05594aed289c48a890f028e91 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Tue, 13 Oct 2026 13:03:26 -0400 Subject: [PATCH 1/4] chore: deprecate renderNs() and processInner() ahead of 7.0 7.0 makes both methods private on C14nCanonicalization and ExclusiveCanonicalization. Mark them @deprecated, and print a DeprecationWarning when one is called from outside process() or overridden in a subclass. process() keeps calling them, so an override still takes effect in 6.x. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 +++ src/c14n-canonicalization.ts | 25 +++++++++++----- src/exclusive-canonicalization.ts | 17 ++++++----- src/utils.ts | 32 ++++++++++++++++++++ test/c14n-non-exclusive-unit-tests.spec.ts | 34 ++++++++++++++++++++++ test/canonicalization-unit-tests.spec.ts | 34 ++++++++++++++++++++++ 6 files changed, 131 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index f5309ed9..72a028aa 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,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 ec5ec0c1..d894f240 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import { X509Certificate } from "crypto"; +import { deprecate } from "util"; import * as xpath from "xpath"; import type { NamespacePrefix, PemLabel } from "./types"; import * as isDomNode from "@xmldom/is-dom-node"; @@ -620,6 +621,37 @@ 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(); + } + + canonicalizationsInProcess.add(canonicalization); + try { + return run(); + } finally { + 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"); diff --git a/test/c14n-non-exclusive-unit-tests.spec.ts b/test/c14n-non-exclusive-unit-tests.spec.ts index 447a69ae..9e438100 100644 --- a/test/c14n-non-exclusive-unit-tests.spec.ts +++ b/test/c14n-non-exclusive-unit-tests.spec.ts @@ -503,3 +503,37 @@ describe("C14N non-exclusive canonicalization tests", function () { }); } }); + +describe("C14nCanonicalization renderNs() and processInner(), public until 7.0", function () { + function parseRoot() { + const doc = new xmldom.DOMParser().parseFromString( + 'text', + "text/xml", + ); + const root = xpath.select1("/*", doc); + isDomNode.assertIsElementNode(root); + return root; + } + + it("canonicalizes through processInner() called directly", function () { + const canonicalization = new C14nCanonicalization(); + const root = parseRoot(); + + /* eslint-disable-next-line deprecation/deprecation */ + expect(canonicalization.processInner(root, [], "", {}, [])).to.equal( + canonicalization.process(root, {}), + ); + }); + + it("uses an override of renderNs()", function () { + class WithoutNamespaces extends C14nCanonicalization { + renderNs() { + return { rendered: "", newDefaultNs: "" }; + } + } + + expect(new WithoutNamespaces().process(parseRoot(), {})).to.equal( + "text", + ); + }); +}); diff --git a/test/canonicalization-unit-tests.spec.ts b/test/canonicalization-unit-tests.spec.ts index 81535257..2e9b15e3 100644 --- a/test/canonicalization-unit-tests.spec.ts +++ b/test/canonicalization-unit-tests.spec.ts @@ -607,3 +607,37 @@ describe("Canonicalization unit tests", function () { ); }); }); + +describe("ExclusiveCanonicalization renderNs() and processInner(), public until 7.0", function () { + function parseRoot() { + const doc = new xmldom.DOMParser().parseFromString( + 'text', + "text/xml", + ); + const root = xpath.select1("/*", doc); + isDomNode.assertIsElementNode(root); + return root; + } + + it("canonicalizes through processInner() called directly", function () { + const canonicalization = new ExclusiveCanonicalization(); + const root = parseRoot(); + + /* eslint-disable-next-line deprecation/deprecation */ + expect(canonicalization.processInner(root, [], "", {}, [])).to.equal( + canonicalization.process(root, {}), + ); + }); + + it("uses an override of renderNs()", function () { + class WithoutNamespaces extends ExclusiveCanonicalization { + renderNs() { + return { rendered: "", newDefaultNs: "" }; + } + } + + expect(new WithoutNamespaces().process(parseRoot(), {})).to.equal( + "text", + ); + }); +}); From 521411e95b0b776f2211ed147eddb630eb522a30 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Tue, 13 Oct 2026 13:20:28 -0400 Subject: [PATCH 2/4] chore: import deprecate after the other imports Keeps this change clear of the line the private-key check edits, so the two merge in either order. Co-Authored-By: Claude Opus 5.5 --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index d894f240..1873091f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,8 @@ import { X509Certificate } from "crypto"; -import { deprecate } from "util"; 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; From 2929ab909a9b798cc68859cf7b2d1380beb52180 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Tue, 13 Oct 2026 14:30:01 -0400 Subject: [PATCH 3/4] chore: keep a nested process() call from clearing the outer call's state A subclass that called process() from inside process() left the instance looking as if it were outside process() once the inner call returned, so the outer call's next processInner() printed the deprecation warning for a caller that used neither deprecated method. Co-Authored-By: Claude Opus 5.5 --- src/utils.ts | 7 ++++++- test/c14n-non-exclusive-unit-tests.spec.ts | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 1873091f..f6acdcdb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -638,11 +638,16 @@ export function insideProcess(canonicalization: object, ownMethods: object, r 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 { - canonicalizationsInProcess.delete(canonicalization); + if (isOutermost) { + canonicalizationsInProcess.delete(canonicalization); + } } } diff --git a/test/c14n-non-exclusive-unit-tests.spec.ts b/test/c14n-non-exclusive-unit-tests.spec.ts index 9e438100..5e75a54d 100644 --- a/test/c14n-non-exclusive-unit-tests.spec.ts +++ b/test/c14n-non-exclusive-unit-tests.spec.ts @@ -536,4 +536,20 @@ describe("C14nCanonicalization renderNs() and processInner(), public until 7.0", "text", ); }); + + it("canonicalizes when a subclass calls process() again from inside it", function () { + const root = parseRoot(); + class Reentrant extends C14nCanonicalization { + renderAttrs(node: Element) { + if (node === root && isDomNode.isElementNode(root.firstChild)) { + this.process(root.firstChild, {}); + } + return super.renderAttrs(node); + } + } + + expect(new Reentrant().process(root, {})).to.equal( + new C14nCanonicalization().process(root, {}), + ); + }); }); From 58a6799dd3a8331d57aba4e333acc1233d8a70c0 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Tue, 13 Oct 2026 17:20:53 -0400 Subject: [PATCH 4/4] chore: drop the tests of direct calls to and overrides of the deprecated methods They pinned behavior this change leaves alone, and ran the warning's branches without asserting on them. Co-Authored-By: Claude Opus 5.5 --- test/c14n-non-exclusive-unit-tests.spec.ts | 50 ---------------------- test/canonicalization-unit-tests.spec.ts | 34 --------------- 2 files changed, 84 deletions(-) diff --git a/test/c14n-non-exclusive-unit-tests.spec.ts b/test/c14n-non-exclusive-unit-tests.spec.ts index 5e75a54d..447a69ae 100644 --- a/test/c14n-non-exclusive-unit-tests.spec.ts +++ b/test/c14n-non-exclusive-unit-tests.spec.ts @@ -503,53 +503,3 @@ describe("C14N non-exclusive canonicalization tests", function () { }); } }); - -describe("C14nCanonicalization renderNs() and processInner(), public until 7.0", function () { - function parseRoot() { - const doc = new xmldom.DOMParser().parseFromString( - 'text', - "text/xml", - ); - const root = xpath.select1("/*", doc); - isDomNode.assertIsElementNode(root); - return root; - } - - it("canonicalizes through processInner() called directly", function () { - const canonicalization = new C14nCanonicalization(); - const root = parseRoot(); - - /* eslint-disable-next-line deprecation/deprecation */ - expect(canonicalization.processInner(root, [], "", {}, [])).to.equal( - canonicalization.process(root, {}), - ); - }); - - it("uses an override of renderNs()", function () { - class WithoutNamespaces extends C14nCanonicalization { - renderNs() { - return { rendered: "", newDefaultNs: "" }; - } - } - - expect(new WithoutNamespaces().process(parseRoot(), {})).to.equal( - "text", - ); - }); - - it("canonicalizes when a subclass calls process() again from inside it", function () { - const root = parseRoot(); - class Reentrant extends C14nCanonicalization { - renderAttrs(node: Element) { - if (node === root && isDomNode.isElementNode(root.firstChild)) { - this.process(root.firstChild, {}); - } - return super.renderAttrs(node); - } - } - - expect(new Reentrant().process(root, {})).to.equal( - new C14nCanonicalization().process(root, {}), - ); - }); -}); diff --git a/test/canonicalization-unit-tests.spec.ts b/test/canonicalization-unit-tests.spec.ts index 2e9b15e3..81535257 100644 --- a/test/canonicalization-unit-tests.spec.ts +++ b/test/canonicalization-unit-tests.spec.ts @@ -607,37 +607,3 @@ describe("Canonicalization unit tests", function () { ); }); }); - -describe("ExclusiveCanonicalization renderNs() and processInner(), public until 7.0", function () { - function parseRoot() { - const doc = new xmldom.DOMParser().parseFromString( - 'text', - "text/xml", - ); - const root = xpath.select1("/*", doc); - isDomNode.assertIsElementNode(root); - return root; - } - - it("canonicalizes through processInner() called directly", function () { - const canonicalization = new ExclusiveCanonicalization(); - const root = parseRoot(); - - /* eslint-disable-next-line deprecation/deprecation */ - expect(canonicalization.processInner(root, [], "", {}, [])).to.equal( - canonicalization.process(root, {}), - ); - }); - - it("uses an override of renderNs()", function () { - class WithoutNamespaces extends ExclusiveCanonicalization { - renderNs() { - return { rendered: "", newDefaultNs: "" }; - } - } - - expect(new WithoutNamespaces().process(parseRoot(), {})).to.equal( - "text", - ); - }); -});