Skip to content

Commit 77f860c

Browse files
committed
consolidate signature reference processing logic, add tests
1 parent 685c4a1 commit 77f860c

File tree

2 files changed

+303
-30
lines changed

2 files changed

+303
-30
lines changed

src/signed-xml.ts

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,37 +1334,23 @@ export class SignedXml {
13341334
prefix = prefix ? `${prefix}:` : prefix;
13351335
const signatureNamespace = "http://www.w3.org/2000/09/xmldsig#";
13361336

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+
13371346
// Process each signature reference
13381347
for (const ref of signatureReferences) {
1339-
const uri = ref.uri?.[0] === "#" ? ref.uri.substring(1) : ref.uri;
1340-
if (!uri) {
1341-
continue;
1342-
}
1343-
1344-
// Find the referenced element in the document
1345-
const objectXpath = `//*[@Id='${uri}']`;
1346-
const selectResult = xpath.select(objectXpath, signatureDoc);
1347-
1348-
// Ensure result is an array of nodes
1349-
const nodes: Node[] = [];
1350-
if (Array.isArray(selectResult)) {
1351-
selectResult.forEach((node) => nodes.push(node));
1352-
} else if (selectResult !== null) {
1353-
// If it's a single node, add it to the array
1354-
nodes.push(selectResult as Node);
1355-
}
1348+
const nodes = xpath.selectWithResolver(ref.xpath ?? "", signatureDoc, this.namespaceResolver);
13561349

13571350
if (!utils.isArrayHasLength(nodes)) {
1358-
throw new Error(`Could not find element with ID ${uri} for signature reference`);
1359-
}
1360-
1361-
// Find the SignedInfo element to append to
1362-
const signedInfoNode = xpath.select1(
1363-
`.//*[local-name(.)='SignedInfo']`,
1364-
signatureDoc,
1365-
) as Element;
1366-
if (!signedInfoNode) {
1367-
throw new Error("Could not find SignedInfo element in signature");
1351+
throw new Error(
1352+
`the following xpath cannot be signed because it was not found: ${ref.xpath}`,
1353+
);
13681354
}
13691355

13701356
// Process the reference
@@ -1374,7 +1360,13 @@ export class SignedXml {
13741360
signatureNamespace,
13751361
`${prefix}Reference`,
13761362
);
1377-
referenceElem.setAttribute("URI", `#${uri}`);
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+
}
13781370

13791371
const transformsElem = signatureDoc.createElementNS(
13801372
signatureNamespace,

test/signature-object-tests.spec.ts

Lines changed: 283 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,288 @@ describe("Object support in XML signatures", function () {
229229
expect(isValid).to.be.true;
230230
});
231231

232+
it("should sign Object with SHA256 digest algorithm", function () {
233+
// Create a simple XML document to sign
234+
const xml = '<root><x xmlns="ns"></x><y attr="value"></y><z><w></w></z></root>';
235+
236+
// Create a SignedXml instance with custom getObjectContent
237+
const sig = new SignedXml({
238+
getObjectContent: () => [
239+
{
240+
content: "<Data>Test data for SHA256 digest</Data>",
241+
attributes: {
242+
Id: "object1",
243+
MimeType: "text/xml",
244+
},
245+
},
246+
],
247+
});
248+
249+
// Set up the signature
250+
sig.privateKey = fs.readFileSync("./test/static/client.pem");
251+
252+
// Add a reference to the document element
253+
sig.addReference({
254+
xpath: "//*[local-name(.)='x']",
255+
digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
256+
transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
257+
});
258+
259+
// Add a reference to the Object element with SHA256
260+
sig.addReference({
261+
xpath: "//*[@Id='object1']",
262+
digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
263+
transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
264+
isSignatureReference: true,
265+
});
266+
267+
// Set required algorithms
268+
sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
269+
sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
270+
271+
// Compute the signature
272+
sig.computeSignature(xml);
273+
274+
// Get the signed XML
275+
const signedXml = sig.getSignedXml();
276+
277+
// Parse the signed XML
278+
const doc = new xmldom.DOMParser().parseFromString(signedXml);
279+
280+
// Verify that the ds:Object element exists
281+
const objectNodes = xpath.select("//*[local-name(.)='Object']", doc);
282+
isDomNode.assertIsArrayOfNodes(objectNodes);
283+
expect(objectNodes.length).to.equal(1);
284+
285+
// Verify that there are two Reference elements
286+
const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc);
287+
isDomNode.assertIsArrayOfNodes(referenceNodes);
288+
expect(referenceNodes.length).to.equal(2);
289+
290+
// Verify that the references use SHA256
291+
const digestMethodNodes = xpath.select("//*[local-name(.)='DigestMethod']", doc);
292+
isDomNode.assertIsArrayOfNodes(digestMethodNodes);
293+
294+
for (const digestMethod of digestMethodNodes) {
295+
isDomNode.assertIsElementNode(digestMethod);
296+
expect(digestMethod.getAttribute("Algorithm")).to.equal(
297+
"http://www.w3.org/2001/04/xmlenc#sha256",
298+
);
299+
}
300+
301+
// Verify that the signature method is RSA-SHA256
302+
const signatureMethod = xpath.select1("//*[local-name(.)='SignatureMethod']", doc);
303+
isDomNode.assertIsElementNode(signatureMethod);
304+
expect(signatureMethod.getAttribute("Algorithm")).to.equal(
305+
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
306+
);
307+
308+
// Verify that the signature is still valid
309+
const verifier = new SignedXml();
310+
verifier.publicCert = fs.readFileSync("./test/static/client_public.pem");
311+
312+
// First load the signature
313+
const signatureNode = xpath.select1(
314+
"//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
315+
doc,
316+
);
317+
isDomNode.assertIsNodeLike(signatureNode);
318+
verifier.loadSignature(signatureNode);
319+
320+
// Then check the signature
321+
const isValid = verifier.checkSignature(signedXml);
322+
expect(isValid).to.be.true;
323+
});
324+
325+
it("should sign Object with SHA512 digest algorithm and RSA-SHA512 signature", function () {
326+
// Create a simple XML document to sign
327+
const xml = '<root><x xmlns="ns"></x><y attr="value"></y><z><w></w></z></root>';
328+
329+
// Create a SignedXml instance with custom getObjectContent
330+
const sig = new SignedXml({
331+
getObjectContent: () => [
332+
{
333+
content: "<Data>Test data for SHA512 digest</Data>",
334+
attributes: {
335+
Id: "object1",
336+
MimeType: "text/xml",
337+
},
338+
},
339+
],
340+
});
341+
342+
// Set up the signature
343+
sig.privateKey = fs.readFileSync("./test/static/client.pem");
344+
345+
// Add a reference to the document element
346+
sig.addReference({
347+
xpath: "//*[local-name(.)='x']",
348+
digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha512",
349+
transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
350+
});
351+
352+
// Add a reference to the Object element with SHA512
353+
sig.addReference({
354+
xpath: "//*[@Id='object1']",
355+
digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha512",
356+
transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
357+
isSignatureReference: true,
358+
});
359+
360+
// Set required algorithms
361+
sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
362+
sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512";
363+
364+
// Compute the signature
365+
sig.computeSignature(xml);
366+
367+
// Get the signed XML
368+
const signedXml = sig.getSignedXml();
369+
370+
// Parse the signed XML
371+
const doc = new xmldom.DOMParser().parseFromString(signedXml);
372+
373+
// Verify that the ds:Object element exists
374+
const objectNodes = xpath.select("//*[local-name(.)='Object']", doc);
375+
isDomNode.assertIsArrayOfNodes(objectNodes);
376+
expect(objectNodes.length).to.equal(1);
377+
378+
// Verify that there are two Reference elements
379+
const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc);
380+
isDomNode.assertIsArrayOfNodes(referenceNodes);
381+
expect(referenceNodes.length).to.equal(2);
382+
383+
// Verify that the references use SHA512
384+
const digestMethodNodes = xpath.select("//*[local-name(.)='DigestMethod']", doc);
385+
isDomNode.assertIsArrayOfNodes(digestMethodNodes);
386+
387+
for (const digestMethod of digestMethodNodes) {
388+
isDomNode.assertIsElementNode(digestMethod);
389+
expect(digestMethod.getAttribute("Algorithm")).to.equal(
390+
"http://www.w3.org/2001/04/xmlenc#sha512",
391+
);
392+
}
393+
394+
// Verify that the signature method is RSA-SHA512
395+
const signatureMethod = xpath.select1("//*[local-name(.)='SignatureMethod']", doc);
396+
isDomNode.assertIsElementNode(signatureMethod);
397+
expect(signatureMethod.getAttribute("Algorithm")).to.equal(
398+
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
399+
);
400+
401+
// Verify that the signature is still valid
402+
const verifier = new SignedXml();
403+
verifier.publicCert = fs.readFileSync("./test/static/client_public.pem");
404+
405+
// First load the signature
406+
const signatureNode = xpath.select1(
407+
"//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
408+
doc,
409+
);
410+
isDomNode.assertIsNodeLike(signatureNode);
411+
verifier.loadSignature(signatureNode);
412+
413+
// Then check the signature
414+
const isValid = verifier.checkSignature(signedXml);
415+
expect(isValid).to.be.true;
416+
});
417+
418+
it("should sign Object with C14N canonicalization algorithm", function () {
419+
// Create a simple XML document to sign
420+
const xml = '<root><x xmlns="ns"></x><y attr="value"></y><z><w></w></z></root>';
421+
422+
// Create a SignedXml instance with custom getObjectContent
423+
const sig = new SignedXml({
424+
getObjectContent: () => [
425+
{
426+
content: "<Data>Test data for C14N canonicalization</Data>",
427+
attributes: {
428+
Id: "object1",
429+
MimeType: "text/xml",
430+
},
431+
},
432+
],
433+
});
434+
435+
// Set up the signature
436+
sig.privateKey = fs.readFileSync("./test/static/client.pem");
437+
438+
// Add a reference to the document element
439+
sig.addReference({
440+
xpath: "//*[local-name(.)='x']",
441+
digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
442+
transforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"],
443+
});
444+
445+
// Add a reference to the Object element
446+
sig.addReference({
447+
xpath: "//*[@Id='object1']",
448+
digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
449+
transforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"],
450+
isSignatureReference: true,
451+
});
452+
453+
// Set required algorithms
454+
sig.canonicalizationAlgorithm = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
455+
sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
456+
457+
// Compute the signature
458+
sig.computeSignature(xml);
459+
460+
// Get the signed XML
461+
const signedXml = sig.getSignedXml();
462+
463+
// Parse the signed XML
464+
const doc = new xmldom.DOMParser().parseFromString(signedXml);
465+
466+
// Verify that the ds:Object element exists
467+
const objectNodes = xpath.select("//*[local-name(.)='Object']", doc);
468+
isDomNode.assertIsArrayOfNodes(objectNodes);
469+
expect(objectNodes.length).to.equal(1);
470+
471+
// Verify that there are two Reference elements
472+
const referenceNodes = xpath.select("//*[local-name(.)='Reference']", doc);
473+
isDomNode.assertIsArrayOfNodes(referenceNodes);
474+
expect(referenceNodes.length).to.equal(2);
475+
476+
// Verify that the transforms use C14N
477+
const transforms = xpath.select(
478+
"//*[local-name(.)='Reference']/*[local-name(.)='Transforms']/*[local-name(.)='Transform']",
479+
doc,
480+
);
481+
isDomNode.assertIsArrayOfNodes(transforms);
482+
483+
for (const transform of transforms) {
484+
isDomNode.assertIsElementNode(transform);
485+
expect(transform.getAttribute("Algorithm")).to.equal(
486+
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
487+
);
488+
}
489+
490+
// Verify that the CanonicalizationMethod is C14N
491+
const canonMethod = xpath.select1("//*[local-name(.)='CanonicalizationMethod']", doc);
492+
isDomNode.assertIsElementNode(canonMethod);
493+
expect(canonMethod.getAttribute("Algorithm")).to.equal(
494+
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
495+
);
496+
497+
// Verify that the signature is still valid
498+
const verifier = new SignedXml();
499+
verifier.publicCert = fs.readFileSync("./test/static/client_public.pem");
500+
501+
// First load the signature
502+
const signatureNode = xpath.select1(
503+
"//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
504+
doc,
505+
);
506+
isDomNode.assertIsNodeLike(signatureNode);
507+
verifier.loadSignature(signatureNode);
508+
509+
// Then check the signature
510+
const isValid = verifier.checkSignature(signedXml);
511+
expect(isValid).to.be.true;
512+
});
513+
232514
it("should add a reference to an Object element", function () {
233515
// Create a simple XML document to sign
234516
const xml = '<root><x xmlns="ns"></x><y attr="value"></y><z><w></w></z></root>';
@@ -259,7 +541,6 @@ describe("Object support in XML signatures", function () {
259541
// Add a reference to the Object element by its ID
260542
sig.addReference({
261543
xpath: "//*[@Id='object1']",
262-
uri: "#object1",
263544
digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
264545
transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
265546
isSignatureReference: true,
@@ -347,7 +628,7 @@ describe("Object support in XML signatures", function () {
347628

348629
// Add a reference to the Object element by its ID only, marking it as a signature reference
349630
sig.addReference({
350-
uri: "#object1",
631+
xpath: "//*[@Id='object1']",
351632
digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
352633
transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
353634
isSignatureReference: true,

0 commit comments

Comments
 (0)