diff --git a/README.md b/README.md index 96aea9b0..7909aeec 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, 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..79ef66dd 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -38,6 +38,20 @@ 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; + } + + try { + return [utils.bareCertificate(publicCert)]; + } catch { + // Not a certificate in either form, so there is none to publish, and KeyInfo is optional. + 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 +243,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/src/utils.ts b/src/utils.ts index 64412736..f449a7c8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -408,6 +408,19 @@ function pemText(value: string | Buffer): string { : value.toString("base64"); } +// 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."); + } + 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 diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index c6fd483a..7e51e477 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -1442,6 +1442,27 @@ 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; + }); + + 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 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; + }); }); describe("getCertFromKeyInfo", function () { @@ -1504,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, @@ -1525,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); @@ -1564,6 +1585,15 @@ 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("")]); + 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 () { // 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