Skip to content

Commit

Permalink
Add method for checking if element is signed
Browse files Browse the repository at this point in the history
  • Loading branch information
cjbarth committed Jul 28, 2023
1 parent 2e9cbf1 commit 8f01659
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 47 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,19 @@ If the verification process fails `sig.validationErrors` will contain the errors
In order to protect from some attacks we must check the content we want to use is the one that has been signed:

```javascript
var elem = select(doc, "/xpath_to_interesting_element");
// Roll your own
var elem = xpath.select("/xpath_to_interesting_element", doc);
var uri = sig.references[0].uri; // might not be 0 - depending on the document you verify
var id = uri[0] === "#" ? uri.substring(1) : uri;
if (elem.getAttribute("ID") != id && elem.getAttribute("Id") != id && elem.getAttribute("id") != id)
throw new Error("the interesting element was not the one verified by the signature");

// Use the built-in method
let elem = xpath.select("/xpath_to_interesting_element", doc);
const matchingReference = sig.validateElementAgainstReferences(elem, doc);
if (!matchingReference) {
throw new Error("the interesting element was not the one verified by the signature");
}
```

Note:
Expand Down
40 changes: 38 additions & 2 deletions src/signed-xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,23 @@ export class SignedXml {
/**
* Specifies the data to be signed within an XML document. See {@link Reference}
*/
private references: Reference[] = [];
private id = 0;
private signedXml = "";
private signatureXml = "";
private signatureNode: Node | null = null;
private signatureValue = "";
private originalXmlWithIds = "";
private keyInfo: Node | null = null;

/**
* Contains the references that were signed. See {@link Reference}
*/
references: Reference[] = [];

/**
* Contains validation errors (if any) after {@link checkSignature} method is called
*/
validationErrors: string[] = [];
private keyInfo: Node | null = null;

/**
* To add a new transformation algorithm create a new class that implements the {@link TransformationAlgorithm} interface, and register it here. More info: {@link https://github.com/node-saml/xml-crypto#customizing-algorithms|Customizing Algorithms}
Expand Down Expand Up @@ -389,6 +394,37 @@ export class SignedXml {
}
}

validateElementAgainstReferences(elem: Element, doc: Document): Reference | false {
for (const ref of this.references) {
const uri = ref.uri?.[0] === "#" ? ref.uri.substring(1) : ref.uri;
let targetElem: xpath.SelectSingleReturnType;

for (const attr of this.idAttributes) {
const elemId = elem.getAttribute(attr);
if (uri === elemId) {
targetElem = elem;
ref.xpath = `//*[@*[local-name(.)='${attr}']='${uri}']`;
break; // found the correct element, no need to check further
}
}

// @ts-expect-error This is a problem with the types on `xpath`
if (!xpath.isNodeLike(targetElem)) {
continue;

Check warning on line 413 in src/signed-xml.ts

View check run for this annotation

Codecov / codecov/patch

src/signed-xml.ts#L413

Added line #L413 was not covered by tests
}

const canonXml = this.getCanonReferenceXml(doc, ref, targetElem);
const hash = this.findHashAlgorithm(ref.digestAlgorithm);
const digest = hash.getHash(canonXml);

if (utils.validateDigestValue(digest, ref.digestValue)) {
return ref;
}
}

return false; // No references passed validation

Check warning on line 425 in src/signed-xml.ts

View check run for this annotation

Codecov / codecov/patch

src/signed-xml.ts#L425

Added line #L425 was not covered by tests
}

validateReferences(doc) {
for (const ref of this.references) {
let elemXpath;
Expand Down
109 changes: 66 additions & 43 deletions test/signature-unit-tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,60 +32,85 @@ describe("Signature unit tests", function () {
expect(res, "expected signature to be valid, but it was reported invalid").to.equal(true);
}

function passLoadSignature(file, toString) {
function passLoadSignature(file: string, toString?: boolean) {
const xml = fs.readFileSync(file, "utf8");
const doc = new xmldom.DOMParser().parseFromString(xml);
const node = xpath.select1(
const signature = xpath.select1(
"/*//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
doc,
);
const sig = new SignedXml();
// @ts-expect-error FIXME
sig.loadSignature(toString ? node.toString() : node);
if (xpath.isElement(signature)) {
const sig = new SignedXml();
sig.loadSignature(toString ? signature.toString() : signature);

expect(sig.canonicalizationAlgorithm, "wrong canonicalization method").to.equal(
"http://www.w3.org/2001/10/xml-exc-c14n#",
);
expect(sig.canonicalizationAlgorithm, "wrong canonicalization method").to.equal(
"http://www.w3.org/2001/10/xml-exc-c14n#",
);

expect(sig.signatureAlgorithm, "wrong signature method").to.equal(
"http://www.w3.org/2000/09/xmldsig#rsa-sha1",
);
expect(sig.signatureAlgorithm, "wrong signature method").to.equal(
"http://www.w3.org/2000/09/xmldsig#rsa-sha1",
);

// @ts-expect-error FIXME
expect(sig.signatureValue, "wrong signature value").to.equal(
"PI2xGt3XrVcxYZ34Kw7nFdq75c7Mmo7J0q7yeDhBprHuJal/KV9KyKG+Zy3bmQIxNwkPh0KMP5r1YMTKlyifwbWK0JitRCSa0Fa6z6+TgJi193yiR5S1MQ+esoQT0RzyIOBl9/GuJmXx/1rXnqrTxmL7UxtqKuM29/eHwF0QDUI=",
);
sig.getCertFromKeyInfo = (keyInfo) => {
// @ts-expect-error FIXME
if (xpath.isNodeLike(keyInfo)) {
const keyInfoContents = xpath.select1(
"//*[local-name(.)='KeyInfo']/*[local-name(.)='dummyKey']",
keyInfo,
);
if (xpath.isNodeLike(keyInfoContents)) {
const firstChild = keyInfoContents.firstChild;
if (xpath.isTextNode(firstChild)) {
expect(firstChild.data, "keyInfo clause not correctly loaded").to.equal("1234");
} else {
expect(xpath.isTextNode(firstChild), "keyInfo has improper format").to.be.true;
}
} else {
expect(xpath.isNodeLike(keyInfoContents), "KeyInfo contents not found").to.be.true;
}
} else {
// @ts-expect-error FIXME
expect(xpath.isNodeLike(keyInfo), "KeyInfo not found").to.be.true;
}

return fs.readFileSync("./test/static/client.pem", "latin1");
};

const keyInfo = xpath.select1(
"//*[local-name(.)='KeyInfo']/*[local-name(.)='dummyKey']",
// @ts-expect-error FIXME
sig.keyInfo,
);
// @ts-expect-error FIXME
expect(keyInfo.firstChild.data, "keyInfo clause not correctly loaded").to.equal("1234");
const checkedSignature = sig.checkSignature(xml);
expect(checkedSignature).to.be.true;

// @ts-expect-error FIXME
expect(sig.references.length).to.equal(3);
expect(sig.references.length).to.equal(3);

const digests = [
"b5GCZ2xpP5T7tbLWBTkOl4CYupQ=",
"K4dI497ZCxzweDIrbndUSmtoezY=",
"sH1gxKve8wlU8LlFVa2l6w3HMJ0=",
];
const digests = [
"b5GCZ2xpP5T7tbLWBTkOl4CYupQ=",
"K4dI497ZCxzweDIrbndUSmtoezY=",
"sH1gxKve8wlU8LlFVa2l6w3HMJ0=",
];

// @ts-expect-error FIXME
for (let i = 0; i < sig.references.length; i++) {
const firstGrandchild = doc.firstChild?.firstChild;
// @ts-expect-error FIXME
const ref = sig.references[i];
const expectedUri = `#_${i}`;
expect(
ref.uri,
`wrong uri for index ${i}. expected: ${expectedUri} actual: ${ref.uri}`,
).to.equal(expectedUri);
expect(ref.transforms.length).to.equal(1);
expect(ref.transforms[0]).to.equal("http://www.w3.org/2001/10/xml-exc-c14n#");
expect(ref.digestValue).to.equal(digests[i]);
expect(ref.digestAlgorithm).to.equal("http://www.w3.org/2000/09/xmldsig#sha1");
if (xpath.isElement(firstGrandchild)) {
const matchedReference = sig.validateElementAgainstReferences(firstGrandchild, doc);
expect(matchedReference).to.not.be.false;
} else {
// @ts-expect-error FIXME
expect(xpath.isElement(firstGrandchild)).to.be.true;
}

for (let i = 0; i < sig.references.length; i++) {
const ref = sig.references[i];
const expectedUri = `#_${i}`;
expect(
ref.uri,
`wrong uri for index ${i}. expected: ${expectedUri} actual: ${ref.uri}`,
).to.equal(expectedUri);
expect(ref.transforms.length).to.equal(1);
expect(ref.transforms[0]).to.equal("http://www.w3.org/2001/10/xml-exc-c14n#");
expect(ref.digestValue).to.equal(digests[i]);
expect(ref.digestAlgorithm).to.equal("http://www.w3.org/2000/09/xmldsig#sha1");
}
} else {
expect(xpath.isNodeLike(signature)).to.be.true;
}
}

