From 3510993f0220d4752afdef7f8838487467e4e3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Kalm=C3=A1r?= Date: Sat, 12 Jul 2025 14:18:12 +0200 Subject: [PATCH 1/2] Respect idAttribute when generating signatures. Fixes #33 --- src/signed-xml.ts | 2 +- test/signature-unit-tests.spec.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 05dae417..a02f18ac 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -1195,7 +1195,7 @@ export class SignedXml { id, ); } else { - node.setAttribute("Id", id); + node.setAttribute(this.idAttributes[0], id); } return id; diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index baa382db..91ba0517 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -17,9 +17,9 @@ describe("Signature unit tests", function () { expect(node.length, `xpath ${xpathArg} not found`).to.equal(1); } - function verifyAddsId(mode, nsMode) { + function verifyAddsId(mode, nsMode, idAttribute:string|undefined = undefined) { const xml = ''; - const sig = new SignedXml({ idMode: mode }); + const sig = new SignedXml({ idMode: mode, idAttribute }); sig.privateKey = fs.readFileSync("./test/static/client.pem"); sig.addReference({ @@ -46,7 +46,7 @@ describe("Signature unit tests", function () { const op = nsMode === "equal" ? "=" : "!="; - const xpathArg = `//*[local-name(.)='{elem}' and '_{id}' = @*[local-name(.)='Id' and namespace-uri(.)${op}'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd']]`; + const xpathArg = `//*[local-name(.)='{elem}' and '_{id}' = @*[local-name(.)='${idAttribute || 'Id'}' and namespace-uri(.)${op}'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd']]`; //verify each of the signed nodes now has an "Id" attribute with the right value nodeExists(doc, xpathArg.replace("{id}", "0").replace("{elem}", "x")); @@ -54,6 +54,10 @@ describe("Signature unit tests", function () { nodeExists(doc, xpathArg.replace("{id}", "2").replace("{elem}", "w")); } + it("signer adds increasing different id attributes to elements with custom idAttribute", function () { + verifyAddsId(null, "different", 'myIdAttribute'); + }); + it("signer adds increasing different id attributes to elements", function () { verifyAddsId(null, "different"); }); From 193a6a67cf2d1b7dbe5bee7cfeed1ec73f2b82d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Kalm=C3=A1r?= Date: Sun, 2 Nov 2025 15:49:39 +0100 Subject: [PATCH 2/2] Add namespaced id support --- src/constants.ts | 18 +++++++++++++ src/signed-xml.ts | 44 +++++++++++++++---------------- src/types.ts | 14 +++++++++- src/utils.ts | 13 ++++++--- test/signature-unit-tests.spec.ts | 8 +++--- 5 files changed, 68 insertions(+), 29 deletions(-) create mode 100644 src/constants.ts diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 00000000..a410e4ee --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,18 @@ +import { NamespacedId } from "./types"; + +/** Well known namespaced ids */ +export const KNOWN_NAMESPACED_IDS: { [key: string]: NamespacedId } = { + /** WS-Security */ + wssecurity: { + prefix: "wsu", + localName: "Id", + nameSpaceURI: + "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", + }, + /** Xml */ + xml: { + prefix: "xml", + localName: "id", + nameSpaceURI: "http://www.w3.org/XML/1998/namespace", + }, +}; diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 68e99ef9..78cee5c7 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -8,6 +8,7 @@ import type { GetKeyInfoContentArgs, HashAlgorithm, HashAlgorithmType, + NamespacedId, ObjectAttributes, Reference, SignatureAlgorithm, @@ -26,10 +27,11 @@ import * as execC14n from "./exclusive-canonicalization"; import * as hashAlgorithms from "./hash-algorithms"; import * as signatureAlgorithms from "./signature-algorithms"; import * as utils from "./utils"; +import { KNOWN_NAMESPACED_IDS } from "./constants"; export class SignedXml { idMode?: "wssecurity"; - idAttributes: string[]; + idAttributes: (string | NamespacedId)[]; /** * A {@link Buffer} or pem encoded {@link String} containing your private key */ @@ -155,6 +157,10 @@ export class SignedXml { if (idAttribute) { this.idAttributes.unshift(idAttribute); } + if (idMode === "wssecurity") { + this.idAttributes.unshift(KNOWN_NAMESPACED_IDS["wssecurity"]); + } + this.privateKey = privateKey; this.publicCert = publicCert; this.signatureAlgorithm = signatureAlgorithm ?? this.signatureAlgorithm; @@ -503,7 +509,10 @@ export class SignedXml { const uri = ref.uri?.[0] === "#" ? ref.uri.substring(1) : ref.uri; for (const attr of this.idAttributes) { - const elemId = elem.getAttribute(attr); + const elemId = + typeof attr === "string" + ? elem.getAttribute(attr) + : elem.getAttribute(`${attr.prefix}:${attr.localName}`); if (uri === elemId) { ref.xpath = `//*[@*[local-name(.)='${attr}']='${uri}']`; break; // found the correct element, no need to check further @@ -1297,18 +1306,10 @@ export class SignedXml { private ensureHasId(node) { let attr; - if (this.idMode === "wssecurity") { - attr = utils.findAttr( - node, - "Id", - "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", - ); - } else { - this.idAttributes.some((idAttribute) => { - attr = utils.findAttr(node, idAttribute); - return !!attr; // This will break the loop as soon as a truthy attr is found. - }); - } + this.idAttributes.some((idAttribute) => { + attr = utils.findAttr(node, idAttribute); + return !!attr; // This will break the loop as soon as a truthy attr is found. + }); if (attr) { return attr.value; @@ -1317,21 +1318,20 @@ export class SignedXml { //add the attribute const id = `_${this.id++}`; - if (this.idMode === "wssecurity") { + if (typeof this.idAttributes[0] === "string") { + node.setAttribute(this.idAttributes[0], id); + } else { node.setAttributeNS( "http://www.w3.org/2000/xmlns/", - "xmlns:wsu", - "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", + `xmlns:${this.idAttributes[0].prefix}`, + this.idAttributes[0].nameSpaceURI, ); node.setAttributeNS( - "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", - "wsu:Id", + this.idAttributes[0].nameSpaceURI, + `${this.idAttributes[0].prefix}:${this.idAttributes[0].localName}`, id, ); - } else { - node.setAttribute(this.idAttributes[0], id); } - return id; } diff --git a/src/types.ts b/src/types.ts index 89c0b304..3059e29b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -59,12 +59,24 @@ export interface ObjectAttributes { [key: string]: string | undefined; } +/** + * Namespaced id attribute. + */ +export interface NamespacedId { + /** Namespace prefix */ + prefix: string; + /** Attribute local name */ + localName: string; + /** Namespace URI */ + nameSpaceURI: string; +} + /** * Options for the SignedXml constructor. */ export interface SignedXmlOptions { idMode?: "wssecurity"; - idAttribute?: string; + idAttribute?: string | NamespacedId; privateKey?: crypto.KeyLike; publicCert?: crypto.KeyLike; signatureAlgorithm?: SignatureAlgorithmType; diff --git a/src/utils.ts b/src/utils.ts index 466b252e..4d494f14 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import * as xpath from "xpath"; -import type { NamespacePrefix } from "./types"; +import type { NamespacedId, NamespacePrefix } from "./types"; import * as isDomNode from "@xmldom/is-dom-node"; export function isArrayHasLength(array: unknown): array is unknown[] { @@ -17,10 +17,17 @@ function attrEqualsImplicitly(attr: Attr, localName: string, namespace?: string, ); } -export function findAttr(element: Element, localName: string, namespace?: string) { +export function findAttr(element: Element, id: string | NamespacedId) { for (let i = 0; i < element.attributes.length; i++) { const attr = element.attributes[i]; - + let localName: string; + let namespace: string | undefined; + if (typeof id === "string") { + localName = id; + } else { + localName = id.localName; + namespace = id.nameSpaceURI; + } if ( attrEqualsExplicitly(attr, localName, namespace) || attrEqualsImplicitly(attr, localName, namespace, element) diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index bbb419e0..b968448e 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -85,7 +85,7 @@ describe("Signature unit tests", function () { expect(node.length, `xpath ${xpathArg} not found`).to.equal(1); } - function verifyAddsId(mode, nsMode, idAttribute:string|undefined = undefined) { + function verifyAddsId(mode, nsMode, idAttribute: string | undefined = undefined) { const xml = ''; const sig = new SignedXml({ idMode: mode, idAttribute }); sig.privateKey = fs.readFileSync("./test/static/client.pem"); @@ -114,7 +114,9 @@ describe("Signature unit tests", function () { const op = nsMode === "equal" ? "=" : "!="; - const xpathArg = `//*[local-name(.)='{elem}' and '_{id}' = @*[local-name(.)='${idAttribute || 'Id'}' and namespace-uri(.)${op}'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd']]`; + const xpathArg = `//*[local-name(.)='{elem}' and '_{id}' = @*[local-name(.)='${ + idAttribute || "Id" + }' and namespace-uri(.)${op}'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd']]`; //verify each of the signed nodes now has an "Id" attribute with the right value nodeExists(doc, xpathArg.replace("{id}", "0").replace("{elem}", "x")); @@ -123,7 +125,7 @@ describe("Signature unit tests", function () { } it("signer adds increasing different id attributes to elements with custom idAttribute", function () { - verifyAddsId(null, "different", 'myIdAttribute'); + verifyAddsId(null, "different", "myIdAttribute"); }); it("signer adds increasing different id attributes to elements", function () {