Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial code to sign keyinfo #464

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
70 changes: 57 additions & 13 deletions src/signed-xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand All @@ -865,7 +902,6 @@ export class SignedXml {
return;
}
}
const signedInfoNode = signedInfoNodes[0];

if (typeof callback === "function") {
// Asynchronous flow
Expand All @@ -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);
Expand All @@ -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();
}
Expand All @@ -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}<fake>2</fake></${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 "";
Expand All @@ -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}`,
);
Expand All @@ -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}">`;
Expand Down Expand Up @@ -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.
});
}
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
92 changes: 92 additions & 0 deletions test/signature-unit-tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,64 @@
).to.equal("Signature");
});

it("signer appends signature to all root node children when helper function is used", function () {
const xml = "<root><name>xml-crypto</name><repository><name>github</name></repository></root>";
const doc = new xmldom.DOMParser().parseFromString(xml);

Check failure on line 192 in test/signature-unit-tests.spec.ts

View workflow job for this annotation

GitHub Actions / Lint Code

'doc' is assigned a value but never used
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(
'<root><name Id="_0">xml-crypto</name><repository Id="_1"><name>github</name></repository><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#_0"><Transforms><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>q2ONs+Sgbm1r4eFxQPyMGHWGay8=</DigestValue></Reference><Reference URI="#_1"><Transforms><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>SoATUdmNo4CzkYtBMqktRZYdw/0=</DigestValue></Reference></SignedInfo><SignatureValue>L2ghNuRUydddUrpL6OynnGW9JpNxmGANDXL90w4jXGiVPukTM6kn5dzQoTVlKokm5bzEtxibWRLJpeqLYoBlH7g2foIEoKpAIqa3I1an78BRrR/VjRzqT/QKrGtivgHgRID9FWCuZdMmP4h+RMA4t753iH/gsxts4OhrymxJayI=</SignatureValue><KeyInfo><X509Data><X509Certificate>MIIBxDCCAW6gAwIBAgIQxUSXFzWJYYtOZnmmuOMKkjANBgkqhkiG9w0BAQQFADAWMRQwEgYDVQQDEwtSb290IEFnZW5jeTAeFw0wMzA3MDgxODQ3NTlaFw0zOTEyMzEyMzU5NTlaMB8xHTAbBgNVBAMTFFdTRTJRdWlja1N0YXJ0Q2xpZW50MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+L6aB9x928noY4+0QBsXnxkQE4quJl7c3PUPdVu7k9A02hRG481XIfWhrDY5i7OEB7KGW7qFJotLLeMec/UkKUwCgv3VvJrs2nE9xO3SSWIdNzADukYh+Cxt+FUU6tUkDeqg7dqwivOXhuOTRyOI3HqbWTbumaLdc8jufz2LhaQIDAQABo0swSTBHBgNVHQEEQDA+gBAS5AktBh0dTwCNYSHcFmRjoRgwFjEUMBIGA1UEAxMLUm9vdCBBZ2VuY3mCEAY3bACqAGSKEc+41KpcNfQwDQYJKoZIhvcNAQEEBQADQQAfIbnMPVYkNNfX1tG1F+qfLhHwJdfDUZuPyRPucWF5qkh6sSdWVBY5sT/txBnVJGziyO8DPYdu2fPMER8ajJfl</X509Certificate></X509Data></KeyInfo></Signature></root>',
);
expect(sig.getSignedXml()).to.not.equal(
'<root><name Id="_0">xml-crypto</name><repository Id="_2"><name Id="_1">github</name></repository><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#_0"><Transforms><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>q2ONs+Sgbm1r4eFxQPyMGHWGay8=</DigestValue></Reference><Reference URI="#_1"><Transforms><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>1vawhQI+FPhqoGY3rXSh1pwtuyI=</DigestValue></Reference><Reference URI="#_2"><Transforms><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>GlYi55Sr0zYtef64F6Wy2MKsCYw=</DigestValue></Reference></SignedInfo><SignatureValue>n3yP4E3+0qyW/siPxzKU0RTetibusDU5n4OZ1Y0kV+N+qEh57Bj4Jk87RlvHFH0CNq0IJ8fJ+yyfW0d//WNSwHU1DIZkHFdl41M3S1pZWOsPPPCV+4ByCvJBn20enE/zY4okIyeU84PP081oSZMT5RtFsIbjJ6WtXEYZZq31dGs=</SignatureValue><KeyInfo><X509Data><X509Certificate>MIIBxDCCAW6gAwIBAgIQxUSXFzWJYYtOZnmmuOMKkjANBgkqhkiG9w0BAQQFADAWMRQwEgYDVQQDEwtSb290IEFnZW5jeTAeFw0wMzA3MDgxODQ3NTlaFw0zOTEyMzEyMzU5NTlaMB8xHTAbBgNVBAMTFFdTRTJRdWlja1N0YXJ0Q2xpZW50MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+L6aB9x928noY4+0QBsXnxkQE4quJl7c3PUPdVu7k9A02hRG481XIfWhrDY5i7OEB7KGW7qFJotLLeMec/UkKUwCgv3VvJrs2nE9xO3SSWIdNzADukYh+Cxt+FUU6tUkDeqg7dqwivOXhuOTRyOI3HqbWTbumaLdc8jufz2LhaQIDAQABo0swSTBHBgNVHQEEQDA+gBAS5AktBh0dTwCNYSHcFmRjoRgwFjEUMBIGA1UEAxMLUm9vdCBBZ2VuY3mCEAY3bACqAGSKEc+41KpcNfQwDQYJKoZIhvcNAQEEBQADQQAfIbnMPVYkNNfX1tG1F+qfLhHwJdfDUZuPyRPucWF5qkh6sSdWVBY5sT/txBnVJGziyO8DPYdu2fPMER8ajJfl</X509Certificate></X509Data></KeyInfo></Signature></root>',
);
});

it("signer adds a reference to KeyInfo when it is included", function () {
const xml = "<root><name>xml-crypto</name><repository>github</repository></root>";
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 = "<root><name>xml-crypto</name><repository>github</repository></root>";
const sig = new SignedXml();
Expand Down Expand Up @@ -856,6 +914,16 @@
});
});

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);
Expand Down Expand Up @@ -965,6 +1033,30 @@
failInvalidSignature("./test/static/invalid_signature_without_transforms_element.xml");
});
});

describe("pass check known signature", function () {
it("should check to make sure that <KeyInfo> 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 () {
Expand Down
Loading