Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/hash-algorithms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
146 changes: 146 additions & 0 deletions src/signed-xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
HashAlgorithmType,
ObjectAttributes,
Reference,
AttachmentReference,
SignatureAlgorithm,
SignatureAlgorithmType,
SignedXmlOptions,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<Reference> & Pick<AttachmentReference, "attachment">): 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.
*/
Expand All @@ -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];
}
Expand Down Expand Up @@ -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) {
Expand Down
43 changes: 42 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CanonicalizationOrTransformAlgorithmType>;

// 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(
Expand All @@ -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 */
Expand Down