Skip to content

Commit

Permalink
Add method for checking if element is signed (#368)
Browse files Browse the repository at this point in the history
  • Loading branch information
cjbarth authored Aug 2, 2023
1 parent 226e0b2 commit aded3e2
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 22 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,20 @@ 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);
try {
const matchingReference = sig.validateElementAgainstReferences(elem, doc);
} catch {
throw new Error("the interesting element was not the one verified by the signature");
}
```

Note:
Expand Down
51 changes: 43 additions & 8 deletions src/signed-xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export class SignedXml {
privateKey?: crypto.KeyLike;
publicCert?: crypto.KeyLike;
/**
* One of the supported signature algorithms. See {@link SignatureAlgorithmType}
* One of the supported signature algorithms.
* @see {@link SignatureAlgorithmType}
*/
signatureAlgorithm: SignatureAlgorithmType = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
/**
Expand All @@ -56,21 +57,24 @@ export class SignedXml {
getCertFromKeyInfo = SignedXml.getCertFromKeyInfo;

// Internal state
/**
* 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 @@ -205,7 +209,7 @@ export class SignedXml {
* Returns the value of the signing certificate based on the contents of the
* specified KeyInfo.
*
* @param keyInfo KeyInfo element (see https://www.w3.org/TR/2008/REC-xmldsig-core-20080610/#sec-X509Data)
* @param keyInfo KeyInfo element (@see https://www.w3.org/TR/2008/REC-xmldsig-core-20080610/#sec-X509Data)
* @return the signing certificate as a string in PEM format
*/
static getCertFromKeyInfo(keyInfo?: Node | null): string | null {
Expand Down Expand Up @@ -389,6 +393,37 @@ export class SignedXml {
}
}

validateElementAgainstReferences(elem: Element, doc: Document): Reference {
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;
}

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;
}
}

throw new Error("No references passed validation");
}

validateReferences(doc) {
for (const ref of this.references) {
let elemXpath;
Expand Down Expand Up @@ -573,7 +608,7 @@ export class SignedXml {
* DigestMethods take an octet stream rather than a node set. If the output of the last transform is a node set, we
* need to canonicalize the node set to an octet stream using non-exclusive canonicalization. If there are no
* transforms, we need to canonicalize because URI dereferencing for a same-document reference will return a node-set.
* See:
* @see:
* https://www.w3.org/TR/xmldsig-core1/#sec-DigestMethod
* https://www.w3.org/TR/xmldsig-core1/#sec-ReferenceProcessingModel
* https://www.w3.org/TR/xmldsig-core1/#sec-Same-Document
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export type SignatureAlgorithmType =
| string;

/**
* @param cert the certificate as a string or array of strings (see https://www.w3.org/TR/2008/REC-xmldsig-core-20080610/#sec-X509Data)
* @param cert the certificate as a string or array of strings (@see https://www.w3.org/TR/2008/REC-xmldsig-core-20080610/#sec-X509Data)
* @param prefix an optional namespace alias to be used for the generated XML
*/
export interface GetKeyInfoContentArgs {
Expand Down
16 changes: 10 additions & 6 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,22 @@ const xml_special_to_encoded_text = {

export function encodeSpecialCharactersInAttribute(attributeValue) {
return attributeValue.replace(/([&<"\r\n\t])/g, function (str, item) {
// Special character normalization. See:
// - https://www.w3.org/TR/xml-c14n#ProcessingModel (Attribute Nodes)
// - https://www.w3.org/TR/xml-c14n#Example-Chars
/** Special character normalization.
* @see:
* - https://www.w3.org/TR/xml-c14n#ProcessingModel (Attribute Nodes)
* - https://www.w3.org/TR/xml-c14n#Example-Chars
*/
return xml_special_to_encoded_attribute[item];
});
}

export function encodeSpecialCharactersInText(text: string): string {
return text.replace(/([&<>\r])/g, function (str, item) {
// Special character normalization. See:
// - https://www.w3.org/TR/xml-c14n#ProcessingModel (Text Nodes)
// - https://www.w3.org/TR/xml-c14n#Example-Chars
/** Special character normalization.
* @see:
* - https://www.w3.org/TR/xml-c14n#ProcessingModel (Text Nodes)
* - https://www.w3.org/TR/xml-c14n#Example-Chars
*/
return xml_special_to_encoded_text[item];
});
}
Expand Down
10 changes: 6 additions & 4 deletions test/signature-integration-tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,12 @@ describe("Signature integration tests", function () {
it("add canonicalization if output of transforms will be a node-set rather than an octet stream", function () {
let xml = fs.readFileSync("./test/static/windows_store_signature.xml", "utf-8");

// Make sure that whitespace in the source document is removed -- see xml-crypto issue #23 and post at
// http://webservices20.blogspot.co.il/2013/06/validating-windows-mobile-app-store.html
// This regex is naive but works for this test case; for a more general solution consider
// the xmldom-fork-fixed library which can pass {ignoreWhiteSpace: true} into the Dom constructor.
/** Make sure that whitespace in the source document is removed --
* @see xml-crypto issue #23 and post at
* http://webservices20.blogspot.co.il/2013/06/validating-windows-mobile-app-store.html
* This regex is naive but works for this test case; for a more general solution consider
* the xmldom-fork-fixed library which can pass {ignoreWhiteSpace: true} into the Dom constructor.
*/
xml = xml.replace(/>\s*</g, "><");

const doc = new xmldom.DOMParser().parseFromString(xml);
Expand Down
11 changes: 9 additions & 2 deletions test/signature-unit-tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ describe("Signature unit tests", function () {
const checkedSignature = sig.checkSignature(xml);
expect(checkedSignature).to.be.true;

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

const digests = [
Expand All @@ -91,9 +90,17 @@ describe("Signature unit tests", function () {
"sH1gxKve8wlU8LlFVa2l6w3HMJ0=",
];

const firstGrandchild = doc.firstChild?.firstChild;

// @ts-expect-error FIXME
for (let i = 0; i < sig.references.length; i++) {
if (xpath.isElement(firstGrandchild)) {
expect(() => sig.validateElementAgainstReferences(firstGrandchild, doc)).to.not.throw;
} 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(
Expand Down

0 comments on commit aded3e2

Please sign in to comment.