Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<KeyInfo>`, 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 `<KeyInfo>`.
- `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"`

Expand Down
16 changes: 15 additions & 1 deletion src/signed-xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ function findSignatureElements(node: Node): Element[] {
return signatures.filter(isDomNode.isElementNode);
}

function certificatesToPublish(publicCert: string): string[] {
const certificates = utils.pemCertificates(publicCert);
Comment thread
cjbarth marked this conversation as resolved.
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()`.",
Expand Down Expand Up @@ -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) {
Expand Down
13 changes: 13 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 32 additions & 2 deletions test/signature-unit-tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Loading