From 02aea0d6e55800fea0e7bc7efa40ebcd9a6aed56 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Sat, 19 Sep 2026 09:49:51 -0500 Subject: [PATCH 1/8] fix: publish a signing publicCert given as the base64 of a certificate toPem(value, "CERTIFICATE") and getCertFromKeyInfo() read a certificate given as bare base64, which is how X509Certificate carries one, but the default getKeyInfoContent looked only for PEM CERTIFICATE messages and signed without KeyInfo. Read that form too. A value that is not a certificate in either form still signs without KeyInfo. The README described the signing publicCert as a public key, which produces no KeyInfo. It now says it is a certificate or a chain of them. Co-Authored-By: Claude Opus 5 --- README.md | 2 +- src/signed-xml.ts | 17 ++++++++++++++++- test/signature-unit-tests.spec.ts | 25 +++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e6650ef4..aa68222c 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ signature algorithms enabled at same time. When signing a xml document you can pass the following options to the `SignedXml` constructor to customize the signature process: - `privateKey` - **[required]** a `Buffer` or pem encoded `String` containing your private key -- `publicCert` - **[optional]** a `Buffer` or pem encoded `String` containing your public key +- `publicCert` - **[optional]** the X.509 certificate to publish in ``, or a chain of them with the signing certificate first, as PEM or as one certificate's base64 without the PEM boundaries, in a `String` or `Buffer`. A value that holds no certificate, such as a public key, produces no ``. - `signatureAlgorithm` - **[required]** one of the supported [signature algorithms](#signature-algorithms). Ex: `sign.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"` - `canonicalizationAlgorithm` - **[required]** one of the supported [canonicalization algorithms](#canonicalization-and-transformation-algorithms). Ex: `sign.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"` diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 66902e71..0cd63d54 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -38,6 +38,21 @@ function findSignatureElements(node: Node): Element[] { return signatures.filter(isDomNode.isElementNode); } +function certificatesToPublish(publicCert: string): string[] { + const certificates = utils.pemCertificates(publicCert); + if (certificates.length > 0) { + return certificates; + } + + // X509Certificate carries a certificate as bare base64, and getCertFromKeyInfo() reads it so. + try { + return utils.pemCertificates(utils.toPem(publicCert, "CERTIFICATE")); + } catch { + // Throwing would fail configurations that sign today, so that waits for 7.0 (#598). + return []; + } +} + 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()`.", @@ -229,7 +244,7 @@ export class SignedXml { } // A KeyObject holds a key and never a certificate, so there is no X509Data to build from it. - const certificates = typeof publicCert === "string" ? utils.pemCertificates(publicCert) : []; + const certificates = typeof publicCert === "string" ? certificatesToPublish(publicCert) : []; // X509Data requires at least one child: https://www.w3.org/TR/xmldsig-core1/#sec-X509Data if (certificates.length === 0) { diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index 4367660b..5df6a1d3 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -1442,6 +1442,23 @@ describe("Signature unit tests", function () { it("when publicCert is a KeyObject, which holds a key and never a certificate", function () { expect(selectKeyInfo({ publicCert: crypto.createPublicKey(privateKey) })).to.be.empty; }); + + it("when publicCert is a private key", function () { + expect(selectKeyInfo({ publicCert: privateKey })).to.be.empty; + }); + + for (const [name, der] of [ + ["public key", crypto.createPublicKey(privateKey).export({ type: "spki", format: "der" })], + ["private key", crypto.createPrivateKey(privateKey).export({ type: "pkcs8", format: "der" })], + ] as const) { + it(`when publicCert is the base64 of a ${name}, without boundaries`, function () { + expect(selectKeyInfo({ publicCert: der.toString("base64") })).to.be.empty; + }); + } + + it("when publicCert is not a certificate in any form", function () { + expect(selectKeyInfo({ publicCert: "not a certificate" })).to.be.empty; + }); }); describe("getCertFromKeyInfo", function () { @@ -1564,6 +1581,14 @@ describe("Signature unit tests", function () { expect(signWithPublicCert(publicCert)).to.throw("Invalid PEM format."); }); + it("publishes a publicCert given as the base64 of a certificate, without boundaries", function () { + const lines = fs.readFileSync("./test/static/client_public.pem", "latin1").trim().split("\n"); + const data = lines.slice(1, -1); + + expect(publishedCertificates(data.join(""))).to.deep.equal([data.join("")]); + expect(publishedCertificates(data.join("\n"))).to.deep.equal([data.join("")]); + }); + it("signs a BER certificate into KeyInfo with its octets as given", function () { // RFC 7468 section 5.1 allows BER, and XML Signature 1.1 says an implementation SHOULD NOT // re-encode a certificate: https://www.w3.org/TR/xmldsig-core1/#sec-X509Data From 4bf16a04c8f517eea5d8c4422ee5715ccca01adb Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Sat, 19 Sep 2026 09:52:36 -0500 Subject: [PATCH 2/8] fix: warn once when a signing publicCert holds no certificate Signing with the default getKeyInfoContent and a string or Buffer publicCert that holds no certificate omits KeyInfo without saying so. Emit a process warning, once per process, naming the fix. A KeyObject, a custom getKeyInfoContent and an unset publicCert stay silent. 7.0 makes this an error (#598). Kept in its own commit so that review can drop it. Co-Authored-By: Claude Opus 5 --- src/signed-xml.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 0cd63d54..2bf28003 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -53,6 +53,20 @@ function certificatesToPublish(publicCert: string): string[] { } } +let warnedPublicCertWithoutCertificate = false; + +function warnPublicCertWithoutCertificate() { + if (warnedPublicCertWithoutCertificate) { + return; + } + warnedPublicCertWithoutCertificate = true; + + process.emitWarning( + "`publicCert` holds no X.509 certificate, so the signature has no `KeyInfo`. This will be an error in 7.0. Set `publicCert` to a certificate, or leave it unset.", + { code: "XML_CRYPTO_PUBLIC_CERT_WITHOUT_CERTIFICATE" }, + ); +} + 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()`.", @@ -1299,6 +1313,14 @@ export class SignedXml { const keyInfoContent = this.getKeyInfoContent({ publicCert: this.publicCert, prefix }); // KeyInfo requires at least one child: https://www.w3.org/TR/xmldsig-core1/#sec-KeyInfo if (!keyInfoContent) { + // A custom getKeyInfoContent may not use publicCert, and a KeyObject cannot hold a certificate. + if ( + this.getKeyInfoContent === SignedXml.getKeyInfoContent && + (typeof this.publicCert === "string" || Buffer.isBuffer(this.publicCert)) + ) { + warnPublicCertWithoutCertificate(); + } + return ""; } From 30c4d7a1638b4096683eb30fa799af090f063cf0 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Sat, 19 Sep 2026 10:35:18 -0500 Subject: [PATCH 3/8] fix: read bare base64 from a string publicCert only, and imply no chain order A Buffer of base64 text had started publishing its certificate, which the issue did not ask for; toPem() documents base64 as a string, so a Buffer is read for PEM alone, as before. The README no longer asks for the signing certificate first: XMLDSig 4.5.4 says no ordering is implied among an X509Data's certificates. Co-Authored-By: Claude Opus 5 --- README.md | 2 +- src/signed-xml.ts | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index aa68222c..e2181626 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ signature algorithms enabled at same time. When signing a xml document you can pass the following options to the `SignedXml` constructor to customize the signature process: - `privateKey` - **[required]** a `Buffer` or pem encoded `String` containing your private key -- `publicCert` - **[optional]** the X.509 certificate to publish in ``, or a chain of them with the signing certificate first, as PEM or as one certificate's base64 without the PEM boundaries, in a `String` or `Buffer`. A value that holds no certificate, such as a public key, produces no ``. +- `publicCert` - **[optional]** the X.509 certificate to publish in ``, or a chain of them, as a PEM `String` or `Buffer`, or as a `String` of one certificate's base64 without the PEM boundaries. A value that holds no certificate, such as a public key, produces no ``. - `signatureAlgorithm` - **[required]** one of the supported [signature algorithms](#signature-algorithms). Ex: `sign.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"` - `canonicalizationAlgorithm` - **[required]** one of the supported [canonicalization algorithms](#canonicalization-and-transformation-algorithms). Ex: `sign.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"` diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 2bf28003..57dcefab 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -38,7 +38,17 @@ function findSignatureElements(node: Node): Element[] { return signatures.filter(isDomNode.isElementNode); } -function certificatesToPublish(publicCert: string): string[] { +function certificatesToPublish(publicCert: crypto.KeyLike): string[] { + // Base64 text is given as a string, as toPem() documents, so a Buffer is read for PEM alone. + if (Buffer.isBuffer(publicCert)) { + return utils.pemCertificates(publicCert.toString("latin1")); + } + + // A KeyObject holds a key and never a certificate, so there is no X509Data to build from it. + if (typeof publicCert !== "string") { + return []; + } + const certificates = utils.pemCertificates(publicCert); if (certificates.length > 0) { return certificates; @@ -253,12 +263,7 @@ export class SignedXml { prefix = prefix ? `${prefix}:` : ""; - if (Buffer.isBuffer(publicCert)) { - publicCert = publicCert.toString("latin1"); - } - - // A KeyObject holds a key and never a certificate, so there is no X509Data to build from it. - const certificates = typeof publicCert === "string" ? certificatesToPublish(publicCert) : []; + const certificates = certificatesToPublish(publicCert); // X509Data requires at least one child: https://www.w3.org/TR/xmldsig-core1/#sec-X509Data if (certificates.length === 0) { From 6d07f59f49c2c951ea54c01601889f32af271ceb Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Sat, 19 Sep 2026 10:59:56 -0500 Subject: [PATCH 4/8] fix: drop the warning for a signing publicCert with no certificate KeyInfo is optional (XMLDSig 4.5), so signing without one is valid output, and making it an error was declined as adding nothing to security or function. A warning for something that should not throw is code to maintain for no gain. Co-Authored-By: Claude Opus 5 --- src/signed-xml.ts | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 57dcefab..d4cc19bd 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -58,25 +58,11 @@ function certificatesToPublish(publicCert: crypto.KeyLike): string[] { try { return utils.pemCertificates(utils.toPem(publicCert, "CERTIFICATE")); } catch { - // Throwing would fail configurations that sign today, so that waits for 7.0 (#598). + // Not a certificate in either form, so there is none to publish, and KeyInfo is optional. return []; } } -let warnedPublicCertWithoutCertificate = false; - -function warnPublicCertWithoutCertificate() { - if (warnedPublicCertWithoutCertificate) { - return; - } - warnedPublicCertWithoutCertificate = true; - - process.emitWarning( - "`publicCert` holds no X.509 certificate, so the signature has no `KeyInfo`. This will be an error in 7.0. Set `publicCert` to a certificate, or leave it unset.", - { code: "XML_CRYPTO_PUBLIC_CERT_WITHOUT_CERTIFICATE" }, - ); -} - 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()`.", @@ -1318,14 +1304,6 @@ export class SignedXml { const keyInfoContent = this.getKeyInfoContent({ publicCert: this.publicCert, prefix }); // KeyInfo requires at least one child: https://www.w3.org/TR/xmldsig-core1/#sec-KeyInfo if (!keyInfoContent) { - // A custom getKeyInfoContent may not use publicCert, and a KeyObject cannot hold a certificate. - if ( - this.getKeyInfoContent === SignedXml.getKeyInfoContent && - (typeof this.publicCert === "string" || Buffer.isBuffer(this.publicCert)) - ) { - warnPublicCertWithoutCertificate(); - } - return ""; } From 5d0feceab0858622d4c304b1ee2c56bbc35fc621 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Sat, 19 Sep 2026 11:15:40 -0500 Subject: [PATCH 5/8] fix: read a bare-base64 publicCert as one certificate, never as PEM The fallback wrapped the value with toPem() and unwrapped it with pemCertificates(). That parsed the certificate twice, and it was safe only because the second pass filtered out the PEM keys toPem() passes through unchanged. bareCertificate() refuses PEM outright, so no key can come out as a certificate, and the certificate is parsed once. toPem() shares its base64 check, so both read bare base64 alike. Co-Authored-By: Claude Opus 5 --- src/signed-xml.ts | 3 +-- src/utils.ts | 41 ++++++++++++++++++++++++++++------------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/signed-xml.ts b/src/signed-xml.ts index d4cc19bd..4255d10e 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -54,9 +54,8 @@ function certificatesToPublish(publicCert: crypto.KeyLike): string[] { return certificates; } - // X509Certificate carries a certificate as bare base64, and getCertFromKeyInfo() reads it so. try { - return utils.pemCertificates(utils.toPem(publicCert, "CERTIFICATE")); + return [utils.bareCertificate(publicCert)]; } catch { // Not a certificate in either form, so there is none to publish, and KeyInfo is optional. return []; diff --git a/src/utils.ts b/src/utils.ts index 64412736..a6384158 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -408,6 +408,25 @@ function pemText(value: string | Buffer): string { : value.toString("base64"); } +function bareBase64Data(text: string): string { + const data = text.replace(/\n/g, ""); + + if (!BASE64_TEXT_REGEX.test(text) || !isBase64Data(data)) { + throw new Error("Invalid PEM format."); + } + + return data; +} + +// The canonical base64 of one certificate given without boundaries, as XMLDSig's X509Certificate +// carries it. PEM is refused here rather than passed through, so no key can come out as one. +export function bareCertificate(value: string): string { + const data = bareBase64Data(normalizePemInput(value)); + assertX509Certificate(data); + + return canonicalBase64(data); +} + /** * Returns a value as canonical PEM: one message per certificate or key, wrapped at 64 characters. * The value may be a PEM message, several of them, base64 data with the label supplied by the @@ -433,23 +452,19 @@ export function toPem(value: string | Buffer, pemLabel?: PemLabel): string { return messages.map((message) => formatPemMessage(message.label, message.data)).join(""); } - const data = text.replace(/\n/g, ""); + const data = bareBase64Data(text); - if (BASE64_TEXT_REGEX.test(text) && isBase64Data(data)) { - if (pemLabel == null) { - throw new Error("A PEM label is required to wrap base64 data."); - } - - // The label is written into both boundaries, so one that is not a label would produce a - // message this parser could not read back, and `-----` in it would produce a second message. - if (!isLabel(pemLabel)) { - throw new Error("Invalid PEM label."); - } + if (pemLabel == null) { + throw new Error("A PEM label is required to wrap base64 data."); + } - return formatPemMessage(pemLabel, data); + // The label is written into both boundaries, so one that is not a label would produce a + // message this parser could not read back, and `-----` in it would produce a second message. + if (!isLabel(pemLabel)) { + throw new Error("Invalid PEM label."); } - throw new Error("Invalid PEM format."); + return formatPemMessage(pemLabel, data); } function collectAncestorNamespaces( From b3f767095b77a2b300aca7a79160b0c199baec5f Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Sat, 19 Sep 2026 11:31:43 -0500 Subject: [PATCH 6/8] fix: leave toPem() untouched and drop a duplicate test bareCertificate() applies the base64 predicates itself, so toPem() keeps its shape. The bare public-key case was refused by the same certificate check as the bare private-key case, which is the one that matters. Co-Authored-By: Claude Opus 5 --- src/utils.ts | 34 +++++++++++++++---------------- test/signature-unit-tests.spec.ts | 13 +++++------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index a6384158..f449a7c8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -408,20 +408,14 @@ function pemText(value: string | Buffer): string { : value.toString("base64"); } -function bareBase64Data(text: string): string { +// Refuses PEM rather than passing it through as toPem() does, so no key can come out as a certificate. +export function bareCertificate(value: string): string { + const text = normalizePemInput(value); const data = text.replace(/\n/g, ""); if (!BASE64_TEXT_REGEX.test(text) || !isBase64Data(data)) { throw new Error("Invalid PEM format."); } - - return data; -} - -// The canonical base64 of one certificate given without boundaries, as XMLDSig's X509Certificate -// carries it. PEM is refused here rather than passed through, so no key can come out as one. -export function bareCertificate(value: string): string { - const data = bareBase64Data(normalizePemInput(value)); assertX509Certificate(data); return canonicalBase64(data); @@ -452,19 +446,23 @@ export function toPem(value: string | Buffer, pemLabel?: PemLabel): string { return messages.map((message) => formatPemMessage(message.label, message.data)).join(""); } - const data = bareBase64Data(text); + const data = text.replace(/\n/g, ""); - if (pemLabel == null) { - throw new Error("A PEM label is required to wrap base64 data."); - } + if (BASE64_TEXT_REGEX.test(text) && isBase64Data(data)) { + if (pemLabel == null) { + throw new Error("A PEM label is required to wrap base64 data."); + } + + // The label is written into both boundaries, so one that is not a label would produce a + // message this parser could not read back, and `-----` in it would produce a second message. + if (!isLabel(pemLabel)) { + throw new Error("Invalid PEM label."); + } - // The label is written into both boundaries, so one that is not a label would produce a - // message this parser could not read back, and `-----` in it would produce a second message. - if (!isLabel(pemLabel)) { - throw new Error("Invalid PEM label."); + return formatPemMessage(pemLabel, data); } - return formatPemMessage(pemLabel, data); + throw new Error("Invalid PEM format."); } function collectAncestorNamespaces( diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index 5df6a1d3..0a35ae16 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -1447,14 +1447,11 @@ describe("Signature unit tests", function () { expect(selectKeyInfo({ publicCert: privateKey })).to.be.empty; }); - for (const [name, der] of [ - ["public key", crypto.createPublicKey(privateKey).export({ type: "spki", format: "der" })], - ["private key", crypto.createPrivateKey(privateKey).export({ type: "pkcs8", format: "der" })], - ] as const) { - it(`when publicCert is the base64 of a ${name}, without boundaries`, function () { - expect(selectKeyInfo({ publicCert: der.toString("base64") })).to.be.empty; - }); - } + it("when publicCert is the base64 of a private key, without boundaries", function () { + const der = crypto.createPrivateKey(privateKey).export({ type: "pkcs8", format: "der" }); + + expect(selectKeyInfo({ publicCert: der.toString("base64") })).to.be.empty; + }); it("when publicCert is not a certificate in any form", function () { expect(selectKeyInfo({ publicCert: "not a certificate" })).to.be.empty; From cb283ff6562b90ce1fbac898d9ae7ee1ce4cfb9d Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Sat, 19 Sep 2026 11:40:24 -0500 Subject: [PATCH 7/8] test: refuse to publish a bare certificate with more data after it Co-Authored-By: Claude Opus 5 --- test/signature-unit-tests.spec.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index 0a35ae16..dcda2878 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -1453,6 +1453,13 @@ describe("Signature unit tests", function () { expect(selectKeyInfo({ publicCert: der.toString("base64") })).to.be.empty; }); + it("when publicCert is the base64 of a certificate with more data after it", function () { + const der = fs.readFileSync("./test/static/client_public.der"); + const publicCert = Buffer.concat([der, Buffer.from("more")]).toString("base64"); + + expect(selectKeyInfo({ publicCert })).to.be.empty; + }); + it("when publicCert is not a certificate in any form", function () { expect(selectKeyInfo({ publicCert: "not a certificate" })).to.be.empty; }); From d9f56633c84b310958a3b9db1e5256f623dca2da Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Sat, 19 Sep 2026 11:55:18 -0500 Subject: [PATCH 8/8] fix: read a Buffer publicCert as text, as before, for bare base64 too Carving a Buffer out of the bare-base64 path took a branch, a comment and a README qualifier to forbid something harmless that no spec asks to forbid. getKeyInfoContent already read a Buffer as text, as Node's crypto reads a Buffer key, so it now reaches the same reader as a string. Co-Authored-By: Claude Opus 5 --- README.md | 2 +- src/signed-xml.ts | 19 +++++++------------ test/signature-unit-tests.spec.ts | 5 +++-- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e2181626..1f51fe84 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ signature algorithms enabled at same time. When signing a xml document you can pass the following options to the `SignedXml` constructor to customize the signature process: - `privateKey` - **[required]** a `Buffer` or pem encoded `String` containing your private key -- `publicCert` - **[optional]** the X.509 certificate to publish in ``, or a chain of them, as a PEM `String` or `Buffer`, or as a `String` of one certificate's base64 without the PEM boundaries. A value that holds no certificate, such as a public key, produces no ``. +- `publicCert` - **[optional]** the X.509 certificate to publish in ``, or a chain of them, as PEM or as one certificate's base64 without the PEM boundaries, in a `String` or `Buffer`. A value that holds no certificate, such as a public key, produces no ``. - `signatureAlgorithm` - **[required]** one of the supported [signature algorithms](#signature-algorithms). Ex: `sign.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"` - `canonicalizationAlgorithm` - **[required]** one of the supported [canonicalization algorithms](#canonicalization-and-transformation-algorithms). Ex: `sign.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"` diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 4255d10e..79ef66dd 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -38,17 +38,7 @@ function findSignatureElements(node: Node): Element[] { return signatures.filter(isDomNode.isElementNode); } -function certificatesToPublish(publicCert: crypto.KeyLike): string[] { - // Base64 text is given as a string, as toPem() documents, so a Buffer is read for PEM alone. - if (Buffer.isBuffer(publicCert)) { - return utils.pemCertificates(publicCert.toString("latin1")); - } - - // A KeyObject holds a key and never a certificate, so there is no X509Data to build from it. - if (typeof publicCert !== "string") { - return []; - } - +function certificatesToPublish(publicCert: string): string[] { const certificates = utils.pemCertificates(publicCert); if (certificates.length > 0) { return certificates; @@ -248,7 +238,12 @@ export class SignedXml { prefix = prefix ? `${prefix}:` : ""; - const certificates = certificatesToPublish(publicCert); + if (Buffer.isBuffer(publicCert)) { + publicCert = publicCert.toString("latin1"); + } + + // A KeyObject holds a key and never a certificate, so there is no X509Data to build from it. + const certificates = typeof publicCert === "string" ? certificatesToPublish(publicCert) : []; // X509Data requires at least one child: https://www.w3.org/TR/xmldsig-core1/#sec-X509Data if (certificates.length === 0) { diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index dcda2878..3b4f101c 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -1525,7 +1525,7 @@ describe("Signature unit tests", function () { }); }); - function signWithPublicCert(publicCert: string) { + function signWithPublicCert(publicCert: string | Buffer) { const sig = new SignedXml({ privateKey: fs.readFileSync("./test/static/client.pem"), publicCert, @@ -1546,7 +1546,7 @@ describe("Signature unit tests", function () { } // The text of each X509Certificate that signing with this publicCert puts into KeyInfo. - function publishedCertificates(publicCert: string): string[] { + function publishedCertificates(publicCert: string | Buffer): string[] { const doc = new xmldom.DOMParser().parseFromString(signWithPublicCert(publicCert)()); const certificates = xpath.select("//*[local-name(.)='X509Certificate']", doc); isDomNode.assertIsArrayOfNodes(certificates); @@ -1591,6 +1591,7 @@ describe("Signature unit tests", function () { expect(publishedCertificates(data.join(""))).to.deep.equal([data.join("")]); expect(publishedCertificates(data.join("\n"))).to.deep.equal([data.join("")]); + expect(publishedCertificates(Buffer.from(data.join("\n")))).to.deep.equal([data.join("")]); }); it("signs a BER certificate into KeyInfo with its octets as given", function () {