Skip to content

feat: Support formatted SignatureValue and lenient verification #507

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
38 changes: 36 additions & 2 deletions src/signed-xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ export class SignedXml {
* @see {@link SignatureAlgorithmType}
*/
signatureAlgorithm?: SignatureAlgorithmType = undefined;
/**
* Controls formatting of the SignatureValue output.
* - lineLength: number of characters per line (default 76)
* - carriageReturn: if true, use \r\n line endings (default false)
*/
signatureValueFormatting?: {
lineLength?: number;
carriageReturn?: boolean;
};
/**
* Rules used to convert an XML document into its canonical form.
*/
Expand All @@ -63,6 +72,14 @@ export class SignedXml {
private signatureXml = "";
private signatureNode: Node | null = null;
private signatureValue = "";

/**
* Returns the computed SignatureValue as a string.
* Useful for testing and inspection.
*/
public getSignatureValue(): string {
return this.signatureValue;
}
private originalXmlWithIds = "";
private keyInfo: Node | null = null;

Expand Down Expand Up @@ -164,6 +181,7 @@ export class SignedXml {
this.keyInfoAttributes = keyInfoAttributes ?? this.keyInfoAttributes;
this.getKeyInfoContent = getKeyInfoContent ?? this.getKeyInfoContent;
this.getCertFromKeyInfo = getCertFromKeyInfo ?? SignedXml.noop;
this.signatureValueFormatting = options.signatureValueFormatting;
this.CanonicalizationAlgorithms;
this.HashAlgorithms;
this.SignatureAlgorithms;
Expand Down Expand Up @@ -444,10 +462,26 @@ export class SignedXml {
if (this.privateKey == null) {
throw new Error("Private key is required to compute signature");
}

const formatSignature = (rawSignature: string): string => {
if (!this.signatureValueFormatting) {
return rawSignature;
}
const { lineLength = 76, carriageReturn = false } = this.signatureValueFormatting;
const newline = carriageReturn ? "\r\n" : "\n";
return (rawSignature.match(new RegExp(`.{1,${lineLength}}`, "g")) || []).join(newline);
};
if (typeof callback === "function") {
signer.getSignature(signedInfoCanon, this.privateKey, callback);
signer.getSignature(signedInfoCanon, this.privateKey, (err, signature) => {
if (err) {
return callback(err);
}
this.signatureValue = formatSignature(signature || "");
callback(null, this.signatureValue);
});
} else {
this.signatureValue = signer.getSignature(signedInfoCanon, this.privateKey);
const signature = signer.getSignature(signedInfoCanon, this.privateKey);
this.signatureValue = formatSignature(signature);
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ export interface SignedXmlOptions {
keyInfoAttributes?: Record<string, string>;
getKeyInfoContent?(args?: GetKeyInfoContentArgs): string | null;
getCertFromKeyInfo?(keyInfo?: Node | null): string | null;
/**
* Controls formatting of the SignatureValue output.
* - lineLength: number of characters per line (default 76)
* - carriageReturn: if true, use \r\n line endings (default false)
*/
signatureValueFormatting?: {
lineLength?: number;
carriageReturn?: boolean;
};
}

export interface NamespacePrefix {
Expand Down
40 changes: 40 additions & 0 deletions test/signature-unit-tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,46 @@ import { expect } from "chai";
import * as isDomNode from "@xmldom/is-dom-node";

describe("Signature unit tests", function () {
it("should format the signature value with line breaks and carriage returns", function () {
const xml = "<root><x /></root>";
const sig = new SignedXml({
signatureValueFormatting: { lineLength: 76, carriageReturn: true },
});
sig.privateKey = fs.readFileSync("./test/static/client.pem");
sig.addReference({
xpath: "//*[local-name(.)='x']",
digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
});
sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
sig.computeSignature(xml);

// Check the internal signatureValue for \r\n
expect(sig.getSignatureValue()).to.include("\r\n");

// Check for the presence of any line break in the SignatureValue text content in the XML
const signatureXml = sig.getSignatureXml();
const signatureDoc = new xmldom.DOMParser().parseFromString(signatureXml);
const signatureValueNode = xpath.select1("//*[local-name(.)='SignatureValue']", signatureDoc);
expect(signatureValueNode, "SignatureValue node should be found").to.not.be.null;
const textContent = (signatureValueNode as Node).textContent || "";
expect(textContent).to.match(/\r?\n/, "SignatureValue in XML should contain a line break");

// Verify that the signature is still valid
const verifier = new SignedXml();
verifier.publicCert = fs.readFileSync("./test/static/client_public.pem");

// Load the signature from the generated XML
const signedXml = sig.getSignedXml();
const doc = new xmldom.DOMParser().parseFromString(signedXml);
const signatureNode = xpath.select1("//*[local-name(.)='Signature']", doc);
expect(signatureNode, "Signature node should be found").to.not.be.null;
verifier.loadSignature(signatureNode as Node);

const isValid = verifier.checkSignature(signedXml);
expect(isValid, "Formatted signature should be valid").to.be.true;
});
describe("verify adds ID", function () {
function nodeExists(doc, xpathArg) {
if (!doc && !xpathArg) {
Expand Down