diff --git a/README.md b/README.md
index ecfa261..bba708d 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
+- `objects` - array - default `undefined` - an array of objects defining the content of the `` nodes
#### API
@@ -268,10 +269,12 @@ 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, 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)
+ - `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:
@@ -534,6 +537,42 @@ 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.
+
+```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",
+ objects: [
+ {
+ 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#"],
+});
+
+sig.computeSignature(xml);
+fs.writeFileSync("signed.xml", sig.getSignedXml());
+```
+
### more examples (_coming soon_)
## Development
diff --git a/src/signed-xml.ts b/src/signed-xml.ts
index 05dae41..c068656 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,6 +57,7 @@ export class SignedXml {
keyInfoAttributes: { [attrName: string]: string } = {};
getKeyInfoContent = SignedXml.getKeyInfoContent;
getCertFromKeyInfo = SignedXml.getCertFromKeyInfo;
+ objects?: Array<{ content: string; attributes?: ObjectAttributes }>;
// Internal state
private id = 0;
@@ -70,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.
@@ -143,6 +145,7 @@ export class SignedXml {
keyInfoAttributes,
getKeyInfoContent,
getCertFromKeyInfo,
+ objects,
} = options;
// Options
@@ -164,6 +167,7 @@ export class SignedXml {
this.keyInfoAttributes = keyInfoAttributes ?? this.keyInfoAttributes;
this.getKeyInfoContent = getKeyInfoContent ?? this.getKeyInfoContent;
this.getCertFromKeyInfo = getCertFromKeyInfo ?? SignedXml.noop;
+ this.objects = objects;
this.CanonicalizationAlgorithms;
this.HashAlgorithms;
this.SignatureAlgorithms;
@@ -796,6 +800,8 @@ 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 id An optional `Id` attribute for the reference.
+ * @param type An optional `Type` attribute for the reference.
*/
addReference({
xpath,
@@ -805,6 +811,8 @@ export class SignedXml {
digestValue,
inclusiveNamespacesPrefixList = [],
isEmptyUri = false,
+ id = undefined,
+ type = undefined,
}: Partial & Pick): void {
if (digestAlgorithm == null) {
throw new Error("digestAlgorithm is required");
@@ -822,6 +830,9 @@ export class SignedXml {
digestValue,
inclusiveNamespacesPrefixList,
isEmptyUri,
+ id,
+ type,
+ wasProcessed: false,
getValidatedNode: () => {
throw new Error(
"Reference has not been validated yet; Did you call `sig.checkSignature()`?",
@@ -965,6 +976,7 @@ export class SignedXml {
signatureXml += this.createSignedInfo(doc, prefix);
signatureXml += this.getKeyInfo(prefix);
+ signatureXml += this.getObjects(prefix);
signatureXml += `${currentPrefix}Signature>`;
this.originalXmlWithIds = doc.toString();
@@ -980,8 +992,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);
@@ -998,26 +1011,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);
}
- this.signatureNode = signatureDoc;
+ // Process any signature references after the signature has been added to the document
+ this.processSignatureReferences(doc, signatureElem, prefix);
+
+ 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");
@@ -1037,8 +1053,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);
}
@@ -1046,8 +1062,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();
}
}
@@ -1070,6 +1086,38 @@ 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}:` : "";
+
+ if (!this.objects || this.objects.length === 0) {
+ return "";
+ }
+
+ let result = "";
+
+ for (const obj of this.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}${currentPrefix}Object>`;
+ }
+
+ return result;
+ }
+
/**
* Generate the Reference nodes (as part of the signature process)
*
@@ -1085,19 +1133,30 @@ export class SignedXml {
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) {
+ 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);
@@ -1122,6 +1181,7 @@ export class SignedXml {
`<${prefix}DigestValue>${digestAlgorithm.getHash(canonXml)}${prefix}DigestValue>` +
`${prefix}Reference>`;
}
+ ref.wasProcessed = true;
}
return res;
@@ -1262,6 +1322,135 @@ export class SignedXml {
return doc.documentElement.firstChild!;
}
+ /**
+ * 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(doc: Document, signatureElem: Element, prefix?: string) {
+ // Get unprocessed references
+ const unprocessedReferences = this.references.filter((ref) => !ref.wasProcessed);
+ if (unprocessedReferences.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']`,
+ signatureElem,
+ ) as Element;
+ if (!signedInfoNode) {
+ 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);
+
+ 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) {
+ // Must not be a reference to Signature, SignedInfo, or a child of SignedInfo
+ if (
+ node === signatureElem ||
+ node === signedInfoNode ||
+ utils.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(
+ signatureNamespace,
+ `${prefix}Reference`,
+ );
+ if (ref.isEmptyUri) {
+ referenceElem.setAttribute("URI", "");
+ } else {
+ const id = this.ensureHasId(node);
+ ref.uri = id;
+ 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`,
+ );
+
+ 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(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`,
+ );
+ digestMethodElem.setAttribute("Algorithm", digestAlgorithm.getAlgorithmName());
+
+ const digestValueElem = signatureDoc.createElementNS(
+ signatureNamespace,
+ `${prefix}DigestValue`,
+ );
+ digestValueElem.textContent = digestAlgorithm.getHash(canonXml);
+
+ referenceElem.appendChild(transformsElem);
+ referenceElem.appendChild(digestMethodElem);
+ referenceElem.appendChild(digestValueElem);
+
+ // Append the reference element to SignedInfo
+ signedInfoNode.appendChild(referenceElem);
+ }
+ ref.wasProcessed = true;
+ }
+ }
+
/**
* Returns just the signature part, must be called only after {@link computeSignature}
*
diff --git a/src/types.ts b/src/types.ts
index f102c4c..146bdd1 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;
+ objects?: Array<{ content: string; attributes?: ObjectAttributes }>;
}
export interface NamespacePrefix {
@@ -127,6 +143,12 @@ export interface Reference {
// Optional. Indicates whether the URI is empty.
isEmptyUri: 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/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
new file mode 100644
index 0000000..f9fefd4
--- /dev/null
+++ b/test/signature-object-tests.spec.ts
@@ -0,0 +1,520 @@
+import * as fs from "fs";
+import { expect, assert } 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";
+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 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 = select1Ns("//ds:Signature", signedDoc);
+ isDomNode.assertIsNodeLike(signatureNode);
+ verifier.loadSignature(signatureNode);
+ const valid = verifier.checkSignature(signedXml);
+
+ return {
+ valid,
+ errorMessage: verifier
+ .getReferences()
+ .flatMap((ref) => ref.validationError?.message || [])
+ .join(", "),
+ };
+};
+
+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({
+ 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",
+ Encoding: "",
+ },
+ },
+ {
+ content: "Plain text content",
+ attributes: {
+ Id: "object2",
+ 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",
+ },
+ },
+ ],
+ });
+
+ 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.computeSignature(xml);
+ const signedXml = sig.getSignedXml();
+ const doc = new xmldom.DOMParser().parseFromString(signedXml);
+
+ // Should have three Object elements
+ const objectNodes = selectNs("/root/ds:Signature/ds:Object", doc);
+ isDomNode.assertIsArrayOfNodes(objectNodes);
+ expect(objectNodes.length).to.equal(3);
+
+ // Verify the first Object element
+ 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 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",
+ );
+ });
+
+ 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 = "";
+
+ // 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.addReference({
+ xpath: "/*",
+ digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
+ transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
+ });
+
+ sigWithNull.computeSignature(xml);
+ const signedXmlWithNull = sigWithNull.getSignedXml();
+ const docWithNull = new xmldom.DOMParser().parseFromString(signedXmlWithNull);
+
+ // Verify that no Object elements exist
+ const objectNodesWithNull = selectNs("//ds:Object", docWithNull);
+ isDomNode.assertIsArrayOfNodes(objectNodesWithNull);
+ expect(objectNodesWithNull.length).to.equal(0);
+
+ // 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.addReference({
+ xpath: "/*",
+ digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
+ transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
+ });
+
+ sigWithEmpty.computeSignature(xml);
+ const signedXmlWithEmpty = sigWithEmpty.getSignedXml();
+ const docWithEmpty = new xmldom.DOMParser().parseFromString(signedXmlWithEmpty);
+
+ // Verify that no Object elements exist
+ const objectNodesWithEmpty = selectNs("//ds:Object", docWithEmpty);
+ isDomNode.assertIsArrayOfNodes(objectNodesWithEmpty);
+ expect(objectNodesWithEmpty.length).to.equal(0);
+ });
+
+ it("should handle Rerefence to Object", function () {
+ 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' 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#"],
+ });
+
+ sig.computeSignature(xml);
+ const signedXml = sig.getSignedXml();
+ const signedDoc = new xmldom.DOMParser().parseFromString(signedXml);
+
+ // 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#",
+ );
+
+ // 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;
+ });
+});
+
+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/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: "/*",
+ digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
+ 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 signature is valid
+ const { valid, errorMessage } = checkSignature(signedXml, doc);
+ expect(valid, errorMessage).to.be.true;
+ });
+
+ 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',
+ attributes: {
+ Id: "object1",
+ MimeType: "text/xml",
+ },
+ },
+ ],
+ });
+
+ sig.addReference({
+ xpath: "/*",
+ digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
+ transforms: [
+ "http://www.w3.org/2000/09/xmldsig#enveloped-signature",
+ "http://www.w3.org/2001/10/xml-exc-c14n#",
+ ],
+ });
+
+ sig.addReference({
+ xpath: "//*[local-name(.)='Object' and @Id='object1']",
+ digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
+ 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 there are two Reference elements
+ 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 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
+ const { valid, errorMessage } = checkSignature(signedXml, doc);
+ expect(valid, errorMessage).to.be.true;
+ });
+});
+
+describe("XAdES Object support in XML signatures", function () {
+ it("should be able to add and sign XAdES objects", function () {
+ const signatureId = "signature_0";
+ const signedPropertiesId = "signedProperties_0";
+
+ const publicCertDigest = new Sha256().getHash(publicCertDer);
+ const xml = `text`;
+
+ const sig = new SignedXml({
+ publicCert,
+ privateKey,
+ canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#",
+ signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
+ objects: [
+ {
+ content:
+ `` +
+ `` +
+ `` +
+ `2025-06-21T12:00:00Z` +
+ `` +
+ `` +
+ `${publicCertDigest}` +
+ `` +
+ `` +
+ `` +
+ ``,
+ },
+ ],
+ });
+
+ sig.addReference({
+ xpath: `/*`,
+ isEmptyUri: true,
+ 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}']`,
+ 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#"],
+ });
+
+ sig.computeSignature(xml, {
+ prefix: "ds",
+ location: {
+ action: "append",
+ reference: "/root",
+ },
+ attrs: {
+ Id: signatureId,
+ },
+ });
+
+ const signedXml = sig.getSignedXml();
+ const signedDoc = new xmldom.DOMParser().parseFromString(signedXml);
+
+ // ds:Signature exists and has the expected Id
+ const elSig = select1Ns(`/root/ds:Signature[@Id='${signatureId}']`, signedDoc);
+ isDomNode.assertIsElementNode(elSig);
+
+ // ds:Object/xades:QualifyingProperties exists within the signature
+ const elQP = select1Ns("ds:Object/xades:QualifyingProperties", elSig);
+ isDomNode.assertIsElementNode(elQP);
+
+ // 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);
+
+ // Verify that the signature is valid
+ const { valid, errorMessage } = checkSignature(signedXml, signedDoc);
+ 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/);
+ });
+});
diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts
index baa382d..16b66e1 100644
--- a/test/signature-unit-tests.spec.ts
+++ b/test/signature-unit-tests.spec.ts
@@ -1279,4 +1279,60 @@ 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");
+ });
+
+ 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/,
+ );
+ });
});