diff --git a/src/hash-algorithms.ts b/src/hash-algorithms.ts index eeeb8b2..bdfa23d 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 663d3d0..004322f 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 89c0b30..6e028ad 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 */