From 684feb583bbaab25b7d859a74bbf9964fac6901d Mon Sep 17 00:00:00 2001 From: David Thornton Date: Fri, 3 Apr 2026 14:29:33 +1100 Subject: [PATCH 1/4] Implement addAttachmentReference() --- src/hash-algorithms.ts | 6 +- src/signed-xml.ts | 146 +++++++++++++++++++++++++++++++++++++++++ src/types.ts | 43 +++++++++++- 3 files changed, 193 insertions(+), 2 deletions(-) diff --git a/src/hash-algorithms.ts b/src/hash-algorithms.ts index eeeb8b27..bdfa23d2 100644 --- a/src/hash-algorithms.ts +++ b/src/hash-algorithms.ts @@ -17,7 +17,11 @@ export class Sha1 implements HashAlgorithm { export class Sha256 implements HashAlgorithm { getHash = function (xml) { const shasum = crypto.createHash("sha256"); - shasum.update(xml, "utf8"); + if (typeof xml === "string") { + shasum.update(xml, "utf8"); + } else { + shasum.update(xml); + } const res = shasum.digest("base64"); return res; }; diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 663d3d0e..9707f69c 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -10,6 +10,7 @@ import type { HashAlgorithmType, ObjectAttributes, Reference, + AttachmentReference, SignatureAlgorithm, SignatureAlgorithmType, SignedXmlOptions, @@ -73,6 +74,11 @@ export class SignedXml { * @see {@link Reference} */ private references: Reference[] = []; + /** + * Contains the attachment references that were signed. + * @see {@link Reference} + */ + private attachmentReferences: AttachmentReference[] = []; /** * Contains the canonicalized XML of the references that were validly signed. @@ -841,6 +847,56 @@ export class SignedXml { }); } + /** + * Adds an attachment reference to the signature. + * + * @param attachment The attachment to be referenced. + * @param transforms An array of transform algorithms to be applied to the selected nodes. + * @param digestAlgorithm The digest algorithm to use for computing the digest value. + * @param uri The URI identifier for the reference. If empty, an empty URI will be used. + * @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. + */ + addAttachmentReference({ + attachment, + transforms, + digestAlgorithm, + uri = "", + digestValue, + inclusiveNamespacesPrefixList = [], + isEmptyUri = false, + id = undefined, + type = undefined, + }: Partial & Pick): void { + if (digestAlgorithm == null) { + throw new Error("digestAlgorithm is required"); + } + + if (!utils.isArrayHasLength(transforms)) { + throw new Error("transforms must contain at least one transform algorithm"); + } + + this.attachmentReferences.push({ + attachment, + transforms, + digestAlgorithm, + uri, + digestValue, + inclusiveNamespacesPrefixList, + isEmptyUri, + id, + type, + getValidatedNode: () => { + throw new Error( + "Reference has not been validated yet; Did you call `sig.checkSignature()`?", + ); + }, + }); + } + /** * Returns the list of references. */ @@ -855,6 +911,20 @@ export class SignedXml { return this.references; } + /** + * Returns the list of attachment references. + */ + getAttachmentReferences() { + // TODO: Refactor once `getValidatedNode` is removed + /* Once we completely remove the deprecated `getValidatedNode()` method, + we can change this to return a clone to prevent accidental mutations, + e.g.: + return [...this.attachmentReferences]; + */ + + return this.attachmentReferences; + } + getSignedReferences() { return [...this.signedReferences]; } @@ -1209,6 +1279,82 @@ export class SignedXml { signedInfoNode.appendChild(referenceElem); } } + + // Process each attachment reference + for (const ref of this.getAttachmentReferences()) { + // Compute the target URI + let targetUri: string; + if (ref.isEmptyUri) { + targetUri = ""; + } else { + targetUri = `#${ref.uri}`; + } + + // Create the reference element directly using DOM methods to avoid namespace issues + const referenceElem = signatureDoc.createElementNS( + signatureNamespace, + `${currentPrefix}Reference`, + ); + referenceElem.setAttribute("URI", targetUri); + + if (ref.id) { + referenceElem.setAttribute("Id", ref.id); + } + + if (ref.type) { + referenceElem.setAttribute("Type", ref.type); + } + + const transformsElem = signatureDoc.createElementNS( + signatureNamespace, + `${currentPrefix}Transforms`, + ); + + for (const trans of ref.transforms || []) { + const transform = this.findCanonicalizationAlgorithm(trans); + const transformElem = signatureDoc.createElementNS( + signatureNamespace, + `${currentPrefix}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 digest algorithm and compute the digest value + const digestAlgorithm = this.findHashAlgorithm(ref.digestAlgorithm); + + const digestMethodElem = signatureDoc.createElementNS( + signatureNamespace, + `${currentPrefix}DigestMethod`, + ); + digestMethodElem.setAttribute("Algorithm", digestAlgorithm.getAlgorithmName()); + + const digestValueElem = signatureDoc.createElementNS( + signatureNamespace, + `${currentPrefix}DigestValue`, + ); + digestValueElem.textContent = digestAlgorithm.getHash(ref.attachment); + + referenceElem.appendChild(transformsElem); + referenceElem.appendChild(digestMethodElem); + referenceElem.appendChild(digestValueElem); + + // Append the reference element to SignedInfo + signedInfoNode.appendChild(referenceElem); + } } private getKeyInfo(prefix) { diff --git a/src/types.ts b/src/types.ts index 89c0b304..6e028ad2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -160,6 +160,47 @@ export interface Reference { signedReference?: string; } +/** + * Represents an attachment reference node for XML digital signature. + */ +export interface AttachmentReference { + // The attachment data to be signed. + attachment: Buffer; + + // An array of transforms to be applied to the data before signing. + transforms: ReadonlyArray; + + // The algorithm used to calculate the digest value of the data. + digestAlgorithm: HashAlgorithmType; + + // The URI that identifies the data to be signed. + uri: string; + + // Optional. The digest value of the referenced data. + digestValue?: unknown; + + // A list of namespace prefixes to be treated as "inclusive" during canonicalization. + inclusiveNamespacesPrefixList: string[]; + + // 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[]; + + validationError?: Error; + + getValidatedNode(xpathSelector?: string): Node | null; + + signedReference?: string; +} + /** Implement this to create a new CanonicalizationOrTransformationAlgorithm */ export interface CanonicalizationOrTransformationAlgorithm { process( @@ -174,7 +215,7 @@ export interface CanonicalizationOrTransformationAlgorithm { export interface HashAlgorithm { getAlgorithmName(): HashAlgorithmType; - getHash(xml: string): string; + getHash(data: string | Buffer): string; } /** Extend this to create a new SignatureAlgorithm */ From 05bd80fe6b15c4b90d1e46dcb98304d8ba987512 Mon Sep 17 00:00:00 2001 From: David Thornton Date: Fri, 3 Apr 2026 14:49:57 +1100 Subject: [PATCH 2/4] FIX: Attachment URI --- src/signed-xml.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 9707f69c..004322fb 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -1287,7 +1287,7 @@ export class SignedXml { if (ref.isEmptyUri) { targetUri = ""; } else { - targetUri = `#${ref.uri}`; + targetUri = `${ref.uri}`; } // Create the reference element directly using DOM methods to avoid namespace issues From 384696d047bd6f2f67067a72f7f2784b95363794 Mon Sep 17 00:00:00 2001 From: David Thornton Date: Fri, 3 Apr 2026 16:43:17 +1100 Subject: [PATCH 3/4] Address coderabbit review --- src/signed-xml.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 004322fb..384f3348 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -1160,7 +1160,7 @@ export class SignedXml { * Adds all references to the SignedInfo after the signature placeholder is inserted. */ private addAllReferences(doc: Document, signatureElem: Element, prefix?: string): void { - if (!utils.isArrayHasLength(this.references)) { + if (!utils.isArrayHasLength(this.references) && !utils.isArrayHasLength(this.attachmentReferences)) { return; } From dac1feb9b3b7bfb250dca00276fdc20fe537b61a Mon Sep 17 00:00:00 2001 From: David Thornton Date: Fri, 3 Apr 2026 16:46:44 +1100 Subject: [PATCH 4/4] Revert "Address coderabbit review" This reverts commit 384696d047bd6f2f67067a72f7f2784b95363794. Attachment references should be optional, and omitted if not present --- src/signed-xml.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 384f3348..004322fb 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -1160,7 +1160,7 @@ export class SignedXml { * Adds all references to the SignedInfo after the signature placeholder is inserted. */ private addAllReferences(doc: Document, signatureElem: Element, prefix?: string): void { - if (!utils.isArrayHasLength(this.references) && !utils.isArrayHasLength(this.attachmentReferences)) { + if (!utils.isArrayHasLength(this.references)) { return; }