From b10a651f4729702d6370c12641d96c809db075de Mon Sep 17 00:00:00 2001 From: shunkica Date: Fri, 20 Jun 2025 19:30:22 +0200 Subject: [PATCH 01/17] feat: add support for inserting and signing Object elements inside the Signature --- src/signed-xml.ts | 166 +++++++ src/types.ts | 19 + test/signature-object-tests.spec.ts | 681 ++++++++++++++++++++++++++++ 3 files changed, 866 insertions(+) create mode 100644 test/signature-object-tests.spec.ts diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 05dae41..afe8bef 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -56,6 +56,7 @@ export class SignedXml { keyInfoAttributes: { [attrName: string]: string } = {}; getKeyInfoContent = SignedXml.getKeyInfoContent; getCertFromKeyInfo = SignedXml.getCertFromKeyInfo; + getObjectContent = SignedXml.getObjectContent; // Internal state private id = 0; @@ -126,6 +127,16 @@ export class SignedXml { static noop = () => null; + /** + * Default implementation for getObjectContent that returns null (no Objects) + */ + static getObjectContent(): Array<{ + content: string; + attributes?: Record; + }> | null { + return null; + } + /** * The SignedXml constructor provides an abstraction for sign and verify xml documents. The object is constructed using * @param options {@link SignedXmlOptions} @@ -143,6 +154,7 @@ export class SignedXml { keyInfoAttributes, getKeyInfoContent, getCertFromKeyInfo, + getObjectContent, } = options; // Options @@ -164,6 +176,7 @@ export class SignedXml { this.keyInfoAttributes = keyInfoAttributes ?? this.keyInfoAttributes; this.getKeyInfoContent = getKeyInfoContent ?? this.getKeyInfoContent; this.getCertFromKeyInfo = getCertFromKeyInfo ?? SignedXml.noop; + this.getObjectContent = getObjectContent ?? this.getObjectContent; this.CanonicalizationAlgorithms; this.HashAlgorithms; this.SignatureAlgorithms; @@ -796,6 +809,7 @@ export class SignedXml { * @param digestValue The expected digest value for the reference. * @param inclusiveNamespacesPrefixList The prefix list for inclusive namespace canonicalization. * @param isEmptyUri Indicates whether the URI is empty. Defaults to `false`. + * @param isSignatureReference Indicates whether this reference points to an element in the signature itself (like an Object element). */ addReference({ xpath, @@ -805,6 +819,7 @@ export class SignedXml { digestValue, inclusiveNamespacesPrefixList = [], isEmptyUri = false, + isSignatureReference = false, }: Partial & Pick): void { if (digestAlgorithm == null) { throw new Error("digestAlgorithm is required"); @@ -822,6 +837,7 @@ export class SignedXml { digestValue, inclusiveNamespacesPrefixList, isEmptyUri, + isSignatureReference, getValidatedNode: () => { throw new Error( "Reference has not been validated yet; Did you call `sig.checkSignature()`?", @@ -965,6 +981,7 @@ export class SignedXml { signatureXml += this.createSignedInfo(doc, prefix); signatureXml += this.getKeyInfo(prefix); + signatureXml += this.getObjects(prefix); signatureXml += ``; this.originalXmlWithIds = doc.toString(); @@ -979,6 +996,9 @@ export class SignedXml { const dummySignatureWrapper = `${signatureXml}`; const nodeXml = new xmldom.DOMParser().parseFromString(dummySignatureWrapper); + // Process any signature references after the signature has been created + this.processSignatureReferences(nodeXml, prefix); + // Because we are using a dummy wrapper hack described above, we know there will be a `firstChild` // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const signatureDoc = nodeXml.documentElement.firstChild!; @@ -1070,6 +1090,39 @@ export class SignedXml { return ""; } + /** + * Creates XML for Object elements to be included in the signature + * + * @param prefix Optional namespace prefix + * @returns XML string with Object elements or empty string if none + */ + private getObjects(prefix?: string) { + const currentPrefix = prefix ? `${prefix}:` : ""; + const objects = this.getObjectContent?.(); + + if (!objects || objects.length === 0) { + return ""; + } + + let result = ""; + + for (const obj of objects) { + let objectAttrs = ""; + if (obj.attributes) { + Object.keys(obj.attributes).forEach((name) => { + const value = obj.attributes?.[name]; + if (value !== undefined) { + objectAttrs += ` ${name}="${value}"`; + } + }); + } + + result += `<${currentPrefix}Object${objectAttrs}>${obj.content}`; + } + + return result; + } + /** * Generate the Reference nodes (as part of the signature process) * @@ -1082,6 +1135,10 @@ export class SignedXml { /* eslint-disable-next-line deprecation/deprecation */ for (const ref of this.getReferences()) { + if (ref.isSignatureReference) { + // For signature references, we'll handle them separately after the signature is created + continue; + } const nodes = xpath.selectWithResolver(ref.xpath ?? "", doc, this.namespaceResolver); if (!utils.isArrayHasLength(nodes)) { @@ -1262,6 +1319,115 @@ export class SignedXml { return doc.documentElement.firstChild!; } + /** + * Process references that point to elements within the Signature element + * This is called after the initial signature has been created + */ + private processSignatureReferences(signatureDoc: Document, prefix?: string) { + // Get signature references + const signatureReferences = this.references.filter((ref) => ref.isSignatureReference); + if (signatureReferences.length === 0) { + return; + } + + prefix = prefix || ""; + prefix = prefix ? `${prefix}:` : prefix; + const signatureNamespace = "http://www.w3.org/2000/09/xmldsig#"; + + // Find the SignedInfo element to append to + const signedInfoNode = xpath.select1( + `.//*[local-name(.)='SignedInfo']`, + signatureDoc, + ) as Element; + if (!signedInfoNode) { + throw new Error("Could not find SignedInfo element in signature"); + } + + // Process each signature reference + for (const ref of signatureReferences) { + const nodes = xpath.selectWithResolver(ref.xpath ?? "", signatureDoc, this.namespaceResolver); + + if (!utils.isArrayHasLength(nodes)) { + throw new Error( + `the following xpath cannot be signed because it was not found: ${ref.xpath}`, + ); + } + + // Process the reference + for (const node of nodes) { + // Create the reference element directly using DOM methods to avoid namespace issues + const referenceElem = signatureDoc.createElementNS( + signatureNamespace, + `${prefix}Reference`, + ); + if (ref.isEmptyUri) { + referenceElem.setAttribute("URI", ""); + } else { + const id = this.ensureHasId(node); + ref.uri = id; + referenceElem.setAttribute("URI", `#${id}`); + } + + const transformsElem = signatureDoc.createElementNS( + signatureNamespace, + `${prefix}Transforms`, + ); + + for (const trans of ref.transforms || []) { + const transform = this.findCanonicalizationAlgorithm(trans); + const transformElem = signatureDoc.createElementNS( + signatureNamespace, + `${prefix}Transform`, + ); + transformElem.setAttribute("Algorithm", transform.getAlgorithmName()); + + if (utils.isArrayHasLength(ref.inclusiveNamespacesPrefixList)) { + const inclusiveNamespacesElem = signatureDoc.createElementNS( + transform.getAlgorithmName(), + "InclusiveNamespaces", + ); + inclusiveNamespacesElem.setAttribute( + "PrefixList", + ref.inclusiveNamespacesPrefixList.join(" "), + ); + transformElem.appendChild(inclusiveNamespacesElem); + } + + transformsElem.appendChild(transformElem); + } + + // Get the canonicalized XML + const canonXml = this.getCanonReferenceXml(signatureDoc, ref, node); + + // Calculate the digest + const digestAlgorithm = this.findHashAlgorithm(ref.digestAlgorithm); + const digestValue = digestAlgorithm.getHash(canonXml); + + // Store the digest value for later validation + ref.digestValue = digestValue; + + const digestMethodElem = signatureDoc.createElementNS( + signatureNamespace, + `${prefix}DigestMethod`, + ); + digestMethodElem.setAttribute("Algorithm", digestAlgorithm.getAlgorithmName()); + + const digestValueElem = signatureDoc.createElementNS( + signatureNamespace, + `${prefix}DigestValue`, + ); + digestValueElem.textContent = digestValue; + + referenceElem.appendChild(transformsElem); + referenceElem.appendChild(digestMethodElem); + referenceElem.appendChild(digestValueElem); + + // Append the reference element to SignedInfo + signedInfoNode.appendChild(referenceElem); + } + } + } + /** * Returns just the signature part, must be called only after {@link computeSignature} * diff --git a/src/types.ts b/src/types.ts index f102c4c..a3eca60 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,6 +43,21 @@ export interface GetKeyInfoContentArgs { prefix?: string | null; } +/** + * Object attributes as defined in XMLDSig spec + * @see https://www.w3.org/TR/xmldsig-core/#sec-Object + */ +export interface ObjectAttributes { + /** Optional ID attribute */ + Id?: string; + /** Optional MIME type attribute */ + MimeType?: string; + /** Optional encoding attribute */ + Encoding?: string; + /** Any additional custom attributes */ + [key: string]: string | undefined; +} + /** * Options for the SignedXml constructor. */ @@ -58,6 +73,7 @@ export interface SignedXmlOptions { keyInfoAttributes?: Record; getKeyInfoContent?(args?: GetKeyInfoContentArgs): string | null; getCertFromKeyInfo?(keyInfo?: Node | null): string | null; + getObjectContent?(): Array<{ content: string; attributes?: ObjectAttributes }> | null; } export interface NamespacePrefix { @@ -127,6 +143,9 @@ export interface Reference { // Optional. Indicates whether the URI is empty. isEmptyUri: boolean; + // Optional. Indicates if this reference points to an element within the Signature + isSignatureReference?: boolean; + // Optional. The type of the reference node. ancestorNamespaces?: NamespacePrefix[]; diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts new file mode 100644 index 0000000..836b251 --- /dev/null +++ b/test/signature-object-tests.spec.ts @@ -0,0 +1,681 @@ +import * as fs from "fs"; +import { expect } from "chai"; +import * as xpath from "xpath"; +import * as xmldom from "@xmldom/xmldom"; +import * as isDomNode from "@xmldom/is-dom-node"; +import { SignedXml } from "../src/signed-xml"; + +describe("Object support in XML signatures", function () { + it("should add custom ds:Object elements to signature", function () { + // Create a simple XML document to sign + const xml = ''; + + // Create a SignedXml instance with custom getObjectContent + const sig = new SignedXml({ + getObjectContent: () => [ + { + content: "Test data in Object element", + attributes: { + Id: "object1", + MimeType: "text/xml", + }, + }, + { + content: "Plain text content", + attributes: { + Id: "object2", + MimeType: "text/plain", + }, + }, + ], + }); + + // Set up the signature + 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#"], + }); + + // Set required algorithms + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + + // Compute the signature + sig.computeSignature(xml); + + // Get the signed XML + const signedXml = sig.getSignedXml(); + + // Parse the signed XML + const doc = new xmldom.DOMParser().parseFromString(signedXml); + + // Verify that the ds:Object elements exist + const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); + isDomNode.assertIsArrayOfNodes(objectNodes); + + // Should have two Object elements + expect(objectNodes.length).to.equal(2); + + // Verify the first Object element + const firstObject = objectNodes[0]; + isDomNode.assertIsElementNode(firstObject); + expect(firstObject.getAttribute("Id")).to.equal("object1"); + expect(firstObject.getAttribute("MimeType")).to.equal("text/xml"); + expect(firstObject.textContent?.includes("Test data in Object element")).to.be.true; + + // Verify the second Object element + const secondObject = objectNodes[1]; + isDomNode.assertIsElementNode(secondObject); + expect(secondObject.getAttribute("Id")).to.equal("object2"); + expect(secondObject.getAttribute("MimeType")).to.equal("text/plain"); + expect(secondObject.textContent).to.equal("Plain text content"); + + // Verify that the signature is still valid + const verifier = new SignedXml(); + verifier.publicCert = fs.readFileSync("./test/static/client_public.pem"); + + // First load the signature + const signedDoc = new xmldom.DOMParser().parseFromString(signedXml); + const signatureNode = xpath.select1( + "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + signedDoc, + ); + isDomNode.assertIsNodeLike(signatureNode); + verifier.loadSignature(signatureNode); + + // Then check the signature + const isValid = verifier.checkSignature(signedXml); + expect(isValid).to.be.true; + }); + + it("should handle empty or null getObjectContent", function () { + // Create a simple XML document to sign + const xml = ''; + + // Test with null return from getObjectContent + const sigWithNull = new SignedXml({ + getObjectContent: () => null, + }); + + sigWithNull.privateKey = fs.readFileSync("./test/static/client.pem"); + sigWithNull.addReference({ + xpath: "//*[local-name(.)='x']", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + + // Set required algorithms + sigWithNull.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sigWithNull.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + + sigWithNull.computeSignature(xml); + const signedXmlWithNull = sigWithNull.getSignedXml(); + + // Parse the signed XML + const docWithNull = new xmldom.DOMParser().parseFromString(signedXmlWithNull); + + // Verify that no ds:Object elements exist + const objectNodesWithNull = xpath.select("//*[local-name(.)='Object']", docWithNull); + + if (Array.isArray(objectNodesWithNull)) { + expect(objectNodesWithNull.length).to.equal(0); + } else { + expect(objectNodesWithNull).to.not.exist; + } + + // Test with empty array return from getObjectContent + const sigWithEmpty = new SignedXml({ + getObjectContent: () => [], + }); + + sigWithEmpty.privateKey = fs.readFileSync("./test/static/client.pem"); + sigWithEmpty.addReference({ + xpath: "//*[local-name(.)='x']", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + + // Set required algorithms + sigWithEmpty.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sigWithEmpty.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + + sigWithEmpty.computeSignature(xml); + const signedXmlWithEmpty = sigWithEmpty.getSignedXml(); + + // Parse the signed XML + const docWithEmpty = new xmldom.DOMParser().parseFromString(signedXmlWithEmpty); + + // Verify that no ds:Object elements exist + const objectNodesWithEmpty = xpath.select("//*[local-name(.)='Object']", docWithEmpty); + + if (Array.isArray(objectNodesWithEmpty)) { + expect(objectNodesWithEmpty.length).to.equal(0); + } else { + expect(objectNodesWithEmpty).to.not.exist; + } + }); + + it("should handle Object with Encoding attribute", function () { + // Create a simple XML document to sign + const xml = ''; + + // Create a SignedXml instance with custom getObjectContent including Encoding attribute + const sig = new SignedXml({ + getObjectContent: () => [ + { + content: "VGhpcyBpcyBiYXNlNjQgZW5jb2RlZCBkYXRh", // "This is base64 encoded data" + attributes: { + Id: "object1", + MimeType: "application/octet-stream", + Encoding: "http://www.w3.org/2000/09/xmldsig#base64", + }, + }, + ], + }); + + // Set up the signature + 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#"], + }); + + // Set required algorithms + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + + // Compute the signature + sig.computeSignature(xml); + + // Get the signed XML + const signedXml = sig.getSignedXml(); + + // Parse the signed XML + const signedDoc = new xmldom.DOMParser().parseFromString(signedXml); + + // Verify that the ds:Object element exists + const objectNodes = xpath.select("//*[local-name(.)='Object']", signedDoc); + isDomNode.assertIsArrayOfNodes(objectNodes); + + // Should have one Object element + expect(objectNodes.length).to.equal(1); + + // Verify the Object element + const object = objectNodes[0]; + isDomNode.assertIsElementNode(object); + expect(object.getAttribute("Id")).to.equal("object1"); + expect(object.getAttribute("MimeType")).to.equal("application/octet-stream"); + expect(object.getAttribute("Encoding")).to.equal("http://www.w3.org/2000/09/xmldsig#base64"); + expect(object.textContent).to.equal("VGhpcyBpcyBiYXNlNjQgZW5jb2RlZCBkYXRh"); + + // Verify that the signature is still valid + const verifier = new SignedXml(); + verifier.publicCert = fs.readFileSync("./test/static/client_public.pem"); + + // First load the signature + const doc = new xmldom.DOMParser().parseFromString(signedXml); + const signatureNode = xpath.select1( + "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + doc, + ); + isDomNode.assertIsNodeLike(signatureNode); + verifier.loadSignature(signatureNode); + + // Then check the signature + const isValid = verifier.checkSignature(signedXml); + expect(isValid).to.be.true; + }); + + it("should sign Object with SHA256 digest algorithm", function () { + // Create a simple XML document to sign + const xml = ''; + + // Create a SignedXml instance with custom getObjectContent + const sig = new SignedXml({ + getObjectContent: () => [ + { + content: "Test data for SHA256 digest", + attributes: { + Id: "object1", + MimeType: "text/xml", + }, + }, + ], + }); + + // Set up the signature + sig.privateKey = fs.readFileSync("./test/static/client.pem"); + + // Add a reference to the document element + sig.addReference({ + xpath: "//*[local-name(.)='x']", + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + + // Add a reference to the Object element with SHA256 + sig.addReference({ + xpath: "//*[@Id='object1']", + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + isSignatureReference: true, + }); + + // Set required algorithms + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; + + // Compute the signature + sig.computeSignature(xml); + + // Get the signed XML + const signedXml = sig.getSignedXml(); + + // Parse the signed XML + const doc = new xmldom.DOMParser().parseFromString(signedXml); + + // Verify that the ds:Object element exists + const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); + isDomNode.assertIsArrayOfNodes(objectNodes); + expect(objectNodes.length).to.equal(1); + + // Verify that there are two Reference elements + const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc); + isDomNode.assertIsArrayOfNodes(referenceNodes); + expect(referenceNodes.length).to.equal(2); + + // Verify that the references use SHA256 + const digestMethodNodes = xpath.select("//*[local-name(.)='DigestMethod']", doc); + isDomNode.assertIsArrayOfNodes(digestMethodNodes); + + for (const digestMethod of digestMethodNodes) { + isDomNode.assertIsElementNode(digestMethod); + expect(digestMethod.getAttribute("Algorithm")).to.equal( + "http://www.w3.org/2001/04/xmlenc#sha256", + ); + } + + // Verify that the signature method is RSA-SHA256 + const signatureMethod = xpath.select1("//*[local-name(.)='SignatureMethod']", doc); + isDomNode.assertIsElementNode(signatureMethod); + expect(signatureMethod.getAttribute("Algorithm")).to.equal( + "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + ); + + // Verify that the signature is still valid + const verifier = new SignedXml(); + verifier.publicCert = fs.readFileSync("./test/static/client_public.pem"); + + // First load the signature + const signatureNode = xpath.select1( + "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + doc, + ); + isDomNode.assertIsNodeLike(signatureNode); + verifier.loadSignature(signatureNode); + + // Then check the signature + const isValid = verifier.checkSignature(signedXml); + expect(isValid).to.be.true; + }); + + it("should sign Object with SHA512 digest algorithm and RSA-SHA512 signature", function () { + // Create a simple XML document to sign + const xml = ''; + + // Create a SignedXml instance with custom getObjectContent + const sig = new SignedXml({ + getObjectContent: () => [ + { + content: "Test data for SHA512 digest", + attributes: { + Id: "object1", + MimeType: "text/xml", + }, + }, + ], + }); + + // Set up the signature + sig.privateKey = fs.readFileSync("./test/static/client.pem"); + + // Add a reference to the document element + sig.addReference({ + xpath: "//*[local-name(.)='x']", + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha512", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + + // Add a reference to the Object element with SHA512 + sig.addReference({ + xpath: "//*[@Id='object1']", + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha512", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + isSignatureReference: true, + }); + + // Set required algorithms + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"; + + // Compute the signature + sig.computeSignature(xml); + + // Get the signed XML + const signedXml = sig.getSignedXml(); + + // Parse the signed XML + const doc = new xmldom.DOMParser().parseFromString(signedXml); + + // Verify that the ds:Object element exists + const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); + isDomNode.assertIsArrayOfNodes(objectNodes); + expect(objectNodes.length).to.equal(1); + + // Verify that there are two Reference elements + const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc); + isDomNode.assertIsArrayOfNodes(referenceNodes); + expect(referenceNodes.length).to.equal(2); + + // Verify that the references use SHA512 + const digestMethodNodes = xpath.select("//*[local-name(.)='DigestMethod']", doc); + isDomNode.assertIsArrayOfNodes(digestMethodNodes); + + for (const digestMethod of digestMethodNodes) { + isDomNode.assertIsElementNode(digestMethod); + expect(digestMethod.getAttribute("Algorithm")).to.equal( + "http://www.w3.org/2001/04/xmlenc#sha512", + ); + } + + // Verify that the signature method is RSA-SHA512 + const signatureMethod = xpath.select1("//*[local-name(.)='SignatureMethod']", doc); + isDomNode.assertIsElementNode(signatureMethod); + expect(signatureMethod.getAttribute("Algorithm")).to.equal( + "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", + ); + + // Verify that the signature is still valid + const verifier = new SignedXml(); + verifier.publicCert = fs.readFileSync("./test/static/client_public.pem"); + + // First load the signature + const signatureNode = xpath.select1( + "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + doc, + ); + isDomNode.assertIsNodeLike(signatureNode); + verifier.loadSignature(signatureNode); + + // Then check the signature + const isValid = verifier.checkSignature(signedXml); + expect(isValid).to.be.true; + }); + + it("should sign Object with C14N canonicalization algorithm", function () { + // Create a simple XML document to sign + const xml = ''; + + // Create a SignedXml instance with custom getObjectContent + const sig = new SignedXml({ + getObjectContent: () => [ + { + content: "Test data for C14N canonicalization", + attributes: { + Id: "object1", + MimeType: "text/xml", + }, + }, + ], + }); + + // Set up the signature + sig.privateKey = fs.readFileSync("./test/static/client.pem"); + + // Add a reference to the document element + sig.addReference({ + xpath: "//*[local-name(.)='x']", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"], + }); + + // Add a reference to the Object element + sig.addReference({ + xpath: "//*[@Id='object1']", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"], + isSignatureReference: true, + }); + + // Set required algorithms + sig.canonicalizationAlgorithm = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"; + sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + + // Compute the signature + sig.computeSignature(xml); + + // Get the signed XML + const signedXml = sig.getSignedXml(); + + // Parse the signed XML + const doc = new xmldom.DOMParser().parseFromString(signedXml); + + // Verify that the ds:Object element exists + const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); + isDomNode.assertIsArrayOfNodes(objectNodes); + expect(objectNodes.length).to.equal(1); + + // Verify that there are two Reference elements + const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc); + isDomNode.assertIsArrayOfNodes(referenceNodes); + expect(referenceNodes.length).to.equal(2); + + // Verify that the transforms use C14N + const transforms = xpath.select( + "//*[local-name(.)='Reference']/*[local-name(.)='Transforms']/*[local-name(.)='Transform']", + doc, + ); + isDomNode.assertIsArrayOfNodes(transforms); + + for (const transform of transforms) { + isDomNode.assertIsElementNode(transform); + expect(transform.getAttribute("Algorithm")).to.equal( + "http://www.w3.org/TR/2001/REC-xml-c14n-20010315", + ); + } + + // Verify that the CanonicalizationMethod is C14N + const canonMethod = xpath.select1("//*[local-name(.)='CanonicalizationMethod']", doc); + isDomNode.assertIsElementNode(canonMethod); + expect(canonMethod.getAttribute("Algorithm")).to.equal( + "http://www.w3.org/TR/2001/REC-xml-c14n-20010315", + ); + + // Verify that the signature is still valid + const verifier = new SignedXml(); + verifier.publicCert = fs.readFileSync("./test/static/client_public.pem"); + + // First load the signature + const signatureNode = xpath.select1( + "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + doc, + ); + isDomNode.assertIsNodeLike(signatureNode); + verifier.loadSignature(signatureNode); + + // Then check the signature + const isValid = verifier.checkSignature(signedXml); + expect(isValid).to.be.true; + }); + + it("should add a reference to an Object element", function () { + // Create a simple XML document to sign + const xml = ''; + + // Create a SignedXml instance with custom getObjectContent + const sig = new SignedXml({ + getObjectContent: () => [ + { + content: "Test data in Object element", + attributes: { + Id: "object1", + MimeType: "text/xml", + }, + }, + ], + }); + + // Set up the signature + sig.privateKey = fs.readFileSync("./test/static/client.pem"); + + // Add a reference to the document element + 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#"], + }); + + // Add a reference to the Object element by its ID + sig.addReference({ + xpath: "//*[@Id='object1']", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + isSignatureReference: true, + }); + + // Set required algorithms + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + + // Compute the signature + sig.computeSignature(xml); + + // Get the signed XML + const signedXml = sig.getSignedXml(); + + // Parse the signed XML + const doc = new xmldom.DOMParser().parseFromString(signedXml); + + // Verify that the ds:Object element exists + const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); + isDomNode.assertIsArrayOfNodes(objectNodes); + expect(objectNodes.length).to.equal(1); + + // Verify that there are two Reference elements + const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc); + isDomNode.assertIsArrayOfNodes(referenceNodes); + expect(referenceNodes.length).to.equal(2); + + // Verify that one of the references points to the Object + const objectReference = xpath.select("//*[local-name(.)='Reference' and @URI='#object1']", doc); + isDomNode.assertIsArrayOfNodes(objectReference); + expect(objectReference.length).to.equal(1); + + // Verify that the reference is actually in the SignedInfo section + const signedInfoReference = xpath.select( + "//*[local-name(.)='SignedInfo']/*[local-name(.)='Reference' and @URI='#object1']", + doc, + ); + isDomNode.assertIsArrayOfNodes(signedInfoReference); + expect(signedInfoReference.length).to.equal(1); + + // Verify that the signature is still valid + const verifier = new SignedXml(); + verifier.publicCert = fs.readFileSync("./test/static/client_public.pem"); + + // First load the signature + const signatureNode = xpath.select1( + "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + doc, + ); + isDomNode.assertIsNodeLike(signatureNode); + verifier.loadSignature(signatureNode); + + // Then check the signature + const isValid = verifier.checkSignature(signedXml); + expect(isValid).to.be.true; + }); + + it("should allow signing Object elements within the Signature", function () { + // Create a simple XML document to sign + const xml = ''; + + // Create a SignedXml instance with custom getObjectContent + const sig = new SignedXml({ + getObjectContent: () => [ + { + content: "Test data in Object element", + attributes: { + Id: "object1", + MimeType: "text/xml", + }, + }, + ], + }); + + // Set up the signature + sig.privateKey = fs.readFileSync("./test/static/client.pem"); + + // Add a reference to the document element + 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#"], + }); + + // Add a reference to the Object element by its ID only, marking it as a signature reference + sig.addReference({ + xpath: "//*[@Id='object1']", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + isSignatureReference: true, + }); + + // Set required algorithms + sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + + // Compute the signature + sig.computeSignature(xml); + + // Get the signed XML + const signedXml = sig.getSignedXml(); + + // Parse the signed XML + const doc = new xmldom.DOMParser().parseFromString(signedXml); + + // Verify that the ds:Object element exists + const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); + isDomNode.assertIsArrayOfNodes(objectNodes); + expect(objectNodes.length).to.equal(1); + + // Verify that there are two Reference elements + const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc); + isDomNode.assertIsArrayOfNodes(referenceNodes); + expect(referenceNodes.length).to.equal(2); + + // Verify that one of the references points to the Object + const objectReference = xpath.select("//*[local-name(.)='Reference' and @URI='#object1']", doc); + isDomNode.assertIsArrayOfNodes(objectReference); + expect(objectReference.length).to.equal(1); + + // Verify that the signature is still valid + const verifier = new SignedXml(); + verifier.publicCert = fs.readFileSync("./test/static/client_public.pem"); + + // First load the signature + const signatureNode = xpath.select1( + "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + doc, + ); + isDomNode.assertIsNodeLike(signatureNode); + verifier.loadSignature(signatureNode); + + // Then check the signature + const isValid = verifier.checkSignature(signedXml); + expect(isValid).to.be.true; + }); +}); From b5aa833b96b2987111754297ecb95890d5f25321 Mon Sep 17 00:00:00 2001 From: shunkica Date: Sat, 21 Jun 2025 17:09:11 +0200 Subject: [PATCH 02/17] add XAdES test/example --- test/signature-object-tests.spec.ts | 82 ++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index 836b251..f33a6d9 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -3,7 +3,8 @@ import { expect } from "chai"; import * as xpath from "xpath"; import * as xmldom from "@xmldom/xmldom"; import * as isDomNode from "@xmldom/is-dom-node"; -import { SignedXml } from "../src/signed-xml"; +import { SignedXml } from "../src"; +import { Sha256 } from "../lib/hash-algorithms"; describe("Object support in XML signatures", function () { it("should add custom ds:Object elements to signature", function () { @@ -679,3 +680,82 @@ describe("Object support in XML signatures", function () { expect(isValid).to.be.true; }); }); + +describe("XAdES Object support in XML signatures", function () { + it("should be able to add and sign XAdES objects", function () { + const rootId = "root_0"; + const signatureId = "signature_0"; + const signedPropertiesId = "signedProperties_0"; + + const privateKey = fs.readFileSync("./test/static/client.pem"); + const publicCert = fs.readFileSync("./test/static/client_public.pem"); + const publicCertDer = fs.readFileSync("./test/static/client_public.der"); + const publicCertDigest = new Sha256().getHash(publicCertDer); + const xml = `text`; + + const sig = new SignedXml({ + publicCert: publicCert, + privateKey: privateKey, + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + getObjectContent: () => [ + { + content: + `` + + `` + + `` + + `2025-06-21T12:00:00Z` + + `` + + `` + + `${publicCertDigest}` + + `` + + `` + + `` + + ``, + }, + ], + }); + + sig.addReference({ + xpath: `//*[@Id='${rootId}']`, + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + transforms: [ + "http://www.w3.org/2000/09/xmldsig#enveloped-signature", + "http://www.w3.org/2001/10/xml-exc-c14n#", + ], + }); + + sig.addReference({ + xpath: `//*[@Id='${signedPropertiesId}']`, + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + isSignatureReference: true, + }); + + sig.computeSignature(xml, { + prefix: "ds", + location: { + action: "append", + reference: "/root", + }, + attrs: { + Id: signatureId, + }, + }); + + const signedXml = sig.getSignedXml(); + const signedDoc = new xmldom.DOMParser().parseFromString(signedXml); + const signatureNode = xpath.select1( + "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + signedDoc, + ); + isDomNode.assertIsNodeLike(signatureNode); + + const verifier = new SignedXml({ + publicCert: publicCert, + }); + verifier.loadSignature(signatureNode); + const isValid = verifier.checkSignature(signedXml); + expect(isValid).to.be.true; + }); +}); From 95a4f474d0eea297e8b4bcbac5430b41d58c6add Mon Sep 17 00:00:00 2001 From: shunkica Date: Sat, 21 Jun 2025 17:24:14 +0200 Subject: [PATCH 03/17] update docs --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index ecfa261..104b013 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,7 @@ The `SignedXml` constructor provides an abstraction for sign and verify xml docu - `keyInfoAttributes` - object - default `{}` - a hash of attributes and values `attrName: value` to add to the KeyInfo node - `getKeyInfoContent` - function - default `noop` - a function that returns the content of the KeyInfo node - `getCertFromKeyInfo` - function - default `SignedXml.getCertFromKeyInfo` - a function that returns the certificate from the `` node +- `getObjectContent` - function - default `noop` - a function that returns the content of the `` nodes #### API @@ -272,6 +273,7 @@ To sign xml documents: - `xpath` - a string containing a XPath expression referencing a xml element - `transforms` - an array of [transform algorithms](#canonicalization-and-transformation-algorithms), the referenced element will be transformed for each value in the array - `digestAlgorithm` - one of the supported [hashing algorithms](#hashing-algorithms) + - `isSignatureReference` - boolean - default `false` - indicates whether the target of this reference is located inside the `` element (e.g. an ``) - `computeSignature(xml, [options])` - compute the signature of the given xml where: - `xml` - a string containing a xml document - `options` - an object with the following properties: @@ -534,6 +536,44 @@ sig.computeSignature(xml, { }); ``` +### how to add custom Objects to the signature + +Use the `getObjectContent` option when creating a SignedXml instance to add custom Objects to the signature. You can also reference these Objects in your signature by setting `isSignatureReference` to `true` when adding a reference. + +```javascript +var SignedXml = require("xml-crypto").SignedXml, + fs = require("fs"); + +var xml = "" + "" + "Harry Potter" + "" + ""; + +const sig = new SignedXml({ + privateKey: fs.readFileSync("client.pem"), + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + getObjectContent: () => [ + { + content: "Test data in Object", + attributes: { + Id: "Object1", + MimeType: "text/xml", + }, + }, + ], +}); + +// Add a reference to the Object element +sig.addReference({ + xpath: "//*[@Id='Object1']", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + // IMPORTANT: Set isSignatureReference to true to indicate this is a reference to an element inside the Signature + isSignatureReference: true, +}); + +sig.computeSignature(xml); +fs.writeFileSync("signed.xml", sig.getSignedXml()); +``` + ### more examples (_coming soon_) ## Development From 014b0fbb1e98a3f33401ef21064fedac21dbea19 Mon Sep 17 00:00:00 2001 From: shunkica Date: Mon, 23 Jun 2025 10:40:39 +0200 Subject: [PATCH 04/17] feat: add id and type attributes to Reference elements in XML signature --- README.md | 4 ++- src/signed-xml.ts | 30 +++++++++++++++++++++-- src/types.ts | 6 +++++ test/signature-object-tests.spec.ts | 9 ++++--- test/signature-unit-tests.spec.ts | 38 +++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 104b013..c5a972b 100644 --- a/README.md +++ b/README.md @@ -269,11 +269,13 @@ A `SignedXml` object provides the following methods: To sign xml documents: -- `addReference(xpath, transforms, digestAlgorithm)` - adds a reference to a xml element where: +- `addReference({ xpath, transforms, digestAlgorithm, isSignatureReference, id, type })` - adds a reference to a xml element where: - `xpath` - a string containing a XPath expression referencing a xml element - `transforms` - an array of [transform algorithms](#canonicalization-and-transformation-algorithms), the referenced element will be transformed for each value in the array - `digestAlgorithm` - one of the supported [hashing algorithms](#hashing-algorithms) - `isSignatureReference` - boolean - default `false` - indicates whether the target of this reference is located inside the `` element (e.g. an ``) + - `id` - an optional `Id` attribute to add to the reference element + - `type` - the optional `Type` attribute to add to the reference element (represented as a URI) - `computeSignature(xml, [options])` - compute the signature of the given xml where: - `xml` - a string containing a xml document - `options` - an object with the following properties: diff --git a/src/signed-xml.ts b/src/signed-xml.ts index afe8bef..b0d0747 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -810,6 +810,8 @@ export class SignedXml { * @param inclusiveNamespacesPrefixList The prefix list for inclusive namespace canonicalization. * @param isEmptyUri Indicates whether the URI is empty. Defaults to `false`. * @param isSignatureReference Indicates whether this reference points to an element in the signature itself (like an Object element). + * @param id An optional `Id` attribute for the reference. + * @param type An optional `Type` attribute for the reference. */ addReference({ xpath, @@ -820,6 +822,8 @@ export class SignedXml { inclusiveNamespacesPrefixList = [], isEmptyUri = false, isSignatureReference = false, + id = undefined, + type = undefined, }: Partial & Pick): void { if (digestAlgorithm == null) { throw new Error("digestAlgorithm is required"); @@ -838,6 +842,8 @@ export class SignedXml { inclusiveNamespacesPrefixList, isEmptyUri, isSignatureReference, + id, + type, getValidatedNode: () => { throw new Error( "Reference has not been validated yet; Did you call `sig.checkSignature()`?", @@ -1148,13 +1154,25 @@ export class SignedXml { } for (const node of nodes) { + let referenceAttrs = ""; + if (ref.isEmptyUri) { - res += `<${prefix}Reference URI="">`; + referenceAttrs = 'URI=""'; } else { const id = this.ensureHasId(node); ref.uri = id; - res += `<${prefix}Reference URI="#${id}">`; + referenceAttrs = `URI="#${id}"`; + } + + if (ref.id) { + referenceAttrs += ` Id="${ref.id}"`; + } + + if (ref.type) { + referenceAttrs += ` Type="${ref.type}"`; } + + res += `<${prefix}Reference ${referenceAttrs}>`; res += `<${prefix}Transforms>`; for (const trans of ref.transforms || []) { const transform = this.findCanonicalizationAlgorithm(trans); @@ -1368,6 +1386,14 @@ export class SignedXml { referenceElem.setAttribute("URI", `#${id}`); } + if (ref.id) { + referenceElem.setAttribute("Id", ref.id); + } + + if (ref.type) { + referenceElem.setAttribute("Type", ref.type); + } + const transformsElem = signatureDoc.createElementNS( signatureNamespace, `${prefix}Transforms`, diff --git a/src/types.ts b/src/types.ts index a3eca60..b9c0c42 100644 --- a/src/types.ts +++ b/src/types.ts @@ -146,6 +146,12 @@ export interface Reference { // Optional. Indicates if this reference points to an element within the Signature isSignatureReference?: boolean; + // Optional. The `Id` attribute of the reference node. + id?: string; + + // Optional. The `Type` attribute of the reference node. + type?: string; + // Optional. The type of the reference node. ancestorNamespaces?: NamespacePrefix[]; diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index f33a6d9..704ac80 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -683,7 +683,6 @@ describe("Object support in XML signatures", function () { describe("XAdES Object support in XML signatures", function () { it("should be able to add and sign XAdES objects", function () { - const rootId = "root_0"; const signatureId = "signature_0"; const signedPropertiesId = "signedProperties_0"; @@ -691,7 +690,7 @@ describe("XAdES Object support in XML signatures", function () { const publicCert = fs.readFileSync("./test/static/client_public.pem"); const publicCertDer = fs.readFileSync("./test/static/client_public.der"); const publicCertDigest = new Sha256().getHash(publicCertDer); - const xml = `text`; + const xml = `text`; const sig = new SignedXml({ publicCert: publicCert, @@ -701,7 +700,7 @@ describe("XAdES Object support in XML signatures", function () { getObjectContent: () => [ { content: - `` + + `` + `` + `` + `2025-06-21T12:00:00Z` + @@ -717,7 +716,8 @@ describe("XAdES Object support in XML signatures", function () { }); sig.addReference({ - xpath: `//*[@Id='${rootId}']`, + xpath: `/*`, + isEmptyUri: true, digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", transforms: [ "http://www.w3.org/2000/09/xmldsig#enveloped-signature", @@ -727,6 +727,7 @@ describe("XAdES Object support in XML signatures", function () { sig.addReference({ xpath: `//*[@Id='${signedPropertiesId}']`, + type: "http://uri.etsi.org/01903#SignedProperties", digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], isSignatureReference: true, diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index baa382d..94aeab4 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -1279,4 +1279,42 @@ describe("Signature unit tests", function () { "MIIDZ", ); }); + + it("adds id and type attributes to Reference elements when provided", function () { + const xml = ""; + const sig = new SignedXml(); + 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#"], + id: "ref-1", + type: "http://www.w3.org/2000/09/xmldsig#Object", + }); + + 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); + const signedXml = sig.getSignedXml(); + + const doc = new xmldom.DOMParser().parseFromString(signedXml); + const referenceElements = xpath.select("//*[local-name(.)='Reference']", doc); + isDomNode.assertIsArrayOfNodes(referenceElements); + expect(referenceElements.length, "Reference element should exist").to.equal(1); + + const referenceElement = referenceElements[0]; + isDomNode.assertIsElementNode(referenceElement); + + const idAttribute = referenceElement.getAttribute("Id"); + expect(idAttribute, "Reference element should have the correct Id attribute value").to.equal( + "ref-1", + ); + + const typeAttribute = referenceElement.getAttribute("Type"); + expect( + typeAttribute, + "Reference element should have the correct Type attribute value", + ).to.equal("http://www.w3.org/2000/09/xmldsig#Object"); + }); }); From 71988ee1b08966fc1be9a9103cf4af52f58fef14 Mon Sep 17 00:00:00 2001 From: shunkica Date: Tue, 8 Jul 2025 23:32:56 +0200 Subject: [PATCH 05/17] Update README.md Co-authored-by: Chris Barth --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c5a972b..6841026 100644 --- a/README.md +++ b/README.md @@ -538,7 +538,7 @@ sig.computeSignature(xml, { }); ``` -### how to add custom Objects to the signature +### How to add custom Objects to the signature Use the `getObjectContent` option when creating a SignedXml instance to add custom Objects to the signature. You can also reference these Objects in your signature by setting `isSignatureReference` to `true` when adding a reference. From c53943827463bce5b7627ffb28f2e687d59a37e8 Mon Sep 17 00:00:00 2001 From: shunkica Date: Fri, 15 Aug 2025 11:16:55 +0200 Subject: [PATCH 06/17] refactor!: convert getObjectContent from function to static objects property --- README.md | 6 ++--- src/signed-xml.ts | 22 +++++------------ src/types.ts | 2 +- test/signature-object-tests.spec.ts | 38 ++++++++++++++--------------- 4 files changed, 29 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 6841026..6675e08 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ The `SignedXml` constructor provides an abstraction for sign and verify xml docu - `keyInfoAttributes` - object - default `{}` - a hash of attributes and values `attrName: value` to add to the KeyInfo node - `getKeyInfoContent` - function - default `noop` - a function that returns the content of the KeyInfo node - `getCertFromKeyInfo` - function - default `SignedXml.getCertFromKeyInfo` - a function that returns the certificate from the `` node -- `getObjectContent` - function - default `noop` - a function that returns the content of the `` nodes +- `objects` - array - default `undefined` - an array of objects defining the content of the `` nodes #### API @@ -540,7 +540,7 @@ sig.computeSignature(xml, { ### How to add custom Objects to the signature -Use the `getObjectContent` option when creating a SignedXml instance to add custom Objects to the signature. You can also reference these Objects in your signature by setting `isSignatureReference` to `true` when adding a reference. +Use the `objects` option when creating a SignedXml instance to add custom Objects to the signature. You can also reference these Objects in your signature by setting `isSignatureReference` to `true` when adding a reference. ```javascript var SignedXml = require("xml-crypto").SignedXml, @@ -552,7 +552,7 @@ const sig = new SignedXml({ privateKey: fs.readFileSync("client.pem"), canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", - getObjectContent: () => [ + objects: [ { content: "Test data in Object", attributes: { diff --git a/src/signed-xml.ts b/src/signed-xml.ts index b0d0747..4fb4fc0 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -8,6 +8,7 @@ import type { GetKeyInfoContentArgs, HashAlgorithm, HashAlgorithmType, + ObjectAttributes, Reference, SignatureAlgorithm, SignatureAlgorithmType, @@ -56,7 +57,7 @@ export class SignedXml { keyInfoAttributes: { [attrName: string]: string } = {}; getKeyInfoContent = SignedXml.getKeyInfoContent; getCertFromKeyInfo = SignedXml.getCertFromKeyInfo; - getObjectContent = SignedXml.getObjectContent; + objects?: Array<{ content: string; attributes?: ObjectAttributes }>; // Internal state private id = 0; @@ -127,16 +128,6 @@ export class SignedXml { static noop = () => null; - /** - * Default implementation for getObjectContent that returns null (no Objects) - */ - static getObjectContent(): Array<{ - content: string; - attributes?: Record; - }> | null { - return null; - } - /** * The SignedXml constructor provides an abstraction for sign and verify xml documents. The object is constructed using * @param options {@link SignedXmlOptions} @@ -154,7 +145,7 @@ export class SignedXml { keyInfoAttributes, getKeyInfoContent, getCertFromKeyInfo, - getObjectContent, + objects, } = options; // Options @@ -176,7 +167,7 @@ export class SignedXml { this.keyInfoAttributes = keyInfoAttributes ?? this.keyInfoAttributes; this.getKeyInfoContent = getKeyInfoContent ?? this.getKeyInfoContent; this.getCertFromKeyInfo = getCertFromKeyInfo ?? SignedXml.noop; - this.getObjectContent = getObjectContent ?? this.getObjectContent; + this.objects = objects; this.CanonicalizationAlgorithms; this.HashAlgorithms; this.SignatureAlgorithms; @@ -1104,15 +1095,14 @@ export class SignedXml { */ private getObjects(prefix?: string) { const currentPrefix = prefix ? `${prefix}:` : ""; - const objects = this.getObjectContent?.(); - if (!objects || objects.length === 0) { + if (!this.objects || this.objects.length === 0) { return ""; } let result = ""; - for (const obj of objects) { + for (const obj of this.objects) { let objectAttrs = ""; if (obj.attributes) { Object.keys(obj.attributes).forEach((name) => { diff --git a/src/types.ts b/src/types.ts index b9c0c42..a66bc96 100644 --- a/src/types.ts +++ b/src/types.ts @@ -73,7 +73,7 @@ export interface SignedXmlOptions { keyInfoAttributes?: Record; getKeyInfoContent?(args?: GetKeyInfoContentArgs): string | null; getCertFromKeyInfo?(keyInfo?: Node | null): string | null; - getObjectContent?(): Array<{ content: string; attributes?: ObjectAttributes }> | null; + objects?: Array<{ content: string; attributes?: ObjectAttributes }>; } export interface NamespacePrefix { diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index 704ac80..03e1274 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -11,9 +11,9 @@ describe("Object support in XML signatures", function () { // Create a simple XML document to sign const xml = ''; - // Create a SignedXml instance with custom getObjectContent + // Create a SignedXml instance with custom objects const sig = new SignedXml({ - getObjectContent: () => [ + objects: [ { content: "Test data in Object element", attributes: { @@ -95,9 +95,9 @@ describe("Object support in XML signatures", function () { // Create a simple XML document to sign const xml = ''; - // Test with null return from getObjectContent + // Test with undefined objects const sigWithNull = new SignedXml({ - getObjectContent: () => null, + objects: undefined, }); sigWithNull.privateKey = fs.readFileSync("./test/static/client.pem"); @@ -126,9 +126,9 @@ describe("Object support in XML signatures", function () { expect(objectNodesWithNull).to.not.exist; } - // Test with empty array return from getObjectContent + // Test with empty array objects const sigWithEmpty = new SignedXml({ - getObjectContent: () => [], + objects: [], }); sigWithEmpty.privateKey = fs.readFileSync("./test/static/client.pem"); @@ -162,9 +162,9 @@ describe("Object support in XML signatures", function () { // Create a simple XML document to sign const xml = ''; - // Create a SignedXml instance with custom getObjectContent including Encoding attribute + // Create a SignedXml instance with custom objects including Encoding attribute const sig = new SignedXml({ - getObjectContent: () => [ + objects: [ { content: "VGhpcyBpcyBiYXNlNjQgZW5jb2RlZCBkYXRh", // "This is base64 encoded data" attributes: { @@ -234,9 +234,9 @@ describe("Object support in XML signatures", function () { // Create a simple XML document to sign const xml = ''; - // Create a SignedXml instance with custom getObjectContent + // Create a SignedXml instance with custom objects const sig = new SignedXml({ - getObjectContent: () => [ + objects: [ { content: "Test data for SHA256 digest", attributes: { @@ -327,9 +327,9 @@ describe("Object support in XML signatures", function () { // Create a simple XML document to sign const xml = ''; - // Create a SignedXml instance with custom getObjectContent + // Create a SignedXml instance with custom objects const sig = new SignedXml({ - getObjectContent: () => [ + objects: [ { content: "Test data for SHA512 digest", attributes: { @@ -420,9 +420,9 @@ describe("Object support in XML signatures", function () { // Create a simple XML document to sign const xml = ''; - // Create a SignedXml instance with custom getObjectContent + // Create a SignedXml instance with custom objects const sig = new SignedXml({ - getObjectContent: () => [ + objects: [ { content: "Test data for C14N canonicalization", attributes: { @@ -516,9 +516,9 @@ describe("Object support in XML signatures", function () { // Create a simple XML document to sign const xml = ''; - // Create a SignedXml instance with custom getObjectContent + // Create a SignedXml instance with custom objects const sig = new SignedXml({ - getObjectContent: () => [ + objects: [ { content: "Test data in Object element", attributes: { @@ -604,9 +604,9 @@ describe("Object support in XML signatures", function () { // Create a simple XML document to sign const xml = ''; - // Create a SignedXml instance with custom getObjectContent + // Create a SignedXml instance with custom objects const sig = new SignedXml({ - getObjectContent: () => [ + objects: [ { content: "Test data in Object element", attributes: { @@ -697,7 +697,7 @@ describe("XAdES Object support in XML signatures", function () { privateKey: privateKey, canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", - getObjectContent: () => [ + objects: [ { content: `` + From 8aac54064de5cd42828fdf94600c2edab889d64f Mon Sep 17 00:00:00 2001 From: shunkica Date: Fri, 15 Aug 2025 11:51:20 +0200 Subject: [PATCH 07/17] refactor!: remove necessity for isSignatureReference property by tracking which references were processed --- README.md | 7 ++---- src/signed-xml.ts | 38 +++++++++++------------------ src/types.ts | 3 --- test/signature-object-tests.spec.ts | 8 +----- 4 files changed, 17 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 6675e08..bba708d 100644 --- a/README.md +++ b/README.md @@ -269,11 +269,10 @@ A `SignedXml` object provides the following methods: To sign xml documents: -- `addReference({ xpath, transforms, digestAlgorithm, isSignatureReference, id, type })` - adds a reference to a xml element where: +- `addReference({ xpath, transforms, digestAlgorithm, id, type })` - adds a reference to a xml element where: - `xpath` - a string containing a XPath expression referencing a xml element - `transforms` - an array of [transform algorithms](#canonicalization-and-transformation-algorithms), the referenced element will be transformed for each value in the array - `digestAlgorithm` - one of the supported [hashing algorithms](#hashing-algorithms) - - `isSignatureReference` - boolean - default `false` - indicates whether the target of this reference is located inside the `` element (e.g. an ``) - `id` - an optional `Id` attribute to add to the reference element - `type` - the optional `Type` attribute to add to the reference element (represented as a URI) - `computeSignature(xml, [options])` - compute the signature of the given xml where: @@ -540,7 +539,7 @@ sig.computeSignature(xml, { ### How to add custom Objects to the signature -Use the `objects` option when creating a SignedXml instance to add custom Objects to the signature. You can also reference these Objects in your signature by setting `isSignatureReference` to `true` when adding a reference. +Use the `objects` option when creating a SignedXml instance to add custom Objects to the signature. ```javascript var SignedXml = require("xml-crypto").SignedXml, @@ -568,8 +567,6 @@ sig.addReference({ xpath: "//*[@Id='Object1']", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], - // IMPORTANT: Set isSignatureReference to true to indicate this is a reference to an element inside the Signature - isSignatureReference: true, }); sig.computeSignature(xml); diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 4fb4fc0..a2e258f 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -72,7 +72,7 @@ export class SignedXml { * Contains the references that were signed. * @see {@link Reference} */ - private references: Reference[] = []; + private references: (Reference & { wasProcessed: boolean })[] = []; /** * Contains the canonicalized XML of the references that were validly signed. @@ -800,7 +800,6 @@ export class SignedXml { * @param digestValue The expected digest value for the reference. * @param inclusiveNamespacesPrefixList The prefix list for inclusive namespace canonicalization. * @param isEmptyUri Indicates whether the URI is empty. Defaults to `false`. - * @param isSignatureReference Indicates whether this reference points to an element in the signature itself (like an Object element). * @param id An optional `Id` attribute for the reference. * @param type An optional `Type` attribute for the reference. */ @@ -812,7 +811,6 @@ export class SignedXml { digestValue, inclusiveNamespacesPrefixList = [], isEmptyUri = false, - isSignatureReference = false, id = undefined, type = undefined, }: Partial & Pick): void { @@ -832,9 +830,9 @@ export class SignedXml { digestValue, inclusiveNamespacesPrefixList, isEmptyUri, - isSignatureReference, id, type, + wasProcessed: false, getValidatedNode: () => { throw new Error( "Reference has not been validated yet; Did you call `sig.checkSignature()`?", @@ -1131,16 +1129,11 @@ export class SignedXml { /* eslint-disable-next-line deprecation/deprecation */ for (const ref of this.getReferences()) { - if (ref.isSignatureReference) { - // For signature references, we'll handle them separately after the signature is created - continue; - } const nodes = xpath.selectWithResolver(ref.xpath ?? "", doc, this.namespaceResolver); if (!utils.isArrayHasLength(nodes)) { - throw new Error( - `the following xpath cannot be signed because it was not found: ${ref.xpath}`, - ); + // Don't throw here - we'll handle this in processSignatureReferences + continue; } for (const node of nodes) { @@ -1187,6 +1180,7 @@ export class SignedXml { `<${prefix}DigestValue>${digestAlgorithm.getHash(canonXml)}` + ``; } + ref.wasProcessed = true; } return res; @@ -1328,13 +1322,13 @@ export class SignedXml { } /** - * Process references that point to elements within the Signature element - * This is called after the initial signature has been created + * Process references that weren't found in the initial document + * This is called after the initial signature has been created to handle references to signature elements */ private processSignatureReferences(signatureDoc: Document, prefix?: string) { - // Get signature references - const signatureReferences = this.references.filter((ref) => ref.isSignatureReference); - if (signatureReferences.length === 0) { + // Get unprocessed references + const unprocessedReferences = this.references.filter((ref) => !ref.wasProcessed); + if (unprocessedReferences.length === 0) { return; } @@ -1351,8 +1345,8 @@ export class SignedXml { throw new Error("Could not find SignedInfo element in signature"); } - // Process each signature reference - for (const ref of signatureReferences) { + // Process each unprocessed reference + for (const ref of unprocessedReferences) { const nodes = xpath.selectWithResolver(ref.xpath ?? "", signatureDoc, this.namespaceResolver); if (!utils.isArrayHasLength(nodes)) { @@ -1415,12 +1409,8 @@ export class SignedXml { // Get the canonicalized XML const canonXml = this.getCanonReferenceXml(signatureDoc, ref, node); - // Calculate the digest + // Get the digest algorithm and compute the digest value const digestAlgorithm = this.findHashAlgorithm(ref.digestAlgorithm); - const digestValue = digestAlgorithm.getHash(canonXml); - - // Store the digest value for later validation - ref.digestValue = digestValue; const digestMethodElem = signatureDoc.createElementNS( signatureNamespace, @@ -1432,7 +1422,7 @@ export class SignedXml { signatureNamespace, `${prefix}DigestValue`, ); - digestValueElem.textContent = digestValue; + digestValueElem.textContent = digestAlgorithm.getHash(canonXml); referenceElem.appendChild(transformsElem); referenceElem.appendChild(digestMethodElem); diff --git a/src/types.ts b/src/types.ts index a66bc96..146bdd1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -143,9 +143,6 @@ export interface Reference { // Optional. Indicates whether the URI is empty. isEmptyUri: boolean; - // Optional. Indicates if this reference points to an element within the Signature - isSignatureReference?: boolean; - // Optional. The `Id` attribute of the reference node. id?: string; diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index 03e1274..dcf3007 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -262,7 +262,6 @@ describe("Object support in XML signatures", function () { xpath: "//*[@Id='object1']", digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], - isSignatureReference: true, }); // Set required algorithms @@ -355,7 +354,6 @@ describe("Object support in XML signatures", function () { xpath: "//*[@Id='object1']", digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha512", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], - isSignatureReference: true, }); // Set required algorithms @@ -448,7 +446,6 @@ describe("Object support in XML signatures", function () { xpath: "//*[@Id='object1']", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", transforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"], - isSignatureReference: true, }); // Set required algorithms @@ -544,7 +541,6 @@ describe("Object support in XML signatures", function () { xpath: "//*[@Id='object1']", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], - isSignatureReference: true, }); // Set required algorithms @@ -627,12 +623,11 @@ describe("Object support in XML signatures", function () { transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); - // Add a reference to the Object element by its ID only, marking it as a signature reference + // Add a reference to the Object element by its ID sig.addReference({ xpath: "//*[@Id='object1']", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], - isSignatureReference: true, }); // Set required algorithms @@ -730,7 +725,6 @@ describe("XAdES Object support in XML signatures", function () { type: "http://uri.etsi.org/01903#SignedProperties", digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], - isSignatureReference: true, }); sig.computeSignature(xml, { From 55dc24df9af33ff0f21b824b38dfe121c52f868b Mon Sep 17 00:00:00 2001 From: shunkica Date: Fri, 15 Aug 2025 13:02:24 +0200 Subject: [PATCH 08/17] refactor: resolve review comments --- test/signature-object-tests.spec.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index dcf3007..7e32486 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -119,12 +119,7 @@ describe("Object support in XML signatures", function () { // Verify that no ds:Object elements exist const objectNodesWithNull = xpath.select("//*[local-name(.)='Object']", docWithNull); - - if (Array.isArray(objectNodesWithNull)) { - expect(objectNodesWithNull.length).to.equal(0); - } else { - expect(objectNodesWithNull).to.not.exist; - } + expect(objectNodesWithNull).to.be.an("array").that.is.empty; // Test with empty array objects const sigWithEmpty = new SignedXml({ @@ -150,12 +145,7 @@ describe("Object support in XML signatures", function () { // Verify that no ds:Object elements exist const objectNodesWithEmpty = xpath.select("//*[local-name(.)='Object']", docWithEmpty); - - if (Array.isArray(objectNodesWithEmpty)) { - expect(objectNodesWithEmpty.length).to.equal(0); - } else { - expect(objectNodesWithEmpty).to.not.exist; - } + expect(objectNodesWithEmpty).to.be.an("array").that.is.empty; }); it("should handle Object with Encoding attribute", function () { From 9fd58a5169c3ec6b5983e87cc8565a8d800a204d Mon Sep 17 00:00:00 2001 From: shunkica Date: Fri, 15 Aug 2025 13:55:15 +0200 Subject: [PATCH 09/17] fix: wrong import path --- test/signature-object-tests.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index 7e32486..887b0f6 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -4,7 +4,7 @@ import * as xpath from "xpath"; import * as xmldom from "@xmldom/xmldom"; import * as isDomNode from "@xmldom/is-dom-node"; import { SignedXml } from "../src"; -import { Sha256 } from "../lib/hash-algorithms"; +import { Sha256 } from "../src/hash-algorithms"; describe("Object support in XML signatures", function () { it("should add custom ds:Object elements to signature", function () { @@ -91,7 +91,7 @@ describe("Object support in XML signatures", function () { expect(isValid).to.be.true; }); - it("should handle empty or null getObjectContent", function () { + it("should handle empty or undefined objects", function () { // Create a simple XML document to sign const xml = ''; From eafbfc3f870978a8ecc99fb4aca23671563d3900 Mon Sep 17 00:00:00 2001 From: shunkica Date: Fri, 15 Aug 2025 15:43:49 +0200 Subject: [PATCH 10/17] tests: add and refactor tests, remove redundant comments --- test/signature-object-tests.spec.ts | 352 ++++++++-------------------- test/signature-unit-tests.spec.ts | 18 ++ 2 files changed, 120 insertions(+), 250 deletions(-) diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index 887b0f6..ae46a9d 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -6,13 +6,32 @@ import * as isDomNode from "@xmldom/is-dom-node"; import { SignedXml } from "../src"; import { Sha256 } from "../src/hash-algorithms"; +const privateKey = fs.readFileSync("./test/static/client.pem", "utf-8"); +const publicCert = fs.readFileSync("./test/static/client_public.pem", "utf-8"); +const publicCertDer = fs.readFileSync("./test/static/client_public.der"); + +const checkSignature = (signedXml: string, signedDoc?: Document) => { + if (!signedDoc) { + signedDoc = new xmldom.DOMParser().parseFromString(signedXml); + } + const verifier = new SignedXml({ publicCert }); + const signatureNode = xpath.select1( + "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + signedDoc, + ); + isDomNode.assertIsNodeLike(signatureNode); + verifier.loadSignature(signatureNode); + return verifier.checkSignature(signedXml); +}; + describe("Object support in XML signatures", function () { it("should add custom ds:Object elements to signature", function () { - // Create a simple XML document to sign const xml = ''; - // Create a SignedXml instance with custom objects const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", objects: [ { content: "Test data in Object element", @@ -31,32 +50,19 @@ describe("Object support in XML signatures", function () { ], }); - // Set up the signature - 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#"], }); - // Set required algorithms - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; - - // Compute the signature sig.computeSignature(xml); - - // Get the signed XML const signedXml = sig.getSignedXml(); - - // Parse the signed XML const doc = new xmldom.DOMParser().parseFromString(signedXml); - // Verify that the ds:Object elements exist + // Should have two Object elements const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); isDomNode.assertIsArrayOfNodes(objectNodes); - - // Should have two Object elements expect(objectNodes.length).to.equal(2); // Verify the first Object element @@ -73,87 +79,65 @@ describe("Object support in XML signatures", function () { expect(secondObject.getAttribute("MimeType")).to.equal("text/plain"); expect(secondObject.textContent).to.equal("Plain text content"); - // Verify that the signature is still valid - const verifier = new SignedXml(); - verifier.publicCert = fs.readFileSync("./test/static/client_public.pem"); - - // First load the signature - const signedDoc = new xmldom.DOMParser().parseFromString(signedXml); - const signatureNode = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", - signedDoc, - ); - isDomNode.assertIsNodeLike(signatureNode); - verifier.loadSignature(signatureNode); - - // Then check the signature - const isValid = verifier.checkSignature(signedXml); - expect(isValid).to.be.true; + // Verify that the signature is valid + expect(checkSignature(signedXml, doc)).to.be.true; }); it("should handle empty or undefined objects", function () { - // Create a simple XML document to sign const xml = ''; // Test with undefined objects const sigWithNull = new SignedXml({ + privateKey, + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", objects: undefined, }); - sigWithNull.privateKey = fs.readFileSync("./test/static/client.pem"); sigWithNull.addReference({ xpath: "//*[local-name(.)='x']", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); - // Set required algorithms - sigWithNull.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sigWithNull.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; - sigWithNull.computeSignature(xml); const signedXmlWithNull = sigWithNull.getSignedXml(); - - // Parse the signed XML const docWithNull = new xmldom.DOMParser().parseFromString(signedXmlWithNull); - // Verify that no ds:Object elements exist + // Verify that no Object elements exist const objectNodesWithNull = xpath.select("//*[local-name(.)='Object']", docWithNull); expect(objectNodesWithNull).to.be.an("array").that.is.empty; // Test with empty array objects const sigWithEmpty = new SignedXml({ + privateKey, + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", objects: [], }); - sigWithEmpty.privateKey = fs.readFileSync("./test/static/client.pem"); sigWithEmpty.addReference({ xpath: "//*[local-name(.)='x']", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); - // Set required algorithms - sigWithEmpty.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sigWithEmpty.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; - sigWithEmpty.computeSignature(xml); const signedXmlWithEmpty = sigWithEmpty.getSignedXml(); - - // Parse the signed XML const docWithEmpty = new xmldom.DOMParser().parseFromString(signedXmlWithEmpty); - // Verify that no ds:Object elements exist + // Verify that no Object elements exist const objectNodesWithEmpty = xpath.select("//*[local-name(.)='Object']", docWithEmpty); expect(objectNodesWithEmpty).to.be.an("array").that.is.empty; }); it("should handle Object with Encoding attribute", function () { - // Create a simple XML document to sign const xml = ''; - // Create a SignedXml instance with custom objects including Encoding attribute const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", objects: [ { content: "VGhpcyBpcyBiYXNlNjQgZW5jb2RlZCBkYXRh", // "This is base64 encoded data" @@ -166,32 +150,19 @@ describe("Object support in XML signatures", function () { ], }); - // Set up the signature - 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#"], }); - // Set required algorithms - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; - - // Compute the signature sig.computeSignature(xml); - - // Get the signed XML const signedXml = sig.getSignedXml(); - - // Parse the signed XML const signedDoc = new xmldom.DOMParser().parseFromString(signedXml); - // Verify that the ds:Object element exists + // Verify that the Object element exists const objectNodes = xpath.select("//*[local-name(.)='Object']", signedDoc); isDomNode.assertIsArrayOfNodes(objectNodes); - - // Should have one Object element expect(objectNodes.length).to.equal(1); // Verify the Object element @@ -202,30 +173,17 @@ describe("Object support in XML signatures", function () { expect(object.getAttribute("Encoding")).to.equal("http://www.w3.org/2000/09/xmldsig#base64"); expect(object.textContent).to.equal("VGhpcyBpcyBiYXNlNjQgZW5jb2RlZCBkYXRh"); - // Verify that the signature is still valid - const verifier = new SignedXml(); - verifier.publicCert = fs.readFileSync("./test/static/client_public.pem"); - - // First load the signature - const doc = new xmldom.DOMParser().parseFromString(signedXml); - const signatureNode = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", - doc, - ); - isDomNode.assertIsNodeLike(signatureNode); - verifier.loadSignature(signatureNode); - - // Then check the signature - const isValid = verifier.checkSignature(signedXml); - expect(isValid).to.be.true; + // Verify that the signature is valid + expect(checkSignature(signedXml, signedDoc)).to.be.true; }); it("should sign Object with SHA256 digest algorithm", function () { - // Create a simple XML document to sign const xml = ''; - // Create a SignedXml instance with custom objects const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", objects: [ { content: "Test data for SHA256 digest", @@ -237,34 +195,21 @@ describe("Object support in XML signatures", function () { ], }); - // Set up the signature - sig.privateKey = fs.readFileSync("./test/static/client.pem"); - - // Add a reference to the document element sig.addReference({ xpath: "//*[local-name(.)='x']", digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); - // Add a reference to the Object element with SHA256 sig.addReference({ xpath: "//*[@Id='object1']", digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); - // Set required algorithms - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; - - // Compute the signature sig.computeSignature(xml); - - // Get the signed XML const signedXml = sig.getSignedXml(); - // Parse the signed XML const doc = new xmldom.DOMParser().parseFromString(signedXml); // Verify that the ds:Object element exists @@ -295,29 +240,17 @@ describe("Object support in XML signatures", function () { "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", ); - // Verify that the signature is still valid - const verifier = new SignedXml(); - verifier.publicCert = fs.readFileSync("./test/static/client_public.pem"); - - // First load the signature - const signatureNode = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", - doc, - ); - isDomNode.assertIsNodeLike(signatureNode); - verifier.loadSignature(signatureNode); - - // Then check the signature - const isValid = verifier.checkSignature(signedXml); - expect(isValid).to.be.true; + // Verify that the signature is valid + expect(checkSignature(signedXml, doc)).to.be.true; }); it("should sign Object with SHA512 digest algorithm and RSA-SHA512 signature", function () { - // Create a simple XML document to sign const xml = ''; - // Create a SignedXml instance with custom objects const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", objects: [ { content: "Test data for SHA512 digest", @@ -329,34 +262,20 @@ describe("Object support in XML signatures", function () { ], }); - // Set up the signature - sig.privateKey = fs.readFileSync("./test/static/client.pem"); - - // Add a reference to the document element sig.addReference({ xpath: "//*[local-name(.)='x']", digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha512", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); - // Add a reference to the Object element with SHA512 sig.addReference({ xpath: "//*[@Id='object1']", digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha512", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); - // Set required algorithms - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"; - - // Compute the signature sig.computeSignature(xml); - - // Get the signed XML const signedXml = sig.getSignedXml(); - - // Parse the signed XML const doc = new xmldom.DOMParser().parseFromString(signedXml); // Verify that the ds:Object element exists @@ -387,29 +306,17 @@ describe("Object support in XML signatures", function () { "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", ); - // Verify that the signature is still valid - const verifier = new SignedXml(); - verifier.publicCert = fs.readFileSync("./test/static/client_public.pem"); - - // First load the signature - const signatureNode = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", - doc, - ); - isDomNode.assertIsNodeLike(signatureNode); - verifier.loadSignature(signatureNode); - - // Then check the signature - const isValid = verifier.checkSignature(signedXml); - expect(isValid).to.be.true; + // Verify that the signature is valid + expect(checkSignature(signedXml, doc)).to.be.true; }); it("should sign Object with C14N canonicalization algorithm", function () { - // Create a simple XML document to sign const xml = ''; - // Create a SignedXml instance with custom objects const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: "http://www.w3.org/TR/2001/REC-xml-c14n-20010315", + signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", objects: [ { content: "Test data for C14N canonicalization", @@ -421,34 +328,20 @@ describe("Object support in XML signatures", function () { ], }); - // Set up the signature - sig.privateKey = fs.readFileSync("./test/static/client.pem"); - - // Add a reference to the document element sig.addReference({ xpath: "//*[local-name(.)='x']", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", transforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"], }); - // Add a reference to the Object element sig.addReference({ xpath: "//*[@Id='object1']", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", transforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"], }); - // Set required algorithms - sig.canonicalizationAlgorithm = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; - - // Compute the signature sig.computeSignature(xml); - - // Get the signed XML const signedXml = sig.getSignedXml(); - - // Parse the signed XML const doc = new xmldom.DOMParser().parseFromString(signedXml); // Verify that the ds:Object element exists @@ -482,29 +375,17 @@ describe("Object support in XML signatures", function () { "http://www.w3.org/TR/2001/REC-xml-c14n-20010315", ); - // Verify that the signature is still valid - const verifier = new SignedXml(); - verifier.publicCert = fs.readFileSync("./test/static/client_public.pem"); - - // First load the signature - const signatureNode = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", - doc, - ); - isDomNode.assertIsNodeLike(signatureNode); - verifier.loadSignature(signatureNode); - - // Then check the signature - const isValid = verifier.checkSignature(signedXml); - expect(isValid).to.be.true; + // Verify that the signature is valid + expect(checkSignature(signedXml, doc)).to.be.true; }); it("should add a reference to an Object element", function () { - // Create a simple XML document to sign const xml = ''; - // Create a SignedXml instance with custom objects const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", objects: [ { content: "Test data in Object element", @@ -516,34 +397,20 @@ describe("Object support in XML signatures", function () { ], }); - // Set up the signature - sig.privateKey = fs.readFileSync("./test/static/client.pem"); - - // Add a reference to the document element 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#"], }); - // Add a reference to the Object element by its ID sig.addReference({ xpath: "//*[@Id='object1']", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); - // Set required algorithms - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; - - // Compute the signature sig.computeSignature(xml); - - // Get the signed XML const signedXml = sig.getSignedXml(); - - // Parse the signed XML const doc = new xmldom.DOMParser().parseFromString(signedXml); // Verify that the ds:Object element exists @@ -569,29 +436,17 @@ describe("Object support in XML signatures", function () { isDomNode.assertIsArrayOfNodes(signedInfoReference); expect(signedInfoReference.length).to.equal(1); - // Verify that the signature is still valid - const verifier = new SignedXml(); - verifier.publicCert = fs.readFileSync("./test/static/client_public.pem"); - - // First load the signature - const signatureNode = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", - doc, - ); - isDomNode.assertIsNodeLike(signatureNode); - verifier.loadSignature(signatureNode); - - // Then check the signature - const isValid = verifier.checkSignature(signedXml); - expect(isValid).to.be.true; + // Verify that the signature is valid + expect(checkSignature(signedXml, doc)).to.be.true; }); it("should allow signing Object elements within the Signature", function () { - // Create a simple XML document to sign const xml = ''; - // Create a SignedXml instance with custom objects const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", objects: [ { content: "Test data in Object element", @@ -603,34 +458,20 @@ describe("Object support in XML signatures", function () { ], }); - // Set up the signature - sig.privateKey = fs.readFileSync("./test/static/client.pem"); - - // Add a reference to the document element 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#"], }); - // Add a reference to the Object element by its ID sig.addReference({ xpath: "//*[@Id='object1']", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); - // Set required algorithms - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; - - // Compute the signature sig.computeSignature(xml); - - // Get the signed XML const signedXml = sig.getSignedXml(); - - // Parse the signed XML const doc = new xmldom.DOMParser().parseFromString(signedXml); // Verify that the ds:Object element exists @@ -648,21 +489,45 @@ describe("Object support in XML signatures", function () { isDomNode.assertIsArrayOfNodes(objectReference); expect(objectReference.length).to.equal(1); - // Verify that the signature is still valid - const verifier = new SignedXml(); - verifier.publicCert = fs.readFileSync("./test/static/client_public.pem"); + // Verify that the signature is valid + expect(checkSignature(signedXml, doc)).to.be.true; + }); - // First load the signature - const signatureNode = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", - doc, - ); - isDomNode.assertIsNodeLike(signatureNode); - verifier.loadSignature(signatureNode); + it("should handle inclusiveNamespacesPrefixList in object reference", () => { + const xml = ""; - // Then check the signature - const isValid = verifier.checkSignature(signedXml); - expect(isValid).to.be.true; + const sig = new SignedXml({ + privateKey: privateKey, + signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + objects: [ + { + content: + "Content", + attributes: { + Id: "object1", + }, + }, + ], + }); + + sig.addReference({ + xpath: "//*[local-name(.)='Object']", + inclusiveNamespacesPrefixList: ["ns1", "ns2"], + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + + sig.computeSignature(xml); + const signedXml = sig.getSignedXml(); + + // Verify that the Object element is present + expect(signedXml).to.include('text`; const sig = new SignedXml({ - publicCert: publicCert, - privateKey: privateKey, + publicCert, + privateKey, canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", objects: [ @@ -729,18 +591,8 @@ describe("XAdES Object support in XML signatures", function () { }); const signedXml = sig.getSignedXml(); - const signedDoc = new xmldom.DOMParser().parseFromString(signedXml); - const signatureNode = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", - signedDoc, - ); - isDomNode.assertIsNodeLike(signatureNode); - const verifier = new SignedXml({ - publicCert: publicCert, - }); - verifier.loadSignature(signatureNode); - const isValid = verifier.checkSignature(signedXml); - expect(isValid).to.be.true; + // Verify that the signature is valid + expect(checkSignature(signedXml)).to.be.true; }); }); diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index 94aeab4..16b66e1 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -1317,4 +1317,22 @@ describe("Signature unit tests", function () { "Reference element should have the correct Type attribute value", ).to.equal("http://www.w3.org/2000/09/xmldsig#Object"); }); + + it("should throw if xpath matches no nodes", () => { + const sig = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + + sig.addReference({ + xpath: "//definitelyNotThere", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + + expect(() => sig.computeSignature("")).to.throw( + /the following xpath cannot be signed because it was not found/, + ); + }); }); From e761453dab34b7c55bc1045645d29f4cbbec11b8 Mon Sep 17 00:00:00 2001 From: shunkica Date: Fri, 15 Aug 2025 19:30:51 +0200 Subject: [PATCH 11/17] tests: further refactoring of tests --- test/signature-object-tests.spec.ts | 220 +++++++++++----------------- 1 file changed, 87 insertions(+), 133 deletions(-) diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index ae46a9d..3585f14 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -106,7 +106,8 @@ describe("Object support in XML signatures", function () { // Verify that no Object elements exist const objectNodesWithNull = xpath.select("//*[local-name(.)='Object']", docWithNull); - expect(objectNodesWithNull).to.be.an("array").that.is.empty; + isDomNode.assertIsArrayOfNodes(objectNodesWithNull); + expect(objectNodesWithNull.length).to.equal(0); // Test with empty array objects const sigWithEmpty = new SignedXml({ @@ -128,7 +129,8 @@ describe("Object support in XML signatures", function () { // Verify that no Object elements exist const objectNodesWithEmpty = xpath.select("//*[local-name(.)='Object']", docWithEmpty); - expect(objectNodesWithEmpty).to.be.an("array").that.is.empty; + isDomNode.assertIsArrayOfNodes(objectNodesWithEmpty); + expect(objectNodesWithEmpty.length).to.equal(0); }); it("should handle Object with Encoding attribute", function () { @@ -177,137 +179,80 @@ describe("Object support in XML signatures", function () { expect(checkSignature(signedXml, signedDoc)).to.be.true; }); - it("should sign Object with SHA256 digest algorithm", function () { - const xml = ''; - - const sig = new SignedXml({ - privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + [ + { + name: "SHA256", signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", - objects: [ - { - content: "Test data for SHA256 digest", - attributes: { - Id: "object1", - MimeType: "text/xml", - }, - }, - ], - }); - - sig.addReference({ - xpath: "//*[local-name(.)='x']", digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], - }); - - sig.addReference({ - xpath: "//*[@Id='object1']", - digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], - }); - - sig.computeSignature(xml); - const signedXml = sig.getSignedXml(); - - const doc = new xmldom.DOMParser().parseFromString(signedXml); - - // Verify that the ds:Object element exists - const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); - isDomNode.assertIsArrayOfNodes(objectNodes); - expect(objectNodes.length).to.equal(1); - - // Verify that there are two Reference elements - const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc); - isDomNode.assertIsArrayOfNodes(referenceNodes); - expect(referenceNodes.length).to.equal(2); - - // Verify that the references use SHA256 - const digestMethodNodes = xpath.select("//*[local-name(.)='DigestMethod']", doc); - isDomNode.assertIsArrayOfNodes(digestMethodNodes); - - for (const digestMethod of digestMethodNodes) { - isDomNode.assertIsElementNode(digestMethod); - expect(digestMethod.getAttribute("Algorithm")).to.equal( - "http://www.w3.org/2001/04/xmlenc#sha256", - ); - } - - // Verify that the signature method is RSA-SHA256 - const signatureMethod = xpath.select1("//*[local-name(.)='SignatureMethod']", doc); - isDomNode.assertIsElementNode(signatureMethod); - expect(signatureMethod.getAttribute("Algorithm")).to.equal( - "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", - ); - - // Verify that the signature is valid - expect(checkSignature(signedXml, doc)).to.be.true; - }); - - it("should sign Object with SHA512 digest algorithm and RSA-SHA512 signature", function () { - const xml = ''; - - const sig = new SignedXml({ - privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + }, + { + name: "SHA512", signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", - objects: [ - { - content: "Test data for SHA512 digest", - attributes: { - Id: "object1", - MimeType: "text/xml", - }, - }, - ], - }); - - sig.addReference({ - xpath: "//*[local-name(.)='x']", digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha512", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], - }); - - sig.addReference({ - xpath: "//*[@Id='object1']", - digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha512", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }, + ].forEach(({ name, signatureAlgorithm, digestAlgorithm }) => { + it(`should sign Object with ${name} digest algorithm and RSA-${name} signature`, () => { + const xml = ''; + + const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm, + objects: [ + { + content: "Test data for SHA256 digest", + attributes: { + Id: "object1", + MimeType: "text/xml", + }, + }, + ], + }); + + sig.addReference({ + xpath: "//*[local-name(.)='x']", + digestAlgorithm, + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + + sig.addReference({ + xpath: "//*[@Id='object1']", + digestAlgorithm, + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + + sig.computeSignature(xml); + const signedXml = sig.getSignedXml(); + + const doc = new xmldom.DOMParser().parseFromString(signedXml); + + // Verify that the ds:Object element exists + const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); + isDomNode.assertIsArrayOfNodes(objectNodes); + expect(objectNodes.length).to.equal(1); + + // Verify that there are two Reference elements + const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc); + isDomNode.assertIsArrayOfNodes(referenceNodes); + expect(referenceNodes.length).to.equal(2); + + // Verify that the references use SHA256 + const digestMethodNodes = xpath.select("//*[local-name(.)='DigestMethod']", doc); + isDomNode.assertIsArrayOfNodes(digestMethodNodes); + + for (const digestMethod of digestMethodNodes) { + isDomNode.assertIsElementNode(digestMethod); + expect(digestMethod.getAttribute("Algorithm")).to.equal(digestAlgorithm); + } + + // Verify that the signature method is RSA-SHA256 + const signatureMethod = xpath.select1("//*[local-name(.)='SignatureMethod']", doc); + isDomNode.assertIsElementNode(signatureMethod); + expect(signatureMethod.getAttribute("Algorithm")).to.equal(signatureAlgorithm); + + // Verify that the signature is valid + expect(checkSignature(signedXml, doc)).to.be.true; }); - - sig.computeSignature(xml); - const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); - - // Verify that the ds:Object element exists - const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); - isDomNode.assertIsArrayOfNodes(objectNodes); - expect(objectNodes.length).to.equal(1); - - // Verify that there are two Reference elements - const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc); - isDomNode.assertIsArrayOfNodes(referenceNodes); - expect(referenceNodes.length).to.equal(2); - - // Verify that the references use SHA512 - const digestMethodNodes = xpath.select("//*[local-name(.)='DigestMethod']", doc); - isDomNode.assertIsArrayOfNodes(digestMethodNodes); - - for (const digestMethod of digestMethodNodes) { - isDomNode.assertIsElementNode(digestMethod); - expect(digestMethod.getAttribute("Algorithm")).to.equal( - "http://www.w3.org/2001/04/xmlenc#sha512", - ); - } - - // Verify that the signature method is RSA-SHA512 - const signatureMethod = xpath.select1("//*[local-name(.)='SignatureMethod']", doc); - isDomNode.assertIsElementNode(signatureMethod); - expect(signatureMethod.getAttribute("Algorithm")).to.equal( - "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", - ); - - // Verify that the signature is valid - expect(checkSignature(signedXml, doc)).to.be.true; }); it("should sign Object with C14N canonicalization algorithm", function () { @@ -493,7 +438,7 @@ describe("Object support in XML signatures", function () { expect(checkSignature(signedXml, doc)).to.be.true; }); - it("should handle inclusiveNamespacesPrefixList in object reference", () => { + it("should handle inclusiveNamespacesPrefixList and detect Id in object reference", () => { const xml = ""; const sig = new SignedXml({ @@ -520,14 +465,23 @@ describe("Object support in XML signatures", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); + const signedDoc = new xmldom.DOMParser().parseFromString(signedXml); + + // Verify that the InclusiveNamespaces element is present + const elInclusiveNamespaces = xpath.select1( + "//*[local-name(.)='InclusiveNamespaces' and namespace-uri(.)='http://www.w3.org/2001/10/xml-exc-c14n#']", + signedDoc, + ); + isDomNode.assertIsElementNode(elInclusiveNamespaces); + expect(elInclusiveNamespaces.getAttribute("PrefixList")).to.equal("ns1 ns2"); - // Verify that the Object element is present - expect(signedXml).to.include(' Date: Fri, 15 Aug 2025 20:00:27 +0200 Subject: [PATCH 12/17] tests: remove redundant test and improve xades test --- test/signature-object-tests.spec.ts | 91 ++++++++++++----------------- 1 file changed, 36 insertions(+), 55 deletions(-) diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index 3585f14..768fb91 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -324,7 +324,7 @@ describe("Object support in XML signatures", function () { expect(checkSignature(signedXml, doc)).to.be.true; }); - it("should add a reference to an Object element", function () { + it("should add and sign references to Object elements within the Signature", function () { const xml = ''; const sig = new SignedXml({ @@ -385,59 +385,6 @@ describe("Object support in XML signatures", function () { expect(checkSignature(signedXml, doc)).to.be.true; }); - it("should allow signing Object elements within the Signature", function () { - const xml = ''; - - const sig = new SignedXml({ - privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", - objects: [ - { - content: "Test data in Object element", - attributes: { - Id: "object1", - MimeType: "text/xml", - }, - }, - ], - }); - - 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.addReference({ - xpath: "//*[@Id='object1']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], - }); - - sig.computeSignature(xml); - const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); - - // Verify that the ds:Object element exists - const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); - isDomNode.assertIsArrayOfNodes(objectNodes); - expect(objectNodes.length).to.equal(1); - - // Verify that there are two Reference elements - const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc); - isDomNode.assertIsArrayOfNodes(referenceNodes); - expect(referenceNodes.length).to.equal(2); - - // Verify that one of the references points to the Object - const objectReference = xpath.select("//*[local-name(.)='Reference' and @URI='#object1']", doc); - isDomNode.assertIsArrayOfNodes(objectReference); - expect(objectReference.length).to.equal(1); - - // Verify that the signature is valid - expect(checkSignature(signedXml, doc)).to.be.true; - }); - it("should handle inclusiveNamespacesPrefixList and detect Id in object reference", () => { const xml = ""; @@ -545,8 +492,42 @@ describe("XAdES Object support in XML signatures", function () { }); const signedXml = sig.getSignedXml(); + const signedDoc = new xmldom.DOMParser().parseFromString(signedXml); + + // ds:Signature has the expected Id + const elSig = xpath.select1("//*[local-name(.)='Signature']", signedDoc); + isDomNode.assertIsElementNode(elSig); + expect(elSig.getAttribute("Id")).to.equal(signatureId); + + // xades:QualifyingProperties targets the signature Id + const elQP = xpath.select1("//*[local-name(.)='QualifyingProperties']", signedDoc); + isDomNode.assertIsElementNode(elQP); + expect(elQP.getAttribute("Target")).to.equal(`#${signatureId}`); + + // xades:SignedProperties has the expected Id + const elSP = xpath.select1("//*[local-name(.)='SignedProperties']", signedDoc); + isDomNode.assertIsElementNode(elSP); + expect(elSP.getAttribute("Id")).to.equal(signedPropertiesId); + + // Reference for SignedProperties exists with correct @Type and @URI + const elSPRef = xpath.select1( + "//*[local-name(.)='SignedInfo']/*[local-name(.)='Reference' and @Type='http://uri.etsi.org/01903#SignedProperties']", + signedDoc, + ); + isDomNode.assertIsElementNode(elSPRef); + expect(elSPRef.getAttribute("URI")).to.equal(`#${signedPropertiesId}`); + + // DigestMethod for SignedProperties is SHA-256 + const elSPDigestMethod = xpath.select1( + `//*[local-name(.)='SignedInfo']/*[local-name(.)='Reference' and @URI='#${signedPropertiesId}']/*[local-name(.)='DigestMethod']`, + signedDoc, + ) as Element; + isDomNode.assertIsElementNode(elSPDigestMethod); + expect(elSPDigestMethod.getAttribute("Algorithm")).to.equal( + "http://www.w3.org/2001/04/xmlenc#sha256", + ); // Verify that the signature is valid - expect(checkSignature(signedXml)).to.be.true; + expect(checkSignature(signedXml, signedDoc)).to.be.true; }); }); From 3b5893de3cd19fe1a91d12427608a86aaaa0b603 Mon Sep 17 00:00:00 2001 From: shunkica Date: Sat, 16 Aug 2025 14:57:55 +0200 Subject: [PATCH 13/17] refactor: rewrite tests, use namespaces for select, remove redundant testing --- test/signature-object-tests.spec.ts | 465 ++++++++++++---------------- 1 file changed, 192 insertions(+), 273 deletions(-) diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index 768fb91..c03aeed 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -1,5 +1,5 @@ import * as fs from "fs"; -import { expect } from "chai"; +import { expect, assert } from "chai"; import * as xpath from "xpath"; import * as xmldom from "@xmldom/xmldom"; import * as isDomNode from "@xmldom/is-dom-node"; @@ -9,23 +9,37 @@ import { Sha256 } from "../src/hash-algorithms"; const privateKey = fs.readFileSync("./test/static/client.pem", "utf-8"); const publicCert = fs.readFileSync("./test/static/client_public.pem", "utf-8"); const publicCertDer = fs.readFileSync("./test/static/client_public.der"); - -const checkSignature = (signedXml: string, signedDoc?: Document) => { - if (!signedDoc) { - signedDoc = new xmldom.DOMParser().parseFromString(signedXml); - } +const selectNs = (expression: string, node: Node, ns?: Record) => + xpath.useNamespaces({ + ds: "http://www.w3.org/2000/09/xmldsig#", + xades: "http://uri.etsi.org/01903/v1.3.2#", + ...ns, + })(expression, node, false); +const select1Ns = (expression: string, node: Node, ns?: Record) => + xpath.useNamespaces({ + ds: "http://www.w3.org/2000/09/xmldsig#", + xades: "http://uri.etsi.org/01903/v1.3.2#", + ...ns, + })(expression, node, true); + +const checkSignature = (signedXml: string, signedDoc: Document) => { const verifier = new SignedXml({ publicCert }); - const signatureNode = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", - signedDoc, - ); + const signatureNode = select1Ns("//ds:Signature", signedDoc); isDomNode.assertIsNodeLike(signatureNode); verifier.loadSignature(signatureNode); - return verifier.checkSignature(signedXml); + const valid = verifier.checkSignature(signedXml); + + return { + valid, + errorMessage: verifier + .getReferences() + .flatMap((ref) => ref.validationError?.message || []) + .join(", "), + }; }; -describe("Object support in XML signatures", function () { - it("should add custom ds:Object elements to signature", function () { +describe("ds:Object support in XML signatures", function () { + it("should add custom ds:Object elements with attributes to the signature", function () { const xml = ''; const sig = new SignedXml({ @@ -38,6 +52,7 @@ describe("Object support in XML signatures", function () { attributes: { Id: "object1", MimeType: "text/xml", + Encoding: "", }, }, { @@ -47,6 +62,14 @@ describe("Object support in XML signatures", function () { MimeType: "text/plain", }, }, + { + content: Buffer.from("This is base64 encoded data").toString("base64"), + attributes: { + Id: "object3", + MimeType: "text/plain", + Encoding: "http://www.w3.org/2000/09/xmldsig#base64", + }, + }, ], }); @@ -60,31 +83,78 @@ describe("Object support in XML signatures", function () { const signedXml = sig.getSignedXml(); const doc = new xmldom.DOMParser().parseFromString(signedXml); - // Should have two Object elements - const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); + // Should have three Object elements + const objectNodes = selectNs("/root/ds:Signature/ds:Object", doc); isDomNode.assertIsArrayOfNodes(objectNodes); - expect(objectNodes.length).to.equal(2); + expect(objectNodes.length).to.equal(3); // Verify the first Object element - const firstObject = objectNodes[0]; - isDomNode.assertIsElementNode(firstObject); - expect(firstObject.getAttribute("Id")).to.equal("object1"); - expect(firstObject.getAttribute("MimeType")).to.equal("text/xml"); - expect(firstObject.textContent?.includes("Test data in Object element")).to.be.true; + const object1 = objectNodes[0]; + isDomNode.assertIsElementNode(object1); + expect(object1.getAttribute("Id")).to.equal("object1"); + expect(object1.getAttribute("MimeType")).to.equal("text/xml"); + expect(object1.hasAttribute("Encoding")).to.be.true; + expect(object1.getAttribute("Encoding")).to.equal(""); + const object1Data = select1Ns("ds:Data", object1); + isDomNode.assertIsElementNode(object1Data); + expect(object1Data.textContent).to.equal("Test data in Object element"); // Verify the second Object element - const secondObject = objectNodes[1]; - isDomNode.assertIsElementNode(secondObject); - expect(secondObject.getAttribute("Id")).to.equal("object2"); - expect(secondObject.getAttribute("MimeType")).to.equal("text/plain"); - expect(secondObject.textContent).to.equal("Plain text content"); + const object2 = objectNodes[1]; + isDomNode.assertIsElementNode(object2); + expect(object2.getAttribute("Id")).to.equal("object2"); + expect(object2.getAttribute("MimeType")).to.equal("text/plain"); + expect(object2.hasAttribute("Encoding")).to.be.false; + expect(object2.textContent).to.equal("Plain text content"); + + // Verify the third Object element + const object3 = objectNodes[2]; + isDomNode.assertIsElementNode(object3); + expect(object3.getAttribute("Id")).to.equal("object3"); + expect(object3.getAttribute("MimeType")).to.equal("text/plain"); + expect(object3.getAttribute("Encoding")).to.equal("http://www.w3.org/2000/09/xmldsig#base64"); + assert(object3.textContent); + expect(Buffer.from(object3.textContent, "base64").toString("utf-8")).to.equal( + "This is base64 encoded data", + ); + }); - // Verify that the signature is valid - expect(checkSignature(signedXml, doc)).to.be.true; + it("should have correct ds:Object namespace when there is no default namespace", function () { + const xml = ""; + + const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + objects: [ + { + content: "Test data", + attributes: { + Id: "object1", + MimeType: "text/plain", + }, + }, + ], + }); + + sig.addReference({ + xpath: "/*", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + + // When we add a prefix to the signature, there is no default namespace + sig.computeSignature(xml, { prefix: "ds" }); + const signedXml = sig.getSignedXml(); + const doc = new xmldom.DOMParser().parseFromString(signedXml); + + // Verify the namespace of the ds:Object element + const objectNode = select1Ns("/root/ds:Signature/ds:Object[@Id='object1']", doc); + isDomNode.assertIsElementNode(objectNode); }); it("should handle empty or undefined objects", function () { - const xml = ''; + const xml = ""; // Test with undefined objects const sigWithNull = new SignedXml({ @@ -95,7 +165,7 @@ describe("Object support in XML signatures", function () { }); sigWithNull.addReference({ - xpath: "//*[local-name(.)='x']", + xpath: "/*", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); @@ -105,7 +175,7 @@ describe("Object support in XML signatures", function () { const docWithNull = new xmldom.DOMParser().parseFromString(signedXmlWithNull); // Verify that no Object elements exist - const objectNodesWithNull = xpath.select("//*[local-name(.)='Object']", docWithNull); + const objectNodesWithNull = selectNs("//ds:Object", docWithNull); isDomNode.assertIsArrayOfNodes(objectNodesWithNull); expect(objectNodesWithNull.length).to.equal(0); @@ -118,7 +188,7 @@ describe("Object support in XML signatures", function () { }); sigWithEmpty.addReference({ - xpath: "//*[local-name(.)='x']", + xpath: "/*", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); @@ -128,32 +198,32 @@ describe("Object support in XML signatures", function () { const docWithEmpty = new xmldom.DOMParser().parseFromString(signedXmlWithEmpty); // Verify that no Object elements exist - const objectNodesWithEmpty = xpath.select("//*[local-name(.)='Object']", docWithEmpty); + const objectNodesWithEmpty = selectNs("//ds:Object", docWithEmpty); isDomNode.assertIsArrayOfNodes(objectNodesWithEmpty); expect(objectNodesWithEmpty.length).to.equal(0); }); - it("should handle Object with Encoding attribute", function () { - const xml = ''; + it("should handle Rerefence to Object", function () { + const xml = ""; const sig = new SignedXml({ - privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + privateKey: privateKey, signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", objects: [ { - content: "VGhpcyBpcyBiYXNlNjQgZW5jb2RlZCBkYXRh", // "This is base64 encoded data" + content: + "Content", attributes: { Id: "object1", - MimeType: "application/octet-stream", - Encoding: "http://www.w3.org/2000/09/xmldsig#base64", }, }, ], }); sig.addReference({ - xpath: "//*[local-name(.)='x']", + xpath: "//*[local-name(.)='Object' and @Id='object1']", + inclusiveNamespacesPrefixList: ["ns1", "ns2"], digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], }); @@ -162,109 +232,55 @@ describe("Object support in XML signatures", function () { const signedXml = sig.getSignedXml(); const signedDoc = new xmldom.DOMParser().parseFromString(signedXml); - // Verify that the Object element exists - const objectNodes = xpath.select("//*[local-name(.)='Object']", signedDoc); - isDomNode.assertIsArrayOfNodes(objectNodes); - expect(objectNodes.length).to.equal(1); - - // Verify the Object element - const object = objectNodes[0]; - isDomNode.assertIsElementNode(object); - expect(object.getAttribute("Id")).to.equal("object1"); - expect(object.getAttribute("MimeType")).to.equal("application/octet-stream"); - expect(object.getAttribute("Encoding")).to.equal("http://www.w3.org/2000/09/xmldsig#base64"); - expect(object.textContent).to.equal("VGhpcyBpcyBiYXNlNjQgZW5jb2RlZCBkYXRh"); - - // Verify that the signature is valid - expect(checkSignature(signedXml, signedDoc)).to.be.true; - }); + // Verify that there is exactly one ds:Reference + const referenceNodes = selectNs("/root/ds:Signature/ds:SignedInfo/ds:Reference", signedDoc); + isDomNode.assertIsArrayOfNodes(referenceNodes); + expect(referenceNodes.length).to.equal(1); + const referenceEl = referenceNodes[0]; + isDomNode.assertIsElementNode(referenceEl); + + // Verify that the Reference URI points to the Object + expect(referenceEl.getAttribute("URI")).to.equal("#object1"); + + // Verify that the Reference contains the correct Transform + const transformEl = select1Ns("ds:Transforms/ds:Transform", referenceEl); + isDomNode.assertIsElementNode(transformEl); + expect(transformEl.getAttribute("Algorithm")).to.equal( + "http://www.w3.org/2001/10/xml-exc-c14n#", + ); - [ - { - name: "SHA256", - signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", - digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", - }, - { - name: "SHA512", - signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", - digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha512", - }, - ].forEach(({ name, signatureAlgorithm, digestAlgorithm }) => { - it(`should sign Object with ${name} digest algorithm and RSA-${name} signature`, () => { - const xml = ''; - - const sig = new SignedXml({ - privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm, - objects: [ - { - content: "Test data for SHA256 digest", - attributes: { - Id: "object1", - MimeType: "text/xml", - }, - }, - ], - }); - - sig.addReference({ - xpath: "//*[local-name(.)='x']", - digestAlgorithm, - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], - }); - - sig.addReference({ - xpath: "//*[@Id='object1']", - digestAlgorithm, - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], - }); - - sig.computeSignature(xml); - const signedXml = sig.getSignedXml(); - - const doc = new xmldom.DOMParser().parseFromString(signedXml); - - // Verify that the ds:Object element exists - const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); - isDomNode.assertIsArrayOfNodes(objectNodes); - expect(objectNodes.length).to.equal(1); - - // Verify that there are two Reference elements - const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc); - isDomNode.assertIsArrayOfNodes(referenceNodes); - expect(referenceNodes.length).to.equal(2); - - // Verify that the references use SHA256 - const digestMethodNodes = xpath.select("//*[local-name(.)='DigestMethod']", doc); - isDomNode.assertIsArrayOfNodes(digestMethodNodes); - - for (const digestMethod of digestMethodNodes) { - isDomNode.assertIsElementNode(digestMethod); - expect(digestMethod.getAttribute("Algorithm")).to.equal(digestAlgorithm); - } - - // Verify that the signature method is RSA-SHA256 - const signatureMethod = xpath.select1("//*[local-name(.)='SignatureMethod']", doc); - isDomNode.assertIsElementNode(signatureMethod); - expect(signatureMethod.getAttribute("Algorithm")).to.equal(signatureAlgorithm); - - // Verify that the signature is valid - expect(checkSignature(signedXml, doc)).to.be.true; + // Verify that the InclusiveNamespacesPrefixList is set correctly + const inclusiveNamespacesEl = select1Ns("ec:InclusiveNamespaces", transformEl, { + ec: "http://www.w3.org/2001/10/xml-exc-c14n#", }); + isDomNode.assertIsElementNode(inclusiveNamespacesEl); + expect(inclusiveNamespacesEl.getAttribute("PrefixList")).to.equal("ns1 ns2"); + + // Verify that the Reference contains the correct DigestMethod + const digestMethodEl = select1Ns("ds:DigestMethod", referenceEl); + isDomNode.assertIsElementNode(digestMethodEl); + expect(digestMethodEl.getAttribute("Algorithm")).to.equal( + "http://www.w3.org/2000/09/xmldsig#sha1", + ); + + // Verify that the Reference contains a non-empty DigestValue + const digestValueEl = select1Ns("ds:DigestValue", referenceEl); + isDomNode.assertIsElementNode(digestValueEl); + expect(digestValueEl.textContent).to.not.be.empty; }); +}); - it("should sign Object with C14N canonicalization algorithm", function () { - const xml = ''; +describe("Valid signatures with ds:Object elements", function () { + it("should create valid signatures with NO references to ds:Object", function () { + const xml = ""; const sig = new SignedXml({ privateKey, - canonicalizationAlgorithm: "http://www.w3.org/TR/2001/REC-xml-c14n-20010315", + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", objects: [ { - content: "Test data for C14N canonicalization", + content: "Test data in Object element", attributes: { Id: "object1", MimeType: "text/xml", @@ -274,66 +290,35 @@ describe("Object support in XML signatures", function () { }); sig.addReference({ - xpath: "//*[local-name(.)='x']", + xpath: "/*", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"], - }); - - sig.addReference({ - xpath: "//*[@Id='object1']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"], + transforms: [ + "http://www.w3.org/2000/09/xmldsig#enveloped-signature", + "http://www.w3.org/2001/10/xml-exc-c14n#", + ], }); sig.computeSignature(xml); const signedXml = sig.getSignedXml(); const doc = new xmldom.DOMParser().parseFromString(signedXml); - // Verify that the ds:Object element exists - const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); - isDomNode.assertIsArrayOfNodes(objectNodes); - expect(objectNodes.length).to.equal(1); - - // Verify that there are two Reference elements - const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc); - isDomNode.assertIsArrayOfNodes(referenceNodes); - expect(referenceNodes.length).to.equal(2); - - // Verify that the transforms use C14N - const transforms = xpath.select( - "//*[local-name(.)='Reference']/*[local-name(.)='Transforms']/*[local-name(.)='Transform']", - doc, - ); - isDomNode.assertIsArrayOfNodes(transforms); - - for (const transform of transforms) { - isDomNode.assertIsElementNode(transform); - expect(transform.getAttribute("Algorithm")).to.equal( - "http://www.w3.org/TR/2001/REC-xml-c14n-20010315", - ); - } - - // Verify that the CanonicalizationMethod is C14N - const canonMethod = xpath.select1("//*[local-name(.)='CanonicalizationMethod']", doc); - isDomNode.assertIsElementNode(canonMethod); - expect(canonMethod.getAttribute("Algorithm")).to.equal( - "http://www.w3.org/TR/2001/REC-xml-c14n-20010315", - ); - // Verify that the signature is valid - expect(checkSignature(signedXml, doc)).to.be.true; + const { valid, errorMessage } = checkSignature(signedXml, doc); + expect(valid, errorMessage).to.be.true; }); - it("should add and sign references to Object elements within the Signature", function () { - const xml = ''; + it("should create valid signatures with references to ds:Object", () => { + const xml = ''; const sig = new SignedXml({ privateKey, canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + inclusiveNamespacesPrefixList: ["ns1", "ns2"], objects: [ { - content: "Test data in Object element", + content: + 'Test data in Object element', attributes: { Id: "object1", MimeType: "text/xml", @@ -343,92 +328,43 @@ describe("Object support in XML signatures", function () { }); sig.addReference({ - xpath: "//*[local-name(.)='x']", + xpath: "/*", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + transforms: [ + "http://www.w3.org/2000/09/xmldsig#enveloped-signature", + "http://www.w3.org/2001/10/xml-exc-c14n#", + ], }); sig.addReference({ - xpath: "//*[@Id='object1']", + xpath: "//*[local-name(.)='Object' and @Id='object1']", digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + transforms: [ + "http://www.w3.org/2000/09/xmldsig#enveloped-signature", + "http://www.w3.org/2001/10/xml-exc-c14n#", + ], + inclusiveNamespacesPrefixList: ["ns1", "ns3"], }); sig.computeSignature(xml); const signedXml = sig.getSignedXml(); const doc = new xmldom.DOMParser().parseFromString(signedXml); - // Verify that the ds:Object element exists - const objectNodes = xpath.select("//*[local-name(.)='Object']", doc); - isDomNode.assertIsArrayOfNodes(objectNodes); - expect(objectNodes.length).to.equal(1); - // Verify that there are two Reference elements - const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc); + const referenceNodes = selectNs("/ns1:root/ds:Signature/ds:SignedInfo/ds:Reference", doc, { + ns1: "uri:ns1", + }); isDomNode.assertIsArrayOfNodes(referenceNodes); expect(referenceNodes.length).to.equal(2); - // Verify that one of the references points to the Object - const objectReference = xpath.select("//*[local-name(.)='Reference' and @URI='#object1']", doc); - isDomNode.assertIsArrayOfNodes(objectReference); - expect(objectReference.length).to.equal(1); - - // Verify that the reference is actually in the SignedInfo section - const signedInfoReference = xpath.select( - "//*[local-name(.)='SignedInfo']/*[local-name(.)='Reference' and @URI='#object1']", - doc, - ); - isDomNode.assertIsArrayOfNodes(signedInfoReference); - expect(signedInfoReference.length).to.equal(1); + // Verify that the second Reference points to the ds:Object + const objectReference = referenceNodes[1]; + isDomNode.assertIsElementNode(objectReference); + expect(objectReference.getAttribute("URI")).to.equal("#object1"); // Verify that the signature is valid - expect(checkSignature(signedXml, doc)).to.be.true; - }); - - it("should handle inclusiveNamespacesPrefixList and detect Id in object reference", () => { - const xml = ""; - - const sig = new SignedXml({ - privateKey: privateKey, - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - objects: [ - { - content: - "Content", - attributes: { - Id: "object1", - }, - }, - ], - }); - - sig.addReference({ - xpath: "//*[local-name(.)='Object']", - inclusiveNamespacesPrefixList: ["ns1", "ns2"], - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], - }); - - sig.computeSignature(xml); - const signedXml = sig.getSignedXml(); - const signedDoc = new xmldom.DOMParser().parseFromString(signedXml); - - // Verify that the InclusiveNamespaces element is present - const elInclusiveNamespaces = xpath.select1( - "//*[local-name(.)='InclusiveNamespaces' and namespace-uri(.)='http://www.w3.org/2001/10/xml-exc-c14n#']", - signedDoc, - ); - isDomNode.assertIsElementNode(elInclusiveNamespaces); - expect(elInclusiveNamespaces.getAttribute("PrefixList")).to.equal("ns1 ns2"); - - // Verify that the Reference URI is correct - const elReference = xpath.select1("//*[local-name(.)='Reference']", signedDoc); - isDomNode.assertIsElementNode(elReference); - expect(elReference.getAttribute("URI")).to.equal("#object1"); - - // Verify that the signature is valid - expect(checkSignature(signedXml, signedDoc)).to.be.true; + const { valid, errorMessage } = checkSignature(signedXml, doc); + expect(valid, errorMessage).to.be.true; }); }); @@ -494,40 +430,23 @@ describe("XAdES Object support in XML signatures", function () { const signedXml = sig.getSignedXml(); const signedDoc = new xmldom.DOMParser().parseFromString(signedXml); - // ds:Signature has the expected Id - const elSig = xpath.select1("//*[local-name(.)='Signature']", signedDoc); + // ds:Signature exists and has the expected Id + const elSig = select1Ns(`/root/ds:Signature[@Id='${signatureId}']`, signedDoc); isDomNode.assertIsElementNode(elSig); - expect(elSig.getAttribute("Id")).to.equal(signatureId); - // xades:QualifyingProperties targets the signature Id - const elQP = xpath.select1("//*[local-name(.)='QualifyingProperties']", signedDoc); + // ds:Object/xades:QualifyingProperties exists within the signature + const elQP = select1Ns("ds:Object/xades:QualifyingProperties", elSig); isDomNode.assertIsElementNode(elQP); - expect(elQP.getAttribute("Target")).to.equal(`#${signatureId}`); - // xades:SignedProperties has the expected Id - const elSP = xpath.select1("//*[local-name(.)='SignedProperties']", signedDoc); - isDomNode.assertIsElementNode(elSP); - expect(elSP.getAttribute("Id")).to.equal(signedPropertiesId); - - // Reference for SignedProperties exists with correct @Type and @URI - const elSPRef = xpath.select1( - "//*[local-name(.)='SignedInfo']/*[local-name(.)='Reference' and @Type='http://uri.etsi.org/01903#SignedProperties']", - signedDoc, + // The Reference to SignedProperties exists and has the correct URI and Type + const elSPRef = select1Ns( + `ds:SignedInfo/ds:Reference[@URI='#${signedPropertiesId}' and @Type='http://uri.etsi.org/01903#SignedProperties']`, + elSig, ); isDomNode.assertIsElementNode(elSPRef); - expect(elSPRef.getAttribute("URI")).to.equal(`#${signedPropertiesId}`); - - // DigestMethod for SignedProperties is SHA-256 - const elSPDigestMethod = xpath.select1( - `//*[local-name(.)='SignedInfo']/*[local-name(.)='Reference' and @URI='#${signedPropertiesId}']/*[local-name(.)='DigestMethod']`, - signedDoc, - ) as Element; - isDomNode.assertIsElementNode(elSPDigestMethod); - expect(elSPDigestMethod.getAttribute("Algorithm")).to.equal( - "http://www.w3.org/2001/04/xmlenc#sha256", - ); // Verify that the signature is valid - expect(checkSignature(signedXml, signedDoc)).to.be.true; + const { valid, errorMessage } = checkSignature(signedXml, signedDoc); + expect(valid, errorMessage).to.be.true; }); }); From 230d4c5d2db65db93fdc6149bec562da02ba1460 Mon Sep 17 00:00:00 2001 From: shunkica Date: Sat, 16 Aug 2025 14:58:30 +0200 Subject: [PATCH 14/17] fix: process signature references after signature has been appended to the document --- src/signed-xml.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/signed-xml.ts b/src/signed-xml.ts index a2e258f..860485c 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -991,9 +991,6 @@ export class SignedXml { const dummySignatureWrapper = `${signatureXml}`; const nodeXml = new xmldom.DOMParser().parseFromString(dummySignatureWrapper); - // Process any signature references after the signature has been created - this.processSignatureReferences(nodeXml, prefix); - // Because we are using a dummy wrapper hack described above, we know there will be a `firstChild` // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const signatureDoc = nodeXml.documentElement.firstChild!; @@ -1032,6 +1029,9 @@ export class SignedXml { referenceNode.parentNode.insertBefore(signatureDoc, referenceNode.nextSibling); } + // Process any signature references after the signature has been added to the document + this.processSignatureReferences(doc, prefix); + this.signatureNode = signatureDoc; const signedInfoNodes = utils.findChildren(this.signatureNode, "SignedInfo"); if (signedInfoNodes.length === 0) { From 04a1de4ded5db015991eec8dfb9912bd9bc16ab4 Mon Sep 17 00:00:00 2001 From: shunkica Date: Sun, 17 Aug 2025 10:46:34 +0200 Subject: [PATCH 15/17] fix: prevent signature self-reference --- src/signed-xml.ts | 69 +++++++++++++++-------------- src/utils.ts | 18 ++++++++ test/signature-object-tests.spec.ts | 68 ++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 34 deletions(-) diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 860485c..2c02105 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -26,6 +26,7 @@ import * as execC14n from "./exclusive-canonicalization"; import * as hashAlgorithms from "./hash-algorithms"; import * as signatureAlgorithms from "./signature-algorithms"; import * as utils from "./utils"; +import { isDescendantOf } from "./utils"; export class SignedXml { idMode?: "wssecurity"; @@ -992,8 +993,9 @@ export class SignedXml { const nodeXml = new xmldom.DOMParser().parseFromString(dummySignatureWrapper); // Because we are using a dummy wrapper hack described above, we know there will be a `firstChild` + // and that it will be an `Element` node. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const signatureDoc = nodeXml.documentElement.firstChild!; + const signatureElem = nodeXml.documentElement.firstChild! as Element; const referenceNode = xpath.select1(location.reference, doc); @@ -1010,29 +1012,29 @@ export class SignedXml { } if (location.action === "append") { - referenceNode.appendChild(signatureDoc); + referenceNode.appendChild(signatureElem); } else if (location.action === "prepend") { - referenceNode.insertBefore(signatureDoc, referenceNode.firstChild); + referenceNode.insertBefore(signatureElem, referenceNode.firstChild); } else if (location.action === "before") { if (referenceNode.parentNode == null) { throw new Error( "`location.reference` refers to the root node (by default), so we can't insert `before`", ); } - referenceNode.parentNode.insertBefore(signatureDoc, referenceNode); + referenceNode.parentNode.insertBefore(signatureElem, referenceNode); } else if (location.action === "after") { if (referenceNode.parentNode == null) { throw new Error( "`location.reference` refers to the root node (by default), so we can't insert `after`", ); } - referenceNode.parentNode.insertBefore(signatureDoc, referenceNode.nextSibling); + referenceNode.parentNode.insertBefore(signatureElem, referenceNode.nextSibling); } // Process any signature references after the signature has been added to the document - this.processSignatureReferences(doc, prefix); + this.processSignatureReferences(doc, signatureElem, prefix); - this.signatureNode = signatureDoc; + this.signatureNode = signatureElem; const signedInfoNodes = utils.findChildren(this.signatureNode, "SignedInfo"); if (signedInfoNodes.length === 0) { const err3 = new Error("could not find SignedInfo element in the message"); @@ -1052,8 +1054,8 @@ export class SignedXml { callback(err); } else { this.signatureValue = signature || ""; - signatureDoc.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling); - this.signatureXml = signatureDoc.toString(); + signatureElem.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling); + this.signatureXml = signatureElem.toString(); this.signedXml = doc.toString(); callback(null, this); } @@ -1061,8 +1063,8 @@ export class SignedXml { } else { // Synchronous flow this.calculateSignatureValue(doc); - signatureDoc.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling); - this.signatureXml = signatureDoc.toString(); + signatureElem.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling); + this.signatureXml = signatureElem.toString(); this.signedXml = doc.toString(); } } @@ -1325,7 +1327,7 @@ export class SignedXml { * Process references that weren't found in the initial document * This is called after the initial signature has been created to handle references to signature elements */ - private processSignatureReferences(signatureDoc: Document, prefix?: string) { + private processSignatureReferences(doc: Document, signatureElem: Element, prefix?: string) { // Get unprocessed references const unprocessedReferences = this.references.filter((ref) => !ref.wasProcessed); if (unprocessedReferences.length === 0) { @@ -1338,8 +1340,8 @@ export class SignedXml { // Find the SignedInfo element to append to const signedInfoNode = xpath.select1( - `.//*[local-name(.)='SignedInfo']`, - signatureDoc, + `./*[local-name(.)='SignedInfo']`, + signatureElem, ) as Element; if (!signedInfoNode) { throw new Error("Could not find SignedInfo element in signature"); @@ -1347,7 +1349,7 @@ export class SignedXml { // Process each unprocessed reference for (const ref of unprocessedReferences) { - const nodes = xpath.selectWithResolver(ref.xpath ?? "", signatureDoc, this.namespaceResolver); + const nodes = xpath.selectWithResolver(ref.xpath ?? "", doc, this.namespaceResolver); if (!utils.isArrayHasLength(nodes)) { throw new Error( @@ -1357,8 +1359,19 @@ export class SignedXml { // Process the reference for (const node of nodes) { + // Must not be a reference to Signature, SignedInfo, or a child of SignedInfo + if ( + node === signatureElem || + node === signedInfoNode || + isDescendantOf(node, signedInfoNode) + ) { + throw new Error( + `Cannot sign a reference to the Signature or SignedInfo element itself: ${ref.xpath}`, + ); + } + // Create the reference element directly using DOM methods to avoid namespace issues - const referenceElem = signatureDoc.createElementNS( + const referenceElem = signatureElem.ownerDocument.createElementNS( signatureNamespace, `${prefix}Reference`, ); @@ -1378,21 +1391,15 @@ export class SignedXml { referenceElem.setAttribute("Type", ref.type); } - const transformsElem = signatureDoc.createElementNS( - signatureNamespace, - `${prefix}Transforms`, - ); + const transformsElem = doc.createElementNS(signatureNamespace, `${prefix}Transforms`); for (const trans of ref.transforms || []) { const transform = this.findCanonicalizationAlgorithm(trans); - const transformElem = signatureDoc.createElementNS( - signatureNamespace, - `${prefix}Transform`, - ); + const transformElem = doc.createElementNS(signatureNamespace, `${prefix}Transform`); transformElem.setAttribute("Algorithm", transform.getAlgorithmName()); if (utils.isArrayHasLength(ref.inclusiveNamespacesPrefixList)) { - const inclusiveNamespacesElem = signatureDoc.createElementNS( + const inclusiveNamespacesElem = doc.createElementNS( transform.getAlgorithmName(), "InclusiveNamespaces", ); @@ -1407,21 +1414,15 @@ export class SignedXml { } // Get the canonicalized XML - const canonXml = this.getCanonReferenceXml(signatureDoc, ref, node); + const canonXml = this.getCanonReferenceXml(doc, ref, node); // Get the digest algorithm and compute the digest value const digestAlgorithm = this.findHashAlgorithm(ref.digestAlgorithm); - const digestMethodElem = signatureDoc.createElementNS( - signatureNamespace, - `${prefix}DigestMethod`, - ); + const digestMethodElem = doc.createElementNS(signatureNamespace, `${prefix}DigestMethod`); digestMethodElem.setAttribute("Algorithm", digestAlgorithm.getAlgorithmName()); - const digestValueElem = signatureDoc.createElementNS( - signatureNamespace, - `${prefix}DigestValue`, - ); + const digestValueElem = doc.createElementNS(signatureNamespace, `${prefix}DigestValue`); digestValueElem.textContent = digestAlgorithm.getHash(canonXml); referenceElem.appendChild(transformsElem); diff --git a/src/utils.ts b/src/utils.ts index 6098286..466b252 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -313,3 +313,21 @@ export function validateDigestValue(digest, expectedDigest) { return true; } + +// Check if the given node is descendant of the given parent node +export function isDescendantOf(node: Node, parent: Node): boolean { + if (!node || !parent) { + return false; + } + + let currentNode: Node | null = node.parentNode; + + while (currentNode) { + if (currentNode === parent) { + return true; + } + currentNode = currentNode.parentNode; + } + + return false; +} diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index c03aeed..f9fefd4 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -450,3 +450,71 @@ describe("XAdES Object support in XML signatures", function () { expect(valid, errorMessage).to.be.true; }); }); + +describe("Signature self-reference prevention", function () { + it("should not allow self-referencing the Signature element", function () { + const xml = ""; + + const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + }); + + sig.addReference({ + xpath: ".//*[local-name(.)='Signature']", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + + expect(() => { + sig.computeSignature(xml); + }).to.throw(/Cannot sign a reference to the Signature or SignedInfo element itself/); + }); + + it("should not allow self-referencing the SignedInfo element", function () { + const xml = ""; + + const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + }); + + sig.addReference({ + xpath: ".//*[local-name(.)='SignedInfo']", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + + expect(() => { + sig.computeSignature(xml); + }).to.throw(/Cannot sign a reference to the Signature or SignedInfo element itself/); + }); + + it("should not allow signing children of the SignedInfo element", function () { + const xml = ""; + + const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + }); + + sig.addReference({ + xpath: "/*", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + + sig.addReference({ + xpath: ".//*[local-name(.)='Reference']/*", + digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + }); + + expect(() => { + sig.computeSignature(xml); + }).to.throw(/Cannot sign a reference to the Signature or SignedInfo element itself/); + }); +}); From 38c552bc2b09888f80c3b04e16bb99894f2d72d9 Mon Sep 17 00:00:00 2001 From: shunkica Date: Sun, 17 Aug 2025 11:16:43 +0200 Subject: [PATCH 16/17] refactor: utils import, make it clear why we use signatureDoc in processSignatureReferences --- src/signed-xml.ts | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 2c02105..9e6a2e5 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -26,7 +26,6 @@ import * as execC14n from "./exclusive-canonicalization"; import * as hashAlgorithms from "./hash-algorithms"; import * as signatureAlgorithms from "./signature-algorithms"; import * as utils from "./utils"; -import { isDescendantOf } from "./utils"; export class SignedXml { idMode?: "wssecurity"; @@ -1347,6 +1346,10 @@ export class SignedXml { throw new Error("Could not find SignedInfo element in signature"); } + // Signature document is technically the same document as the one we are signing, + // but we will extract it here for clarity (and also make it support detached signatures in the future) + const signatureDoc = signatureElem.ownerDocument; + // Process each unprocessed reference for (const ref of unprocessedReferences) { const nodes = xpath.selectWithResolver(ref.xpath ?? "", doc, this.namespaceResolver); @@ -1363,7 +1366,7 @@ export class SignedXml { if ( node === signatureElem || node === signedInfoNode || - isDescendantOf(node, signedInfoNode) + utils.isDescendantOf(node, signedInfoNode) ) { throw new Error( `Cannot sign a reference to the Signature or SignedInfo element itself: ${ref.xpath}`, @@ -1371,7 +1374,7 @@ export class SignedXml { } // Create the reference element directly using DOM methods to avoid namespace issues - const referenceElem = signatureElem.ownerDocument.createElementNS( + const referenceElem = signatureDoc.createElementNS( signatureNamespace, `${prefix}Reference`, ); @@ -1391,15 +1394,21 @@ export class SignedXml { referenceElem.setAttribute("Type", ref.type); } - const transformsElem = doc.createElementNS(signatureNamespace, `${prefix}Transforms`); + const transformsElem = signatureDoc.createElementNS( + signatureNamespace, + `${prefix}Transforms`, + ); for (const trans of ref.transforms || []) { const transform = this.findCanonicalizationAlgorithm(trans); - const transformElem = doc.createElementNS(signatureNamespace, `${prefix}Transform`); + const transformElem = signatureDoc.createElementNS( + signatureNamespace, + `${prefix}Transform`, + ); transformElem.setAttribute("Algorithm", transform.getAlgorithmName()); if (utils.isArrayHasLength(ref.inclusiveNamespacesPrefixList)) { - const inclusiveNamespacesElem = doc.createElementNS( + const inclusiveNamespacesElem = signatureDoc.createElementNS( transform.getAlgorithmName(), "InclusiveNamespaces", ); @@ -1419,10 +1428,16 @@ export class SignedXml { // Get the digest algorithm and compute the digest value const digestAlgorithm = this.findHashAlgorithm(ref.digestAlgorithm); - const digestMethodElem = doc.createElementNS(signatureNamespace, `${prefix}DigestMethod`); + const digestMethodElem = signatureDoc.createElementNS( + signatureNamespace, + `${prefix}DigestMethod`, + ); digestMethodElem.setAttribute("Algorithm", digestAlgorithm.getAlgorithmName()); - const digestValueElem = doc.createElementNS(signatureNamespace, `${prefix}DigestValue`); + const digestValueElem = signatureDoc.createElementNS( + signatureNamespace, + `${prefix}DigestValue`, + ); digestValueElem.textContent = digestAlgorithm.getHash(canonXml); referenceElem.appendChild(transformsElem); From aa9b086ecf7e0d36de8594fe54c557b9a992ca05 Mon Sep 17 00:00:00 2001 From: shunkica Date: Sun, 17 Aug 2025 11:19:35 +0200 Subject: [PATCH 17/17] mark references as processed (however this flag could be removed if issue #512 is resolved) --- src/signed-xml.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 9e6a2e5..c068656 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -1447,6 +1447,7 @@ export class SignedXml { // Append the reference element to SignedInfo signedInfoNode.appendChild(referenceElem); } + ref.wasProcessed = true; } }