diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 05dae41..31c3472 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -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. */ @@ -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; @@ -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; @@ -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); } } diff --git a/src/types.ts b/src/types.ts index f102c4c..6145a0a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,6 +58,15 @@ export interface SignedXmlOptions { keyInfoAttributes?: Record; 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 { diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index baa382d..b6ebe8c 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -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 = ""; + 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) {