Skip to content

Commit b10a651

Browse files
committed
feat: add support for inserting and signing Object elements inside the Signature
1 parent 9b91edf commit b10a651

File tree

3 files changed

+866
-0
lines changed

3 files changed

+866
-0
lines changed

src/signed-xml.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export class SignedXml {
5656
keyInfoAttributes: { [attrName: string]: string } = {};
5757
getKeyInfoContent = SignedXml.getKeyInfoContent;
5858
getCertFromKeyInfo = SignedXml.getCertFromKeyInfo;
59+
getObjectContent = SignedXml.getObjectContent;
5960

6061
// Internal state
6162
private id = 0;
@@ -126,6 +127,16 @@ export class SignedXml {
126127

127128
static noop = () => null;
128129

130+
/**
131+
* Default implementation for getObjectContent that returns null (no Objects)
132+
*/
133+
static getObjectContent(): Array<{
134+
content: string;
135+
attributes?: Record<string, string | undefined>;
136+
}> | null {
137+
return null;
138+
}
139+
129140
/**
130141
* The SignedXml constructor provides an abstraction for sign and verify xml documents. The object is constructed using
131142
* @param options {@link SignedXmlOptions}
@@ -143,6 +154,7 @@ export class SignedXml {
143154
keyInfoAttributes,
144155
getKeyInfoContent,
145156
getCertFromKeyInfo,
157+
getObjectContent,
146158
} = options;
147159

148160
// Options
@@ -164,6 +176,7 @@ export class SignedXml {
164176
this.keyInfoAttributes = keyInfoAttributes ?? this.keyInfoAttributes;
165177
this.getKeyInfoContent = getKeyInfoContent ?? this.getKeyInfoContent;
166178
this.getCertFromKeyInfo = getCertFromKeyInfo ?? SignedXml.noop;
179+
this.getObjectContent = getObjectContent ?? this.getObjectContent;
167180
this.CanonicalizationAlgorithms;
168181
this.HashAlgorithms;
169182
this.SignatureAlgorithms;
@@ -796,6 +809,7 @@ export class SignedXml {
796809
* @param digestValue The expected digest value for the reference.
797810
* @param inclusiveNamespacesPrefixList The prefix list for inclusive namespace canonicalization.
798811
* @param isEmptyUri Indicates whether the URI is empty. Defaults to `false`.
812+
* @param isSignatureReference Indicates whether this reference points to an element in the signature itself (like an Object element).
799813
*/
800814
addReference({
801815
xpath,
@@ -805,6 +819,7 @@ export class SignedXml {
805819
digestValue,
806820
inclusiveNamespacesPrefixList = [],
807821
isEmptyUri = false,
822+
isSignatureReference = false,
808823
}: Partial<Reference> & Pick<Reference, "xpath">): void {
809824
if (digestAlgorithm == null) {
810825
throw new Error("digestAlgorithm is required");
@@ -822,6 +837,7 @@ export class SignedXml {
822837
digestValue,
823838
inclusiveNamespacesPrefixList,
824839
isEmptyUri,
840+
isSignatureReference,
825841
getValidatedNode: () => {
826842
throw new Error(
827843
"Reference has not been validated yet; Did you call `sig.checkSignature()`?",
@@ -965,6 +981,7 @@ export class SignedXml {
965981

966982
signatureXml += this.createSignedInfo(doc, prefix);
967983
signatureXml += this.getKeyInfo(prefix);
984+
signatureXml += this.getObjects(prefix);
968985
signatureXml += `</${currentPrefix}Signature>`;
969986

970987
this.originalXmlWithIds = doc.toString();
@@ -979,6 +996,9 @@ export class SignedXml {
979996
const dummySignatureWrapper = `<Dummy ${existingPrefixesString}>${signatureXml}</Dummy>`;
980997
const nodeXml = new xmldom.DOMParser().parseFromString(dummySignatureWrapper);
981998

999+
// Process any signature references after the signature has been created
1000+
this.processSignatureReferences(nodeXml, prefix);
1001+
9821002
// Because we are using a dummy wrapper hack described above, we know there will be a `firstChild`
9831003
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
9841004
const signatureDoc = nodeXml.documentElement.firstChild!;
@@ -1070,6 +1090,39 @@ export class SignedXml {
10701090
return "";
10711091
}
10721092

1093+
/**
1094+
* Creates XML for Object elements to be included in the signature
1095+
*
1096+
* @param prefix Optional namespace prefix
1097+
* @returns XML string with Object elements or empty string if none
1098+
*/
1099+
private getObjects(prefix?: string) {
1100+
const currentPrefix = prefix ? `${prefix}:` : "";
1101+
const objects = this.getObjectContent?.();
1102+
1103+
if (!objects || objects.length === 0) {
1104+
return "";
1105+
}
1106+
1107+
let result = "";
1108+
1109+
for (const obj of objects) {
1110+
let objectAttrs = "";
1111+
if (obj.attributes) {
1112+
Object.keys(obj.attributes).forEach((name) => {
1113+
const value = obj.attributes?.[name];
1114+
if (value !== undefined) {
1115+
objectAttrs += ` ${name}="${value}"`;
1116+
}
1117+
});
1118+
}
1119+
1120+
result += `<${currentPrefix}Object${objectAttrs}>${obj.content}</${currentPrefix}Object>`;
1121+
}
1122+
1123+
return result;
1124+
}
1125+
10731126
/**
10741127
* Generate the Reference nodes (as part of the signature process)
10751128
*
@@ -1082,6 +1135,10 @@ export class SignedXml {
10821135

10831136
/* eslint-disable-next-line deprecation/deprecation */
10841137
for (const ref of this.getReferences()) {
1138+
if (ref.isSignatureReference) {
1139+
// For signature references, we'll handle them separately after the signature is created
1140+
continue;
1141+
}
10851142
const nodes = xpath.selectWithResolver(ref.xpath ?? "", doc, this.namespaceResolver);
10861143

10871144
if (!utils.isArrayHasLength(nodes)) {
@@ -1262,6 +1319,115 @@ export class SignedXml {
12621319
return doc.documentElement.firstChild!;
12631320
}
12641321

1322+
/**
1323+
* Process references that point to elements within the Signature element
1324+
* This is called after the initial signature has been created
1325+
*/
1326+
private processSignatureReferences(signatureDoc: Document, prefix?: string) {
1327+
// Get signature references
1328+
const signatureReferences = this.references.filter((ref) => ref.isSignatureReference);
1329+
if (signatureReferences.length === 0) {
1330+
return;
1331+
}
1332+
1333+
prefix = prefix || "";
1334+
prefix = prefix ? `${prefix}:` : prefix;
1335+
const signatureNamespace = "http://www.w3.org/2000/09/xmldsig#";
1336+
1337+
// Find the SignedInfo element to append to
1338+
const signedInfoNode = xpath.select1(
1339+
`.//*[local-name(.)='SignedInfo']`,
1340+
signatureDoc,
1341+
) as Element;
1342+
if (!signedInfoNode) {
1343+
throw new Error("Could not find SignedInfo element in signature");
1344+
}
1345+
1346+
// Process each signature reference
1347+
for (const ref of signatureReferences) {
1348+
const nodes = xpath.selectWithResolver(ref.xpath ?? "", signatureDoc, this.namespaceResolver);
1349+
1350+
if (!utils.isArrayHasLength(nodes)) {
1351+
throw new Error(
1352+
`the following xpath cannot be signed because it was not found: ${ref.xpath}`,
1353+
);
1354+
}
1355+
1356+
// Process the reference
1357+
for (const node of nodes) {
1358+
// Create the reference element directly using DOM methods to avoid namespace issues
1359+
const referenceElem = signatureDoc.createElementNS(
1360+
signatureNamespace,
1361+
`${prefix}Reference`,
1362+
);
1363+
if (ref.isEmptyUri) {
1364+
referenceElem.setAttribute("URI", "");
1365+
} else {
1366+
const id = this.ensureHasId(node);
1367+
ref.uri = id;
1368+
referenceElem.setAttribute("URI", `#${id}`);
1369+
}
1370+
1371+
const transformsElem = signatureDoc.createElementNS(
1372+
signatureNamespace,
1373+
`${prefix}Transforms`,
1374+
);
1375+
1376+
for (const trans of ref.transforms || []) {
1377+
const transform = this.findCanonicalizationAlgorithm(trans);
1378+
const transformElem = signatureDoc.createElementNS(
1379+
signatureNamespace,
1380+
`${prefix}Transform`,
1381+
);
1382+
transformElem.setAttribute("Algorithm", transform.getAlgorithmName());
1383+
1384+
if (utils.isArrayHasLength(ref.inclusiveNamespacesPrefixList)) {
1385+
const inclusiveNamespacesElem = signatureDoc.createElementNS(
1386+
transform.getAlgorithmName(),
1387+
"InclusiveNamespaces",
1388+
);
1389+
inclusiveNamespacesElem.setAttribute(
1390+
"PrefixList",
1391+
ref.inclusiveNamespacesPrefixList.join(" "),
1392+
);
1393+
transformElem.appendChild(inclusiveNamespacesElem);
1394+
}
1395+
1396+
transformsElem.appendChild(transformElem);
1397+
}
1398+
1399+
// Get the canonicalized XML
1400+
const canonXml = this.getCanonReferenceXml(signatureDoc, ref, node);
1401+
1402+
// Calculate the digest
1403+
const digestAlgorithm = this.findHashAlgorithm(ref.digestAlgorithm);
1404+
const digestValue = digestAlgorithm.getHash(canonXml);
1405+
1406+
// Store the digest value for later validation
1407+
ref.digestValue = digestValue;
1408+
1409+
const digestMethodElem = signatureDoc.createElementNS(
1410+
signatureNamespace,
1411+
`${prefix}DigestMethod`,
1412+
);
1413+
digestMethodElem.setAttribute("Algorithm", digestAlgorithm.getAlgorithmName());
1414+
1415+
const digestValueElem = signatureDoc.createElementNS(
1416+
signatureNamespace,
1417+
`${prefix}DigestValue`,
1418+
);
1419+
digestValueElem.textContent = digestValue;
1420+
1421+
referenceElem.appendChild(transformsElem);
1422+
referenceElem.appendChild(digestMethodElem);
1423+
referenceElem.appendChild(digestValueElem);
1424+
1425+
// Append the reference element to SignedInfo
1426+
signedInfoNode.appendChild(referenceElem);
1427+
}
1428+
}
1429+
}
1430+
12651431
/**
12661432
* Returns just the signature part, must be called only after {@link computeSignature}
12671433
*

src/types.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@ export interface GetKeyInfoContentArgs {
4343
prefix?: string | null;
4444
}
4545

46+
/**
47+
* Object attributes as defined in XMLDSig spec
48+
* @see https://www.w3.org/TR/xmldsig-core/#sec-Object
49+
*/
50+
export interface ObjectAttributes {
51+
/** Optional ID attribute */
52+
Id?: string;
53+
/** Optional MIME type attribute */
54+
MimeType?: string;
55+
/** Optional encoding attribute */
56+
Encoding?: string;
57+
/** Any additional custom attributes */
58+
[key: string]: string | undefined;
59+
}
60+
4661
/**
4762
* Options for the SignedXml constructor.
4863
*/
@@ -58,6 +73,7 @@ export interface SignedXmlOptions {
5873
keyInfoAttributes?: Record<string, string>;
5974
getKeyInfoContent?(args?: GetKeyInfoContentArgs): string | null;
6075
getCertFromKeyInfo?(keyInfo?: Node | null): string | null;
76+
getObjectContent?(): Array<{ content: string; attributes?: ObjectAttributes }> | null;
6177
}
6278

6379
export interface NamespacePrefix {
@@ -127,6 +143,9 @@ export interface Reference {
127143
// Optional. Indicates whether the URI is empty.
128144
isEmptyUri: boolean;
129145

146+
// Optional. Indicates if this reference points to an element within the Signature
147+
isSignatureReference?: boolean;
148+
130149
// Optional. The type of the reference node.
131150
ancestorNamespaces?: NamespacePrefix[];
132151

0 commit comments

Comments
 (0)