Expand Down Expand Up @@ -759,7 +784,6 @@ describe("Signature unit tests", function () {
});

it("correctly loads signature", function () {
// @ts-expect-error FIXME
passLoadSignature("./test/static/valid_signature.xml");
});

Expand All @@ -768,7 +792,6 @@ describe("Signature unit tests", function () {
});

it("correctly loads signature with root level sig namespace", function () {
// @ts-expect-error FIXME
passLoadSignature("./test/static/valid_signature_with_root_level_sig_namespace.xml");
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<root xmlns:ns1="http://www.w3.org/2000/09/xmldsig#"><x xmlns="ns" Id="_0"/><y z_attr="value" a_attr1="foo" Id="_1"/><z><ns:w ns:attr="value" xmlns:ns="myns" Id="_2"/></z><ns1:Signature><ns1:SignedInfo><ns1:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ns1:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><ns1:Reference URI="#_0"><ns1:Transforms><ns1:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ns1:Transforms><ns1:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ns1:DigestValue>b5GCZ2xpP5T7tbLWBTkOl4CYupQ=</ns1:DigestValue></ns1:Reference><ns1:Reference URI="#_1"><ns1:Transforms><ns1:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ns1:Transforms><ns1:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ns1:DigestValue>K4dI497ZCxzweDIrbndUSmtoezY=</ns1:DigestValue></ns1:Reference><ns1:Reference URI="#_2"><ns1:Transforms><ns1:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ns1:Transforms><ns1:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ns1:DigestValue>sH1gxKve8wlU8LlFVa2l6w3HMJ0=</ns1:DigestValue></ns1:Reference></ns1:SignedInfo><ns1:SignatureValue>PI2xGt3XrVcxYZ34Kw7nFdq75c7Mmo7J0q7yeDhBprHuJal/KV9KyKG+Zy3bmQIxNwkPh0KMP5r1YMTKlyifwbWK0JitRCSa0Fa6z6+TgJi193yiR5S1MQ+esoQT0RzyIOBl9/GuJmXx/1rXnqrTxmL7UxtqKuM29/eHwF0QDUI=</ns1:SignatureValue><ns1:KeyInfo><ns1:dummyKey>1234</ns1:dummyKey></ns1:KeyInfo></ns1:Signature></root>
<root xmlns:ns1="http://www.w3.org/2000/09/xmldsig#"><x xmlns="ns" Id="_0"/><y z_attr="value" a_attr1="foo" Id="_1"/><z><ns:w ns:attr="value" xmlns:ns="myns" Id="_2"/></z><ns1:Signature><ns1:SignedInfo><ns1:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ns1:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><ns1:Reference URI="#_0"><ns1:Transforms><ns1:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ns1:Transforms><ns1:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ns1:DigestValue>b5GCZ2xpP5T7tbLWBTkOl4CYupQ=</ns1:DigestValue></ns1:Reference><ns1:Reference URI="#_1"><ns1:Transforms><ns1:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ns1:Transforms><ns1:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ns1:DigestValue>K4dI497ZCxzweDIrbndUSmtoezY=</ns1:DigestValue></ns1:Reference><ns1:Reference URI="#_2"><ns1:Transforms><ns1:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ns1:Transforms><ns1:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ns1:DigestValue>sH1gxKve8wlU8LlFVa2l6w3HMJ0=</ns1:DigestValue></ns1:Reference></ns1:SignedInfo><ns1:SignatureValue>rR8+4xHiI8GQJ9Wty2TUbNI7Dd4uc89/BsAygYfeobEjmt4awzg6bQNA0nuQ+VggiPCYdKuKL8cPI7FUhk8osbVKdLPdy+rdJnibsyNpV87R7W5GZlFBEu/NqG6EYOMTHjpD4hq+H8ZeHC5YZDHPknPzJV8+A1UKN/BL2oWMQcg=</ns1:SignatureValue><ns1:KeyInfo><ns1:dummyKey>1234</ns1:dummyKey></ns1:KeyInfo></ns1:Signature></root>

0 comments on commit 8f01659

Please sign in to comment.