Skip to content

Commit b756f6c

Browse files
Merge pull request #1 from davidthornton/fork-6.1.2-feature-implement-addAttachmentReference
Implement `addAttachmentReference()`
2 parents b673581 + 05bd80f commit b756f6c

3 files changed

Lines changed: 193 additions & 2 deletions

File tree

src/hash-algorithms.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ export class Sha1 implements HashAlgorithm {
1717
export class Sha256 implements HashAlgorithm {
1818
getHash = function (xml) {
1919
const shasum = crypto.createHash("sha256");
20-
shasum.update(xml, "utf8");
20+
if (typeof xml === "string") {
21+
shasum.update(xml, "utf8");
22+
} else {
23+
shasum.update(xml);
24+
}
2125
const res = shasum.digest("base64");
2226
return res;
2327
};

src/signed-xml.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
HashAlgorithmType,
1111
ObjectAttributes,
1212
Reference,
13+
AttachmentReference,
1314
SignatureAlgorithm,
1415
SignatureAlgorithmType,
1516
SignedXmlOptions,
@@ -73,6 +74,11 @@ export class SignedXml {
7374
* @see {@link Reference}
7475
*/
7576
private references: Reference[] = [];
77+
/**
78+
* Contains the attachment references that were signed.
79+
* @see {@link Reference}
80+
*/
81+
private attachmentReferences: AttachmentReference[] = [];
7682

7783
/**
7884
* Contains the canonicalized XML of the references that were validly signed.
@@ -841,6 +847,56 @@ export class SignedXml {
841847
});
842848
}
843849

850+
/**
851+
* Adds an attachment reference to the signature.
852+
*
853+
* @param attachment The attachment to be referenced.
854+
* @param transforms An array of transform algorithms to be applied to the selected nodes.
855+
* @param digestAlgorithm The digest algorithm to use for computing the digest value.
856+
* @param uri The URI identifier for the reference. If empty, an empty URI will be used.
857+
* @param digestValue The expected digest value for the reference.
858+
* @param inclusiveNamespacesPrefixList The prefix list for inclusive namespace canonicalization.
859+
* @param isEmptyUri Indicates whether the URI is empty. Defaults to `false`.
860+
* @param id An optional `Id` attribute for the reference.
861+
* @param type An optional `Type` attribute for the reference.
862+
*/
863+
addAttachmentReference({
864+
attachment,
865+
transforms,
866+
digestAlgorithm,
867+
uri = "",
868+
digestValue,
869+
inclusiveNamespacesPrefixList = [],
870+
isEmptyUri = false,
871+
id = undefined,
872+
type = undefined,
873+
}: Partial<Reference> & Pick<AttachmentReference, "attachment">): void {
874+
if (digestAlgorithm == null) {
875+
throw new Error("digestAlgorithm is required");
876+
}
877+
878+
if (!utils.isArrayHasLength(transforms)) {
879+
throw new Error("transforms must contain at least one transform algorithm");
880+
}
881+
882+
this.attachmentReferences.push({
883+
attachment,
884+
transforms,
885+
digestAlgorithm,
886+
uri,
887+
digestValue,
888+
inclusiveNamespacesPrefixList,
889+
isEmptyUri,
890+
id,
891+
type,
892+
getValidatedNode: () => {
893+
throw new Error(
894+
"Reference has not been validated yet; Did you call `sig.checkSignature()`?",
895+
);
896+
},
897+
});
898+
}
899+
844900
/**
845901
* Returns the list of references.
846902
*/
@@ -855,6 +911,20 @@ export class SignedXml {
855911
return this.references;
856912
}
857913

914+
/**
915+
* Returns the list of attachment references.
916+
*/
917+
getAttachmentReferences() {
918+
// TODO: Refactor once `getValidatedNode` is removed
919+
/* Once we completely remove the deprecated `getValidatedNode()` method,
920+
we can change this to return a clone to prevent accidental mutations,
921+
e.g.:
922+
return [...this.attachmentReferences];
923+
*/
924+
925+
return this.attachmentReferences;
926+
}
927+
858928
getSignedReferences() {
859929
return [...this.signedReferences];
860930
}
@@ -1209,6 +1279,82 @@ export class SignedXml {
12091279
signedInfoNode.appendChild(referenceElem);
12101280
}
12111281
}
1282+
1283+
// Process each attachment reference
1284+
for (const ref of this.getAttachmentReferences()) {
1285+
// Compute the target URI
1286+
let targetUri: string;
1287+
if (ref.isEmptyUri) {
1288+
targetUri = "";
1289+
} else {
1290+
targetUri = `${ref.uri}`;
1291+
}
1292+
1293+
// Create the reference element directly using DOM methods to avoid namespace issues
1294+
const referenceElem = signatureDoc.createElementNS(
1295+
signatureNamespace,
1296+
`${currentPrefix}Reference`,
1297+
);
1298+
referenceElem.setAttribute("URI", targetUri);
1299+
1300+
if (ref.id) {
1301+
referenceElem.setAttribute("Id", ref.id);
1302+
}
1303+
1304+
if (ref.type) {
1305+
referenceElem.setAttribute("Type", ref.type);
1306+
}
1307+
1308+
const transformsElem = signatureDoc.createElementNS(
1309+
signatureNamespace,
1310+
`${currentPrefix}Transforms`,
1311+
);
1312+
1313+
for (const trans of ref.transforms || []) {
1314+
const transform = this.findCanonicalizationAlgorithm(trans);
1315+
const transformElem = signatureDoc.createElementNS(
1316+
signatureNamespace,
1317+
`${currentPrefix}Transform`,
1318+
);
1319+
transformElem.setAttribute("Algorithm", transform.getAlgorithmName());
1320+
1321+
if (utils.isArrayHasLength(ref.inclusiveNamespacesPrefixList)) {
1322+
const inclusiveNamespacesElem = signatureDoc.createElementNS(
1323+
transform.getAlgorithmName(),
1324+
"InclusiveNamespaces",
1325+
);
1326+
inclusiveNamespacesElem.setAttribute(
1327+
"PrefixList",
1328+
ref.inclusiveNamespacesPrefixList.join(" "),
1329+
);
1330+
transformElem.appendChild(inclusiveNamespacesElem);
1331+
}
1332+
1333+
transformsElem.appendChild(transformElem);
1334+
}
1335+
1336+
// Get the digest algorithm and compute the digest value
1337+
const digestAlgorithm = this.findHashAlgorithm(ref.digestAlgorithm);
1338+
1339+
const digestMethodElem = signatureDoc.createElementNS(
1340+
signatureNamespace,
1341+
`${currentPrefix}DigestMethod`,
1342+
);
1343+
digestMethodElem.setAttribute("Algorithm", digestAlgorithm.getAlgorithmName());
1344+
1345+
const digestValueElem = signatureDoc.createElementNS(
1346+
signatureNamespace,
1347+
`${currentPrefix}DigestValue`,
1348+
);
1349+
digestValueElem.textContent = digestAlgorithm.getHash(ref.attachment);
1350+
1351+
referenceElem.appendChild(transformsElem);
1352+
referenceElem.appendChild(digestMethodElem);
1353+
referenceElem.appendChild(digestValueElem);
1354+
1355+
// Append the reference element to SignedInfo
1356+
signedInfoNode.appendChild(referenceElem);
1357+
}
12121358
}
12131359

12141360
private getKeyInfo(prefix) {

src/types.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,47 @@ export interface Reference {
160160
signedReference?: string;
161161
}
162162

163+
/**
164+
* Represents an attachment reference node for XML digital signature.
165+
*/
166+
export interface AttachmentReference {
167+
// The attachment data to be signed.
168+
attachment: Buffer;
169+
170+
// An array of transforms to be applied to the data before signing.
171+
transforms: ReadonlyArray<CanonicalizationOrTransformAlgorithmType>;
172+
173+
// The algorithm used to calculate the digest value of the data.
174+
digestAlgorithm: HashAlgorithmType;
175+
176+
// The URI that identifies the data to be signed.
177+
uri: string;
178+
179+
// Optional. The digest value of the referenced data.
180+
digestValue?: unknown;
181+
182+
// A list of namespace prefixes to be treated as "inclusive" during canonicalization.
183+
inclusiveNamespacesPrefixList: string[];
184+
185+
// Optional. Indicates whether the URI is empty.
186+
isEmptyUri: boolean;
187+
188+
// Optional. The `Id` attribute of the reference node.
189+
id?: string;
190+
191+
// Optional. The `Type` attribute of the reference node.
192+
type?: string;
193+
194+
// Optional. The type of the reference node.
195+
ancestorNamespaces?: NamespacePrefix[];
196+
197+
validationError?: Error;
198+
199+
getValidatedNode(xpathSelector?: string): Node | null;
200+
201+
signedReference?: string;
202+
}
203+
163204
/** Implement this to create a new CanonicalizationOrTransformationAlgorithm */
164205
export interface CanonicalizationOrTransformationAlgorithm {
165206
process(
@@ -174,7 +215,7 @@ export interface CanonicalizationOrTransformationAlgorithm {
174215
export interface HashAlgorithm {
175216
getAlgorithmName(): HashAlgorithmType;
176217

177-
getHash(xml: string): string;
218+
getHash(data: string | Buffer): string;
178219
}
179220

180221
/** Extend this to create a new SignatureAlgorithm */

0 commit comments

Comments
 (0)