diff --git a/src/signed-xml.ts b/src/signed-xml.ts
index e5d80af..322e2d0 100644
--- a/src/signed-xml.ts
+++ b/src/signed-xml.ts
@@ -637,6 +637,28 @@ export class SignedXml {
});
}
+ addReferenceToAllRootChildren(
+ doc: Document,
+ transforms: CanonicalizationOrTransformAlgorithmType[],
+ ) {
+ const root = doc.documentElement;
+ if (root == null) {
+ throw new Error("Document has no root element");
+ }
+
+ const children = root.childNodes;
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i];
+ if (isDomNode.isElementNode(child)) {
+ this.addReference({
+ xpath: `./*/*[local-name(.)='${child.localName}']`,
+ transforms,
+ digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
+ });
+ }
+ }
+ }
+
/**
* Adds a reference to the signature.
*
@@ -798,10 +820,19 @@ export class SignedXml {
// add the xml namespace attribute
signatureAttrs.push(`${xmlNsAttr}="http://www.w3.org/2000/09/xmldsig#"`);
+ const keyInfo = this.getKeyInfo(prefix);
+ if (keyInfo != null && keyInfo.length > 0) {
+ // this.addReference({
+ // xpath: ".//*[local-name(.)='KeyInfo']",
+ // transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
+ // digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
+ // });
+ }
+
let signatureXml = `<${currentPrefix}Signature ${signatureAttrs.join(" ")}>`;
- signatureXml += this.createSignedInfo(doc, prefix);
- signatureXml += this.getKeyInfo(prefix);
+ // signatureXml += this.createSignedInfo(doc, prefix);
+ signatureXml += keyInfo;
signatureXml += `${currentPrefix}Signature>`;
this.originalXmlWithIds = doc.toString();
@@ -854,6 +885,12 @@ export class SignedXml {
referenceNode.parentNode.insertBefore(signatureDoc, referenceNode.nextSibling);
}
+ // Now that we've inserted the Signature node into the document
+ // we need to calculate the SignedInfo and insert it into the Signature element
+ const signedInfoXml = this.createSignedInfo(doc, prefix);
+ const signedInfoElement = new xmldom.DOMParser().parseFromString(signedInfoXml).documentElement;
+ signatureDoc.insertBefore(signedInfoElement, signatureDoc.firstChild);
+
this.signatureNode = signatureDoc;
const signedInfoNodes = utils.findChildren(this.signatureNode, "SignedInfo");
if (signedInfoNodes.length === 0) {
@@ -865,7 +902,6 @@ export class SignedXml {
return;
}
}
- const signedInfoNode = signedInfoNodes[0];
if (typeof callback === "function") {
// Asynchronous flow
@@ -874,7 +910,7 @@ export class SignedXml {
callback(err);
} else {
this.signatureValue = signature || "";
- signatureDoc.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling);
+ signatureDoc.insertBefore(this.createSignature(prefix), signedInfoElement.nextSibling);
this.signatureXml = signatureDoc.toString();
this.signedXml = doc.toString();
callback(null, this);
@@ -883,7 +919,7 @@ export class SignedXml {
} else {
// Synchronous flow
this.calculateSignatureValue(doc);
- signatureDoc.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling);
+ signatureDoc.insertBefore(this.createSignature(prefix), signedInfoElement.nextSibling);
this.signatureXml = signatureDoc.toString();
this.signedXml = doc.toString();
}
@@ -901,7 +937,14 @@ export class SignedXml {
const keyInfoContent = this.getKeyInfoContent({ publicCert: this.publicCert, prefix });
if (keyInfoAttrs || keyInfoContent) {
- return `<${currentPrefix}KeyInfo${keyInfoAttrs}>${keyInfoContent}${currentPrefix}KeyInfo>`;
+ const keyInfoXml = `<${currentPrefix}KeyInfo${keyInfoAttrs}>${keyInfoContent}2${currentPrefix}KeyInfo>`;
+
+ // The following is if we want to always include an `Id` attribute in the `KeyInfo` element
+ // const keyInfoDoc = new xmldom.DOMParser().parseFromString(keyInfoXml);
+ // this.ensureHasId(keyInfoDoc.documentElement);
+ // return keyInfoDoc.toString();
+
+ return keyInfoXml;
}
return "";
@@ -920,7 +963,7 @@ export class SignedXml {
for (const ref of this.getReferences()) {
const nodes = xpath.selectWithResolver(ref.xpath ?? "", doc, this.namespaceResolver);
- if (!utils.isArrayHasLength(nodes)) {
+ if (!utils.isArrayHasLength(nodes) || nodes.some((node) => !isDomNode.isElementNode(node))) {
throw new Error(
`the following xpath cannot be signed because it was not found: ${ref.xpath}`,
);
@@ -930,6 +973,7 @@ export class SignedXml {
if (ref.isEmptyUri) {
res += `<${prefix}Reference URI="">`;
} else {
+ isDomNode.assertIsElementNode(node)
const id = this.ensureHasId(node);
ref.uri = id;
res += `<${prefix}Reference URI="#${id}">`;
@@ -996,18 +1040,18 @@ export class SignedXml {
* Ensure an element has Id attribute. If not create it with unique value.
* Work with both normal and wssecurity Id flavour
*/
- private ensureHasId(node) {
+ private ensureHasId(elem: Element) {
let attr;
if (this.idMode === "wssecurity") {
attr = utils.findAttr(
- node,
+ elem,
"Id",
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
);
} else {
this.idAttributes.some((idAttribute) => {
- attr = utils.findAttr(node, idAttribute);
+ attr = utils.findAttr(elem, idAttribute);
return !!attr; // This will break the loop as soon as a truthy attr is found.
});
}
@@ -1020,18 +1064,18 @@ export class SignedXml {
const id = `_${this.id++}`;
if (this.idMode === "wssecurity") {
- node.setAttributeNS(
+ elem.setAttributeNS(
"http://www.w3.org/2000/xmlns/",
"xmlns:wsu",
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
);
- node.setAttributeNS(
+ elem.setAttributeNS(
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
"wsu:Id",
id,
);
} else {
- node.setAttribute("Id", id);
+ elem.setAttribute("Id", id);
}
return id;
diff --git a/src/utils.ts b/src/utils.ts
index 6098286..d22b56c 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -18,7 +18,7 @@ function attrEqualsImplicitly(attr: Attr, localName: string, namespace?: string,
}
export function findAttr(element: Element, localName: string, namespace?: string) {
- for (let i = 0; i < element.attributes.length; i++) {
+ for (let i = 0; i < element.attributes?.length; i++) {
const attr = element.attributes[i];
if (
diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts
index 0db89a4..bd6e85a 100644
--- a/test/signature-unit-tests.spec.ts
+++ b/test/signature-unit-tests.spec.ts
@@ -187,6 +187,64 @@ describe("Signature unit tests", function () {
).to.equal("Signature");
});
+ it("signer appends signature to all root node children when helper function is used", function () {
+ const xml = "xml-cryptogithub";
+ const doc = new xmldom.DOMParser().parseFromString(xml);
+ const sig = new SignedXml();
+
+ sig.privateKey = fs.readFileSync("./test/static/client.pem");
+ sig.publicCert = fs.readFileSync("./test/static/client_public.pem");
+ // sig.addReferenceToAllRootChildren(doc, ["http://www.w3.org/2001/10/xml-exc-c14n#"]);
+ sig.addReference({
+ xpath: "/*",
+ digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
+ transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
+ });
+
+ sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
+ sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
+ sig.computeSignature(xml);
+
+ const signatureElement = new xmldom.DOMParser().parseFromString(
+ sig.getSignatureXml(),
+ ).documentElement;
+ const signatureValueNode = xpath.select1(
+ ".//*[local-name(.)='SignatureValue']/text()",
+ signatureElement,
+ );
+ isDomNode.assertIsTextNode(signatureValueNode);
+ const signatureValue = signatureValueNode.textContent;
+ expect(signatureValue).to.equal(
+ "GPMBtom1Qos8OXyAK2VdUadRaipPTjJpyOpO+6msrQM54L4OullXbpoEf/n7BCrRLzXUarw5xpxiBYOR4gFtTOOMD0LUN2zIQEdO87mjxhqoo8iVEHjuluILj2lEmhNYDsyQZfd7uUD2k5OgpBhpi9fgs+7w0N43iTfTiJ+4qC4=",
+ );
+ expect(sig.getSignedXml()).to.equal(
+ 'xml-cryptogithubq2ONs+Sgbm1r4eFxQPyMGHWGay8=SoATUdmNo4CzkYtBMqktRZYdw/0=L2ghNuRUydddUrpL6OynnGW9JpNxmGANDXL90w4jXGiVPukTM6kn5dzQoTVlKokm5bzEtxibWRLJpeqLYoBlH7g2foIEoKpAIqa3I1an78BRrR/VjRzqT/QKrGtivgHgRID9FWCuZdMmP4h+RMA4t753iH/gsxts4OhrymxJayI=MIIBxDCCAW6gAwIBAgIQxUSXFzWJYYtOZnmmuOMKkjANBgkqhkiG9w0BAQQFADAWMRQwEgYDVQQDEwtSb290IEFnZW5jeTAeFw0wMzA3MDgxODQ3NTlaFw0zOTEyMzEyMzU5NTlaMB8xHTAbBgNVBAMTFFdTRTJRdWlja1N0YXJ0Q2xpZW50MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+L6aB9x928noY4+0QBsXnxkQE4quJl7c3PUPdVu7k9A02hRG481XIfWhrDY5i7OEB7KGW7qFJotLLeMec/UkKUwCgv3VvJrs2nE9xO3SSWIdNzADukYh+Cxt+FUU6tUkDeqg7dqwivOXhuOTRyOI3HqbWTbumaLdc8jufz2LhaQIDAQABo0swSTBHBgNVHQEEQDA+gBAS5AktBh0dTwCNYSHcFmRjoRgwFjEUMBIGA1UEAxMLUm9vdCBBZ2VuY3mCEAY3bACqAGSKEc+41KpcNfQwDQYJKoZIhvcNAQEEBQADQQAfIbnMPVYkNNfX1tG1F+qfLhHwJdfDUZuPyRPucWF5qkh6sSdWVBY5sT/txBnVJGziyO8DPYdu2fPMER8ajJfl',
+ );
+ expect(sig.getSignedXml()).to.not.equal(
+ 'xml-cryptogithubq2ONs+Sgbm1r4eFxQPyMGHWGay8=1vawhQI+FPhqoGY3rXSh1pwtuyI=GlYi55Sr0zYtef64F6Wy2MKsCYw=n3yP4E3+0qyW/siPxzKU0RTetibusDU5n4OZ1Y0kV+N+qEh57Bj4Jk87RlvHFH0CNq0IJ8fJ+yyfW0d//WNSwHU1DIZkHFdl41M3S1pZWOsPPPCV+4ByCvJBn20enE/zY4okIyeU84PP081oSZMT5RtFsIbjJ6WtXEYZZq31dGs=MIIBxDCCAW6gAwIBAgIQxUSXFzWJYYtOZnmmuOMKkjANBgkqhkiG9w0BAQQFADAWMRQwEgYDVQQDEwtSb290IEFnZW5jeTAeFw0wMzA3MDgxODQ3NTlaFw0zOTEyMzEyMzU5NTlaMB8xHTAbBgNVBAMTFFdTRTJRdWlja1N0YXJ0Q2xpZW50MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+L6aB9x928noY4+0QBsXnxkQE4quJl7c3PUPdVu7k9A02hRG481XIfWhrDY5i7OEB7KGW7qFJotLLeMec/UkKUwCgv3VvJrs2nE9xO3SSWIdNzADukYh+Cxt+FUU6tUkDeqg7dqwivOXhuOTRyOI3HqbWTbumaLdc8jufz2LhaQIDAQABo0swSTBHBgNVHQEEQDA+gBAS5AktBh0dTwCNYSHcFmRjoRgwFjEUMBIGA1UEAxMLUm9vdCBBZ2VuY3mCEAY3bACqAGSKEc+41KpcNfQwDQYJKoZIhvcNAQEEBQADQQAfIbnMPVYkNNfX1tG1F+qfLhHwJdfDUZuPyRPucWF5qkh6sSdWVBY5sT/txBnVJGziyO8DPYdu2fPMER8ajJfl',
+ );
+ });
+
+ it("signer adds a reference to KeyInfo when it is included", function () {
+ const xml = "xml-cryptogithub";
+ const sig = new SignedXml();
+
+ sig.privateKey = fs.readFileSync("./test/static/client.pem");
+ sig.publicCert = fs.readFileSync("./test/static/client_public.pem");
+
+ sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
+ sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
+ sig.computeSignature(xml);
+
+ const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml());
+ const keyInfoNode = xpath.select1("//KeyInfo", doc);
+
+ isDomNode.assertIsNodeLike(keyInfoNode);
+ const x509DataNode = xpath.select1("//X509Data", keyInfoNode);
+
+ isDomNode.assertIsNodeLike(x509DataNode);
+ });
+
it("signer appends signature to a reference node", function () {
const xml = "xml-cryptogithub";
const sig = new SignedXml();
@@ -856,6 +914,16 @@ describe("Signature unit tests", function () {
});
});
+ describe("fail loading signatures", function () {
+ it("should be unable to load a signature if the CanonicalizationMethod is not missing", function () {
+ const xml = fs.readFileSync("./test/static/valid_signature.xml", "utf8");
+ const doc = new xmldom.DOMParser().parseFromString(xml);
+
+ const sig = new SignedXml();
+ expect(sig.loadSignature(sig.findSignatures(doc)[0])).to.throw;
+ });
+ });
+
describe("pass verify signature", function () {
function verifySignature(xml: string, idMode?: "wssecurity") {
const doc = new xmldom.DOMParser().parseFromString(xml);
@@ -965,6 +1033,30 @@ describe("Signature unit tests", function () {
failInvalidSignature("./test/static/invalid_signature_without_transforms_element.xml");
});
});
+
+ describe("pass check known signature", function () {
+ it("should check to make sure that signature is in list of expected signature", function () {
+ const xml = fs.readFileSync("./test/static/valid_signature.xml", "utf8");
+ const doc = new xmldom.DOMParser().parseFromString(xml);
+ const node = xpath.select1(
+ "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
+ doc,
+ );
+ isDomNode.assertIsNodeLike(node);
+ const sig = new SignedXml();
+ const publicCert = fs.readFileSync("./test/static/client_public.pem");
+ sig.publicCert = publicCert;
+ sig.loadSignature(node);
+ try {
+ const res = sig.checkSignature(xml);
+ const foundCert = sig.getCertFromKeyInfo();
+ expect(foundCert).to.equal(sig.publicCert);
+ return res;
+ } catch (e) {
+ return false;
+ }
+ });
+ });
});
it("allow empty reference uri when signing", function () {