diff --git a/.gitignore b/.gitignore index 7477aada..185cdbc6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ p.txt *.*~ *.swp npm-debug.log -/test/validators/XmlCryptoJava/target/ .eslintcache .nyc_output/ coverage/ diff --git a/.prettierignore b/.prettierignore index 11e7ea11..968e7cba 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,6 +5,5 @@ package-lock.json .eslintcache .prettierignore test/static/* -test/validators/* .nyc_output/* coverage/* diff --git a/AGENTS.md b/AGENTS.md index b817972e..d77e8672 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,13 +6,18 @@ depended on by security-sensitive projects such as `node-saml`, so a bug here can become an authentication bypass downstream. Treat every change as security-relevant. +## Purpose and scope + +Keep the library secure, spec-compliant, and easy to use. Verify a concrete security, +correctness, conformance, or usability problem before proposing or doing work. + ## Layout - `src/` — TypeScript source; the only code that ships. - `src/index.ts` — the public barrel. Anything re-exported here is public API. - `src/signed-xml.ts` — the core signing and verification logic. - `test/*.spec.ts` — Mocha specs. -- `test/static/`, `test/validators/` — fixtures. See the warning below. +- `test/static/` — fixtures. See the warning below. - `lib/` — build output. Generated; never edit. ## Commands @@ -28,10 +33,10 @@ Run `npm run build && npm test && npm run lint` before calling work done. ### Fixtures are byte-sensitive -`test/static/` and `test/validators/` contain XML signature fixtures. Canonicalization -and digests depend on the exact bytes, so reformatting whitespace silently invalidates -signatures and the failure can look unrelated. `.prettierignore` excludes both -directories — keep it that way, and never run a formatter over them. +`test/static/` contains XML signature fixtures. Canonicalization and digests depend on +the exact bytes, so reformatting whitespace silently invalidates signatures and the +failure can look unrelated. `.prettierignore` excludes that directory — keep it that +way, and never run a formatter over it. ### The supported Node floor is real diff --git a/README.md b/README.md index 7909aeec..f5309ed9 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,19 @@ Exclusive canonicalization (`http://www.w3.org/2001/10/xml-exc-c14n#` and its `# variant) renders the last case the same way when the redeclared prefix is listed in the `InclusiveNamespaces` `PrefixList`. +Inclusive and exclusive canonicalization escape namespace declaration values as they escape +attribute values. + +Ordinary attributes whose names start with `xmlns`, such as `xmlnsRole`, are part of the canonical +output. Exclusive canonicalization takes `PrefixList` bindings only from namespace declarations. + +Processing instructions inside signed content are rendered with their target and `` +delimiters. + +An empty-`URI` `Reference` signs processing instructions outside the document element. They appear +in `getSignedReferences()` when the transforms preserve them. When such instructions exist, a custom +transform receives the `Document` node rather than the document element. + ### Comments in referenced content A `Reference` whose `URI` is empty or `#` followed by an ID, such as `#item`, removes comments from @@ -350,12 +363,14 @@ A `SignedXml` object provides the following methods: To sign xml documents: -- `addReference({ xpath, transforms, digestAlgorithm, id, type })` - adds a reference to a xml element where: +- `addReference({ xpath, transforms, digestAlgorithm, id, type, isEmptyUri, inclusiveNamespacesPrefixList })` - adds a reference to a xml element where: - `xpath` - a string containing a XPath expression referencing a xml element - `transforms` - an array of [transform algorithms](#canonicalization-and-transformation-algorithms), the referenced element will be transformed for each value in the array - `digestAlgorithm` - one of the supported [hashing algorithms](#hashing-algorithms) - `id` - an optional `Id` attribute to add to the reference element - `type` - the optional `Type` attribute to add to the reference element (represented as a URI) + - `isEmptyUri` - optional, default `false` - when `true`, the reference is written as `URI=""`, which [refers to the whole document](https://www.w3.org/TR/xmldsig-core1/#sec-Same-Document), and no `Id` is added to the referenced element. `xpath` must then select the document element, for example `/*`: a signature that references any other element this way doesn't verify + - `inclusiveNamespacesPrefixList` - an optional array of namespace prefixes that exclusive canonicalization of this reference renders even where they aren't visibly used. It is written as the [`InclusiveNamespaces` `PrefixList`](https://www.w3.org/TR/xml-exc-c14n/#def-InclusiveNamespaces-PrefixList) parameter of the reference's exclusive canonicalization transform. Use `#default` to include the default namespace, including on a prefixed element - `computeSignature(xml, [options])` - compute the signature of the given xml where: - `xml` - a string containing a xml document - `options` - an object with the following properties: @@ -386,6 +401,13 @@ To verify xml documents: - `getSignedReferences()` - returns the canonical XML of each reference, only after `checkSignature` succeeds - `validateElementAgainstReferences(elemOrXpath, doc)` - **[deprecated]** after `checkSignature` succeeds, use the XML that `getSignedReferences()` returns instead of nodes from the original document +#### One instance per signature + +Create a new `SignedXml` for each signature you create or verify, including each signature in a document that has more than one. An instance keeps state between calls: + +- `computeSignature()` signs every reference the instance holds. `addReference()` adds one, so calling it for each signature on a reused instance adds one more `Reference` to every new signature. `checkSignature()` replaces them with the references of the signature it checks. +- `getSignedReferences()` returns the references of each successful `checkSignature()` call since the last failed one, which empties it. On a reused instance, that can include content that earlier signatures cover. + ## Customizing Algorithms The following sample shows how to sign a message using custom algorithms. diff --git a/src/c14n-canonicalization.ts b/src/c14n-canonicalization.ts index fac8d4f7..4611ea33 100644 --- a/src/c14n-canonicalization.ts +++ b/src/c14n-canonicalization.ts @@ -7,6 +7,10 @@ import type { import * as utils from "./utils"; import * as isDomNode from "@xmldom/is-dom-node"; +type C14nOptionsWithXmlAttributes = CanonicalizationOrTransformationAlgorithmProcessOptions & { + ancestorXmlAttributes?: utils.InheritedXmlAttribute[]; +}; + export class C14nCanonicalization implements CanonicalizationOrTransformationAlgorithm { protected includeComments = false; @@ -55,8 +59,7 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg if (node.attributes) { for (i = 0; i < node.attributes.length; ++i) { attr = node.attributes[i]; - //ignore namespace definition attributes - if (attr.name.indexOf("xmlns") === 0) { + if (attr.name === "xmlns" || attr.prefix === "xmlns") { continue; } attrListToRender.push(attr); @@ -138,7 +141,7 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg if (nodeDefaultNs !== defaultNs) { newDefaultNs = nodeDefaultNs; - res.push(' xmlns="', newDefaultNs, '"'); + res.push(' xmlns="', utils.encodeSpecialCharactersInAttribute(newDefaultNs), '"'); } //handle the attributes namespace @@ -183,10 +186,11 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg //render namespaces res.push( ...nsListToRender.map((attr) => { + const namespaceURI = utils.encodeSpecialCharactersInAttribute(attr.namespaceURI); if (attr.prefix) { - return ` xmlns:${attr.prefix}="${attr.namespaceURI}"`; + return ` xmlns:${attr.prefix}="${namespaceURI}"`; } - return ` xmlns="${attr.namespaceURI}"`; + return ` xmlns="${namespaceURI}"`; }), ); @@ -204,9 +208,37 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg ancestorNamespaces, namespacesInScope?: NamespacePrefix[], ) { + if (isDomNode.isDocumentNode(node)) { + const res: string[] = []; + for (let i = 0; i < node.childNodes.length; i++) { + const child = node.childNodes[i]; + if (isDomNode.isCommentNode(child)) { + if (this.includeComments) { + res.push(this.renderComment(child, true)); + } + continue; + } + if (utils.isCanonicalDocumentChild(child)) { + res.push( + this.processInner( + child, + prefixesInScope, + defaultNs, + defaultNsForPrefix, + ancestorNamespaces, + namespacesInScope, + ), + ); + } + } + return res.join("\n"); + } if (isDomNode.isCommentNode(node)) { return this.renderComment(node); } + if (isDomNode.isProcessingInstructionNode(node)) { + return utils.renderProcessingInstruction(node); + } if (node.data) { return utils.encodeSpecialCharactersInText(node.data); } @@ -246,11 +278,16 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg } // Thanks to deoxxa/xml-c14n for comment renderer - renderComment(node: Comment) { + renderComment(node: Comment, documentChild = false) { if (!this.includeComments) { return ""; } + const encodedText = utils.encodeSpecialCharactersInText(node.data); + if (documentChild) { + return ``; + } + const isOutsideDocument = node.ownerDocument === node.parentNode; let isBeforeDocument = false; let isAfterDocument = false; @@ -280,8 +317,6 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg const afterDocument = isAfterDocument ? "\n" : ""; const beforeDocument = isBeforeDocument ? "\n" : ""; - const encodedText = utils.encodeSpecialCharactersInText(node.data); - return `${afterDocument}${beforeDocument}`; } @@ -296,6 +331,24 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg const defaultNs = options.defaultNs || ""; const defaultNsForPrefix = options.defaultNsForPrefix || {}; const ancestorNamespaces = options.ancestorNamespaces || []; + const inheritedXmlAttributes = + (options as C14nOptionsWithXmlAttributes).ancestorXmlAttributes ?? + utils.findAncestorXmlAttributes(node); + let canonicalNode = node; + if (isDomNode.isElementNode(node) && inheritedXmlAttributes.length > 0) { + const clone = node.cloneNode(true); + isDomNode.assertIsElementNode(clone); + for (const attr of inheritedXmlAttributes) { + if (!clone.hasAttributeNS("http://www.w3.org/XML/1998/namespace", attr.localName)) { + clone.setAttributeNS( + "http://www.w3.org/XML/1998/namespace", + `xml:${attr.localName}`, + attr.value, + ); + } + } + canonicalNode = clone; + } const prefixesInScope: string[] = []; for (let i = 0; i < ancestorNamespaces.length; i++) { @@ -303,7 +356,7 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg } const res = this.processInner( - node, + canonicalNode, prefixesInScope, defaultNs, defaultNsForPrefix, diff --git a/src/exclusive-canonicalization.ts b/src/exclusive-canonicalization.ts index 56d28c37..210eb61d 100644 --- a/src/exclusive-canonicalization.ts +++ b/src/exclusive-canonicalization.ts @@ -55,8 +55,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati if (node.attributes) { for (i = 0; i < node.attributes.length; ++i) { attr = node.attributes[i]; - //ignore namespace definition attributes - if (attr.name.indexOf("xmlns") === 0) { + if (attr.name === "xmlns" || attr.prefix === "xmlns") { continue; } attrListToRender.push(attr); @@ -114,10 +113,17 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati namespaceURI: node.namespaceURI || defaultNsForPrefix[node.prefix], }); } - } else if (defaultNs !== currNs) { - //new default ns - newDefaultNs = node.namespaceURI; - res.push(' xmlns="', newDefaultNs, '"'); + } + + const currentDefaultNs = node.prefix + ? (node.getAttributeNode("xmlns")?.value ?? defaultNs) + : currNs; + if ( + (!node.prefix || inclusiveNamespacesPrefixList.includes("#default")) && + defaultNs !== currentDefaultNs + ) { + newDefaultNs = currentDefaultNs; + res.push(' xmlns="', utils.encodeSpecialCharactersInAttribute(newDefaultNs), '"'); } //handle the attributes namespace @@ -128,7 +134,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati //handle all prefixed attributes that are included in the prefix list and where //the prefix is not defined already if ( - attr.prefix && + attr.prefix === "xmlns" && !utils.isPrefixInScope(prefixesInScope, attr.localName, attr.value) && inclusiveNamespacesPrefixList.indexOf(attr.localName) >= 0 ) { @@ -154,7 +160,8 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati //render namespaces for (const p of nsListToRender) { - res.push(" xmlns:", p.prefix, '="', p.namespaceURI, '"'); + const namespaceURI = utils.encodeSpecialCharactersInAttribute(p.namespaceURI); + res.push(" xmlns:", p.prefix, '="', namespaceURI, '"'); } return { rendered: res.join(""), newDefaultNs: newDefaultNs }; @@ -170,9 +177,36 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati defaultNsForPrefix, inclusiveNamespacesPrefixList: string[], ) { + if (isDomNode.isDocumentNode(node)) { + const res: string[] = []; + for (let i = 0; i < node.childNodes.length; i++) { + const child = node.childNodes[i]; + if (isDomNode.isCommentNode(child)) { + if (this.includeComments) { + res.push(this.renderComment(child, true)); + } + continue; + } + if (utils.isCanonicalDocumentChild(child)) { + res.push( + this.processInner( + child, + prefixesInScope, + defaultNs, + defaultNsForPrefix, + inclusiveNamespacesPrefixList, + ), + ); + } + } + return res.join("\n"); + } if (isDomNode.isCommentNode(node)) { return this.renderComment(node); } + if (isDomNode.isProcessingInstructionNode(node)) { + return utils.renderProcessingInstruction(node); + } if (node.data) { return utils.encodeSpecialCharactersInText(node.data); } @@ -210,11 +244,16 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati } // Thanks to deoxxa/xml-c14n for comment renderer - renderComment(node: Comment) { + renderComment(node: Comment, documentChild = false) { if (!this.includeComments) { return ""; } + const encodedText = utils.encodeSpecialCharactersInText(node.data); + if (documentChild) { + return ``; + } + const isOutsideDocument = node.ownerDocument === node.parentNode; let isBeforeDocument = false; let isAfterDocument = false; @@ -244,18 +283,20 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati const afterDocument = isAfterDocument ? "\n" : ""; const beforeDocument = isBeforeDocument ? "\n" : ""; - const encodedText = utils.encodeSpecialCharactersInText(node.data); - return `${afterDocument}${beforeDocument}`; } /** - * Perform canonicalization of the given element node + * Perform canonicalization of the given node * * @api public */ - process(elem: Element, options: CanonicalizationOrTransformationAlgorithmProcessOptions): string { + process( + node: Element | Document, + options: CanonicalizationOrTransformationAlgorithmProcessOptions, + ): string { options = options || {}; + const elem = isDomNode.isDocumentNode(node) ? node.documentElement : node; let inclusiveNamespacesPrefixList = options.inclusiveNamespacesPrefixList || []; const defaultNs = options.defaultNs || ""; const defaultNsForPrefix = options.defaultNsForPrefix || {}; @@ -265,7 +306,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati * If the inclusiveNamespacesPrefixList has not been explicitly provided then look it up in CanonicalizationMethod/InclusiveNamespaces */ if (!utils.isArrayHasLength(inclusiveNamespacesPrefixList)) { - const CanonicalizationMethod = utils.findChildren(elem, "CanonicalizationMethod"); + const CanonicalizationMethod = utils.findChildren(node, "CanonicalizationMethod"); if (CanonicalizationMethod.length !== 0) { const inclusiveNamespaces = utils.findChildren( CanonicalizationMethod[0], @@ -286,7 +327,15 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati inclusiveNamespacesPrefixList.forEach(function (prefix) { if (ancestorNamespaces) { ancestorNamespaces.forEach(function (ancestorNamespace) { - if (prefix === ancestorNamespace.prefix) { + if (prefix === "#default" && ancestorNamespace.prefix === "") { + if (!elem.hasAttribute("xmlns")) { + elem.setAttributeNS( + "http://www.w3.org/2000/xmlns/", + "xmlns", + ancestorNamespace.namespaceURI, + ); + } + } else if (prefix === ancestorNamespace.prefix) { elem.setAttributeNS( "http://www.w3.org/2000/xmlns/", `xmlns:${prefix}`, @@ -299,7 +348,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati } const res = this.processInner( - elem, + node, [], defaultNs, defaultNsForPrefix, diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 79ef66dd..841e23c1 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -28,6 +28,9 @@ import * as signatureAlgorithms from "./signature-algorithms"; import * as utils from "./utils"; type SigningReferenceTarget = { node: Element; digestValue?: string }; +type C14nOptionsWithXmlAttributes = CanonicalizationOrTransformationAlgorithmProcessOptions & { + ancestorXmlAttributes?: utils.InheritedXmlAttribute[]; +}; function findSignatureElements(node: Node): Element[] { const signatures = xpath.select( @@ -38,6 +41,29 @@ function findSignatureElements(node: Node): Element[] { return signatures.filter(isDomNode.isElementNode); } +// SignatureValue is base64Binary, so whitespace in it carries no value: +// https://www.w3.org/TR/xmldsig-core1/#sec-SignatureValue +function findSignatureValue(signature: Node): string | undefined { + return utils.findChildren(signature, "SignatureValue")[0]?.textContent?.replace(/[\t\n\r ]/g, ""); +} + +// A Transform carries only its own algorithm's parameters, and InclusiveNamespaces belongs to +// exclusive canonicalization, in this namespace for both identifiers: +// https://www.w3.org/TR/xmldsig-core1/#sec-Transforms +// https://www.w3.org/TR/xml-exc-c14n/#sec-Use +const exclusiveC14nNamespace = "http://www.w3.org/2001/10/xml-exc-c14n#"; + +function isExclusiveC14n(algorithm: string | undefined): boolean { + return ( + algorithm === exclusiveC14nNamespace || + algorithm === "http://www.w3.org/2001/10/xml-exc-c14n#WithComments" + ); +} + +function decodeSignatureValue(signature: Node): Buffer { + return Buffer.from(findSignatureValue(signature) ?? "", "base64"); +} + function certificatesToPublish(publicCert: string): string[] { const certificates = utils.pemCertificates(publicCert); if (certificates.length > 0) { @@ -111,6 +137,8 @@ export class SignedXml { */ private references: Reference[] = []; + private loadedPrefixLists = new WeakMap(); + /** * Contains the canonicalized XML of the references that were validly signed. * @@ -442,18 +470,29 @@ export class SignedXml { } } - /** - * Search for ancestor namespaces before canonicalization. - */ - const ancestorNamespaces = utils.findAncestorNs(doc, "//*[local-name()='SignedInfo']"); - - const c14nOptions = { - ancestorNamespaces: ancestorNamespaces, - }; + const c14nOptions = this.findSignedInfoAncestorContext(doc); return this.getCanonXml([this.canonicalizationAlgorithm], signedInfo[0], c14nOptions); } + // The checked document gives SignedInfo its inherited context, not wherever a loaded copy + // came from: https://www.w3.org/TR/xml-c14n#ProcessingModel + private findSignedInfoAncestorContext(doc: Document): C14nOptionsWithXmlAttributes { + const signatureInDoc = this.findLoadedSignature(doc); + if (signatureInDoc == null) { + return { ancestorNamespaces: [], ancestorXmlAttributes: [] }; + } + + const signedInfoInDoc = utils.findChildren(signatureInDoc, "SignedInfo")[0]; + if (signedInfoInDoc == null) { + throw new Error("could not find SignedInfo element in the message"); + } + return { + ancestorNamespaces: utils.findAncestorNsForElement(signedInfoInDoc), + ancestorXmlAttributes: utils.findAncestorXmlAttributes(signedInfoInDoc), + }; + } + private getCanonReferenceXml(doc: Document, ref: Reference, node: Node) { /** * Search for ancestor namespaces before canonicalization. @@ -463,14 +502,27 @@ export class SignedXml { } const c14nOptions = { - inclusiveNamespacesPrefixList: ref.inclusiveNamespacesPrefixList, ancestorNamespaces: ref.ancestorNamespaces, + ancestorXmlAttributes: utils.findAncestorXmlAttributes(node), }; // Only a same-document URI dereferences without comments; validateReference resolves no // XPointer: https://www.w3.org/TR/xmldsig-core1/#sec-Same-Document const discardComments = ref.uri === "" || ref.uri.startsWith("#"); - return this.canonicalize(ref.transforms, node, c14nOptions, { discardComments }); + return this.canonicalize(ref.transforms, node, c14nOptions, { + discardComments, + prefixLists: this.loadedPrefixLists.get(ref) ?? this.getSigningPrefixLists(ref), + }); + } + + // Signing gives the reference's current list to the exclusive canonicalization transforms, the + // ones createReferences writes it under, so the digest matches the Transforms it serializes. + private getSigningPrefixLists(ref: Reference): string[][] { + return ref.transforms.map((transform) => + isExclusiveC14n(this.findCanonicalizationAlgorithm(transform).getAlgorithmName()) + ? ref.inclusiveNamespacesPrefixList + : [], + ); } private calculateSignatureValue(doc: Document, callback?: ErrorFirstCallback) { @@ -562,7 +614,7 @@ export class SignedXml { let elem: xpath.SelectSingleReturnType = null; if (uri === "") { - elem = xpath.select1("//*", doc); + elem = utils.hasDocumentLevelProcessingInstruction(doc) ? doc : xpath.select1("//*", doc); } else if (uri?.indexOf("'") !== -1) { // xpath injection throw new Error("Cannot validate a uri with quotes inside it"); @@ -710,20 +762,8 @@ export class SignedXml { this.loadReference(reference); } - const signatureValue = xpath.select1( - ".//*[local-name(.)='SignatureValue']/text()", - signatureNode, - ); - - if (isDomNode.isTextNode(signatureValue)) { - this.signatureValue = signatureValue.data.replace(/\r?\n/g, ""); - } - - const keyInfo = xpath.select1(".//*[local-name(.)='KeyInfo']", signatureNode); - - if (isDomNode.isNodeLike(keyInfo)) { - this.keyInfo = keyInfo; - } + this.signatureValue = findSignatureValue(signatureNode) ?? ""; + this.keyInfo = utils.findChildren(signatureNode, "KeyInfo")[0] ?? null; } /** @@ -759,7 +799,9 @@ export class SignedXml { } const transforms: string[] = []; - let inclusiveNamespacesPrefixList: string[] = []; + // A Transform's content governs only that transform, so each one keeps the PrefixList it + // carries: https://www.w3.org/TR/xmldsig-core1/#sec-Transforms + const prefixLists: string[][] = []; nodes = utils.findChildren(refNode, "Transforms"); if (nodes.length !== 0) { const transformsNode = nodes[0]; @@ -769,25 +811,29 @@ export class SignedXml { if (transformAttr) { transforms.push(transformAttr.value); + // PrefixList is NMTOKENS, delimited by XML white space: + // https://www.w3.org/TR/xml-exc-c14n/#sec-Use + prefixLists.push( + utils + .findChildren(transform, "InclusiveNamespaces") + .flatMap((namespace) => + (namespace.getAttribute("PrefixList") ?? "").split(/[ \t\r\n]+/), + ) + .filter((value) => value.length > 0), + ); } } - - // This is a little strange, we are looking for children of the last child of `transformsNode` - const inclusiveNamespaces = utils.findChildren( - transformsAll[transformsAll.length - 1], - "InclusiveNamespaces", - ); - if (utils.isArrayHasLength(inclusiveNamespaces)) { - // Should really only be one prefix list, but maybe there's some circumstances where more than one to let's handle it - inclusiveNamespacesPrefixList = inclusiveNamespaces - .flatMap((namespace) => (namespace.getAttribute("PrefixList") ?? "").split(" ")) - .filter((value) => value.length > 0); - } } + // A Reference exposes one list, so it reports the last exclusive transform's. An implicit + // transform has no Transform to carry parameters, so it takes that list. + const inclusiveNamespacesPrefixList = + prefixLists.filter((_, index) => isExclusiveC14n(transforms[index])).pop() ?? []; + if (utils.isArrayHasLength(this.implicitTransforms)) { this.implicitTransforms.forEach(function (t) { transforms.push(t); + prefixLists.push(inclusiveNamespacesPrefixList); }); } @@ -818,6 +864,7 @@ export class SignedXml { inclusiveNamespacesPrefixList, isEmptyUri: false, }); + this.loadedPrefixLists.set(this.references[this.references.length - 1], prefixLists); } /** @@ -1065,20 +1112,17 @@ export class SignedXml { referenceNode.appendChild(signatureElem); } else if (location.action === "prepend") { referenceNode.insertBefore(signatureElem, referenceNode.firstChild); - } else if (location.action === "before") { - if (referenceNode.parentNode == null) { - throw new Error( - "`location.reference` refers to the root node (by default), so we can't insert `before`", - ); - } - referenceNode.parentNode.insertBefore(signatureElem, referenceNode); - } else if (location.action === "after") { - if (referenceNode.parentNode == null) { + } else { + const parentNode = referenceNode.parentNode; + if (parentNode == null) { throw new Error( - "`location.reference` refers to the root node (by default), so we can't insert `after`", + `\`location.reference\` selects a node without a parent, so we can't insert \`${location.action}\``, ); } - referenceNode.parentNode.insertBefore(signatureElem, referenceNode.nextSibling); + parentNode.insertBefore( + signatureElem, + location.action === "before" ? referenceNode : referenceNode.nextSibling, + ); } this.addAllReferences(doc, signatureElem, referenceTargets, prefix); @@ -1139,15 +1183,21 @@ export class SignedXml { } private calculateReferenceDigest(ref: Reference, node: Element): string { + const referenceNode = + ref.isEmptyUri && + node === node.ownerDocument.documentElement && + utils.hasDocumentLevelProcessingInstruction(node.ownerDocument) + ? node.ownerDocument + : node; ref.ancestorNamespaces = utils.findAncestorNsForElement(node); const canonXml = this.canonicalize( ref.transforms, - node, + referenceNode, { - inclusiveNamespacesPrefixList: ref.inclusiveNamespacesPrefixList, ancestorNamespaces: ref.ancestorNamespaces, + ancestorXmlAttributes: utils.findAncestorXmlAttributes(node), }, - { discardComments: true }, + { discardComments: true, prefixLists: this.getSigningPrefixLists(ref) }, ); return this.findHashAlgorithm(ref.digestAlgorithm).getHash(canonXml); } @@ -1255,9 +1305,12 @@ export class SignedXml { ); transformElem.setAttribute("Algorithm", transform.getAlgorithmName()); - if (utils.isArrayHasLength(ref.inclusiveNamespacesPrefixList)) { + if ( + utils.isArrayHasLength(ref.inclusiveNamespacesPrefixList) && + isExclusiveC14n(transform.getAlgorithmName()) + ) { const inclusiveNamespacesElem = signatureDoc.createElementNS( - transform.getAlgorithmName(), + exclusiveC14nNamespace, "InclusiveNamespaces", ); inclusiveNamespacesElem.setAttribute( @@ -1356,13 +1409,19 @@ export class SignedXml { private canonicalize( transforms: Reference["transforms"], node: Node, - options: CanonicalizationOrTransformationAlgorithmProcessOptions, - { discardComments }: { discardComments: boolean }, + options: C14nOptionsWithXmlAttributes, + { + discardComments, + prefixLists, + }: { discardComments: boolean; prefixLists?: ReadonlyArray }, ) { options.defaultNsForPrefix = options.defaultNsForPrefix ?? SignedXml.defaultNsForPrefix; + options.ancestorXmlAttributes = + options.ancestorXmlAttributes ?? utils.findAncestorXmlAttributes(node); options.signatureNode = this.signatureNode; const canonXml = node.cloneNode(true); // Deep clone + const preserveDocument = isDomNode.isDocumentNode(node); if (transforms.includes("http://www.w3.org/2000/09/xmldsig#enveloped-signature")) { const signaturePath: number[] = []; let signatureAncestor = this.findLoadedSignature(node); @@ -1388,20 +1447,25 @@ export class SignedXml { // Octets are parsed into a node-set for the next transform, and a node-set left at the end is // converted to octets with C14N: https://www.w3.org/TR/xmldsig-core1/#sec-ReferenceProcessingModel - for (const transformName of transforms) { + for (const [index, transformName] of transforms.entries()) { if (!isDomNode.isNodeLike(transformedXml)) { - transformedXml = this.parseTransformInput(transformedXml, transformName); + transformedXml = this.parseTransformInput(transformedXml, transformName, preserveDocument); // The parsed octets are a new document, so the referenced node's ancestors are gone. transformOptions = { ...options, ancestorNamespaces: [], + ancestorXmlAttributes: [], defaultNs: "", signatureNode: this.findLoadedSignature(transformedXml) ?? options.signatureNode, }; } + // Each transform takes only the parameters of its own Transform: + // https://www.w3.org/TR/xmldsig-core1/#sec-Transforms transformedXml = this.findCanonicalizationAlgorithm(transformName).process( transformedXml, - transformOptions, + prefixLists + ? { ...transformOptions, inclusiveNamespacesPrefixList: prefixLists[index] ?? [] } + : transformOptions, ); } @@ -1415,22 +1479,21 @@ export class SignedXml { } // checkSignature() parses its own copy of the document, and octets a transform returns are parsed - // again, so the loaded signature is found by its SignatureValue. A copy carrying the same value - // could stand in for it, so refuse to guess. + // again, so the loaded signature is found by the SignatureValue bytes verification decodes. A copy + // carrying the same value could stand in for it, so refuse to guess. private findLoadedSignature(node: Node): Node | null { const doc = node.ownerDocument ?? node; if (this.signatureNode == null || this.signatureNode.ownerDocument === doc) { return this.signatureNode; } - const signatureValue = utils.findChildren(this.signatureNode, "SignatureValue")[0]?.textContent; - if (!signatureValue) { + const signatureValue = decodeSignatureValue(this.signatureNode); + if (signatureValue.length === 0) { return null; } - const matches = findSignatureElements(doc).filter( - (signature) => - utils.findChildren(signature, "SignatureValue")[0]?.textContent === signatureValue, + const matches = findSignatureElements(doc).filter((signature) => + decodeSignatureValue(signature).equals(signatureValue), ); if (matches.length > 1) { throw new Error( @@ -1440,7 +1503,11 @@ export class SignedXml { return matches[0] ?? null; } - private parseTransformInput(octets: string, transformName: string): Element { + private parseTransformInput( + octets: string, + transformName: string, + preserveDocument: boolean, + ): Element | Document { const parseErrors: string[] = []; const doc = new xmldom.DOMParser({ errorHandler: (_level, message) => parseErrors.push(String(message)), @@ -1452,7 +1519,7 @@ export class SignedXml { ); } - return doc.documentElement; + return preserveDocument ? doc : doc.documentElement; } /** diff --git a/src/utils.ts b/src/utils.ts index 7e4cc5fa..ec5ec0c1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -94,6 +94,28 @@ export function encodeSpecialCharactersInText(text: string): string { }); } +export function renderProcessingInstruction(node: ProcessingInstruction): string { + const data = node.data ? ` ${node.data}` : ""; + return ``; +} + +export function isCanonicalDocumentChild(node: Node): boolean { + return ( + isDomNode.isElementNode(node) || + // xmldom exposes the XML declaration as a PI, but Canonical XML excludes it. + (isDomNode.isProcessingInstructionNode(node) && node.target.toLowerCase() !== "xml") + ); +} + +export function hasDocumentLevelProcessingInstruction(doc: Document): boolean { + for (let child = doc.firstChild; child != null; child = child.nextSibling) { + if (isDomNode.isProcessingInstructionNode(child) && child.target.toLowerCase() !== "xml") { + return true; + } + } + return false; +} + /* * RFC 7468 'textualmsg', with the deviations below. * https://www.rfc-editor.org/rfc/rfc7468 @@ -465,6 +487,26 @@ export function toPem(value: string | Buffer, pemLabel?: PemLabel): string { throw new Error("Invalid PEM format."); } +export type InheritedXmlAttribute = { localName: string; value: string }; + +export function findAncestorXmlAttributes(node: Node): InheritedXmlAttribute[] { + const attributes = new Map(); + let ancestor = node.parentNode; + while (isDomNode.isElementNode(ancestor)) { + for (let i = 0; i < ancestor.attributes.length; i++) { + const attr = ancestor.attributes[i]; + if ( + attr.namespaceURI === "http://www.w3.org/XML/1998/namespace" && + !attributes.has(attr.localName) + ) { + attributes.set(attr.localName, attr.value); + } + } + ancestor = ancestor.parentNode; + } + return Array.from(attributes, ([localName, value]) => ({ localName, value })); +} + function collectAncestorNamespaces( node: Element, nsArray: NamespacePrefix[] = [], @@ -482,7 +524,7 @@ function collectAncestorNamespaces( if (parent.attributes && parent.attributes.length > 0) { for (let i = 0; i < parent.attributes.length; i++) { const attr = parent.attributes[i]; - if (attr && attr.nodeName && attr.nodeName.search(/^xmlns:?/) !== -1) { + if (attr && (attr.nodeName === "xmlns" || attr.nodeName.startsWith("xmlns:"))) { nsArray.push({ prefix: attr.nodeName.replace(/^xmlns:?/, ""), namespaceURI: attr.nodeValue || "", diff --git a/test/README.md b/test/README.md new file mode 100644 index 00000000..98935dde --- /dev/null +++ b/test/README.md @@ -0,0 +1,9 @@ +# Test fixtures + +This records who signed these fixtures in `static/`. One signed by another XML-DSig implementation checks interoperability rather than only this library's agreement with itself. + +- `hmac_signature.xml` and `hmac.key`: signed with the JDK's `javax.xml.crypto.dsig`. +- `inclusive_namespaces_exc_c14n.xml`, `inclusive_namespaces_exc_c14n_with_comments.xml`, `inclusive_namespaces_enveloped_signature_after_exc_c14n.xml`, `inclusive_namespaces_on_second_of_two_exc_c14n.xml` and `inclusive_namespaces_after_custom_transform.xml`: signed with .NET's `System.Security.Cryptography.Xml.SignedXml` and `client.pem`. +- `inclusive_namespaces_prefix_list_with_tab.xml`: its document and reference digest come from .NET's `SignedXml`. Its `SignedInfo` was canonicalized with libxml2 and signed with openssl and `client.pem`. +- `inclusive_namespaces_in_with_comments_namespace.xml`: signed with xml-crypto 6.3.2 and `client.pem`. It checks that a signature from 6.3.2 or earlier still verifies, not interoperability. Those releases wrote a reference's `InclusiveNamespaces` under every `Transform`, in a namespace named after that Transform's `Algorithm`. +- `windows_store_signature.xml` and `windows_store_certificate.pem`: a Windows Store app receipt and the certificate that verifies it. diff --git a/test/algorithm-uri-whitespace.spec.ts b/test/algorithm-uri-whitespace.spec.ts new file mode 100644 index 00000000..6ae562be --- /dev/null +++ b/test/algorithm-uri-whitespace.spec.ts @@ -0,0 +1,70 @@ +import * as fs from "fs"; +import * as xmldom from "@xmldom/xmldom"; +import { expect } from "chai"; +import { SignedXml } from "../src/index"; + +const exclusiveC14n = "http://www.w3.org/2001/10/xml-exc-c14n#"; +const rsaSha256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; +const sha256 = "http://www.w3.org/2001/04/xmlenc#sha256"; +const algorithms = { + canonicalization: exclusiveC14n, + transform: exclusiveC14n, + signature: rsaSha256, + digest: sha256, +}; + +const padded = (algorithm: string) => `${algorithm} `; + +function sign(signedWith: typeof algorithms): string { + const signer = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: signedWith.canonicalization, + signatureAlgorithm: signedWith.signature, + }); + // The signer writes each Algorithm from getAlgorithmName(), so these let it sign a padded one. + const ExclusiveC14n = signer.CanonicalizationAlgorithms[exclusiveC14n]; + const RsaSha256 = signer.SignatureAlgorithms[rsaSha256]; + const Sha256 = signer.HashAlgorithms[sha256]; + signer.CanonicalizationAlgorithms[padded(exclusiveC14n)] = class extends ExclusiveC14n { + getAlgorithmName = () => padded(exclusiveC14n); + }; + signer.SignatureAlgorithms[padded(rsaSha256)] = class extends RsaSha256 { + getAlgorithmName = () => padded(rsaSha256); + }; + signer.HashAlgorithms[padded(sha256)] = class extends Sha256 { + getAlgorithmName = () => padded(sha256); + }; + signer.addReference({ + xpath: "//*[local-name(.)='book']", + transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature", signedWith.transform], + digestAlgorithm: signedWith.digest, + }); + signer.computeSignature("Harry Potter"); + return signer.getSignedXml(); +} + +function checkSignature(xml: string): boolean { + const verifier = new SignedXml({ + publicCert: fs.readFileSync("./test/static/client_public.pem"), + }); + verifier.loadSignature(verifier.findSignatures(new xmldom.DOMParser().parseFromString(xml))[0]); + return verifier.checkSignature(xml); +} + +describe("Algorithm attributes", function () { + for (const [element, name, kind] of [ + ["CanonicalizationMethod", "canonicalization", "canonicalization"], + ["Transform", "transform", "canonicalization"], + ["SignatureMethod", "signature", "signature"], + ["DigestMethod", "digest", "hash"], + ] as const) { + it(`rejects a ${element} Algorithm signed with whitespace around it`, function () { + const algorithm = padded(algorithms[name]); + const xml = sign({ ...algorithms, [name]: algorithm }); + + expect(() => checkSignature(xml)).to.throw( + `${kind} algorithm '${algorithm}' is not supported`, + ); + }); + } +}); diff --git a/test/c14n-inherited-context.spec.ts b/test/c14n-inherited-context.spec.ts new file mode 100644 index 00000000..6785993e --- /dev/null +++ b/test/c14n-inherited-context.spec.ts @@ -0,0 +1,210 @@ +import * as fs from "fs"; +import * as xmldom from "@xmldom/xmldom"; +import { expect } from "chai"; +import { SignedXml } from "../src/index"; + +const exclusiveC14n = "http://www.w3.org/2001/10/xml-exc-c14n#"; +const inclusiveC14n = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"; +const sha256 = "http://www.w3.org/2001/04/xmlenc#sha256"; +const rsaSha256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; + +function verify(xml: string): boolean { + const verifier = new SignedXml({ + publicCert: fs.readFileSync("./test/static/client_public.pem"), + }); + const doc = new xmldom.DOMParser().parseFromString(xml); + verifier.loadSignature(verifier.findSignatures(doc)[0]); + return verifier.checkSignature(xml); +} + +describe("inherited context in signed references", function () { + it("protects the default namespace requested by an exclusive C14N PrefixList", function () { + const signer = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: exclusiveC14n, + signatureAlgorithm: rsaSha256, + }); + signer.addReference({ + xpath: "//*[local-name(.)='item']", + transforms: [exclusiveC14n], + digestAlgorithm: sha256, + inclusiveNamespacesPrefixList: ["#default"], + }); + signer.computeSignature( + 'value', + ); + + const signedXml = signer.getSignedXml(); + expect(signedXml).to.include('PrefixList="#default"'); + expect(verify(signedXml)).to.be.true; + + const tamperedXml = signedXml.replace('xmlns="urn:trusted"', 'xmlns="urn:attacker"'); + expect(tamperedXml).not.to.equal(signedXml); + expect(verify(tamperedXml)).to.be.false; + }); + + it("protects a default namespace inherited by a prefixed reference", function () { + const signer = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: exclusiveC14n, + signatureAlgorithm: rsaSha256, + }); + signer.addReference({ + xpath: "//*[local-name(.)='item']", + transforms: [exclusiveC14n], + digestAlgorithm: sha256, + inclusiveNamespacesPrefixList: ["#default"], + }); + signer.computeSignature( + 'value', + ); + + const signedXml = signer.getSignedXml(); + expect(verify(signedXml)).to.be.true; + + const tamperedXml = signedXml.replace('xmlns="urn:trusted"', 'xmlns="urn:attacker"'); + expect(tamperedXml).not.to.equal(signedXml); + expect(verify(tamperedXml)).to.be.false; + }); + + it("protects inherited xml:lang under inclusive C14N", function () { + const signer = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: exclusiveC14n, + signatureAlgorithm: rsaSha256, + }); + signer.addReference({ + xpath: "//*[local-name(.)='item']", + transforms: [inclusiveC14n], + digestAlgorithm: sha256, + }); + signer.computeSignature('value'); + + const signedXml = signer.getSignedXml(); + expect(verify(signedXml)).to.be.true; + + const tamperedXml = signedXml.replace('xml:lang="en"', 'xml:lang="fr"'); + expect(tamperedXml).not.to.equal(signedXml); + expect(verify(tamperedXml)).to.be.false; + }); + + it("protects inherited xml:lang when SignedInfo uses inclusive C14N", function () { + const signer = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: inclusiveC14n, + signatureAlgorithm: rsaSha256, + }); + signer.addReference({ + xpath: "//*[local-name(.)='item']", + transforms: [exclusiveC14n], + digestAlgorithm: sha256, + }); + signer.computeSignature('value'); + + const signedXml = signer.getSignedXml(); + expect(verify(signedXml)).to.be.true; + + const tamperedXml = signedXml.replace('xml:lang="en"', 'xml:lang="fr"'); + expect(tamperedXml).not.to.equal(signedXml); + expect(() => verify(tamperedXml)).to.throw(/invalid signature/); + }); + it("rejects a default namespace added to a prefixed reference", function () { + const signer = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: exclusiveC14n, + signatureAlgorithm: rsaSha256, + }); + signer.addReference({ + xpath: "//*[local-name(.)='item']", + transforms: [exclusiveC14n], + digestAlgorithm: sha256, + inclusiveNamespacesPrefixList: ["#default"], + }); + signer.computeSignature('value'); + + const signedXml = signer.getSignedXml(); + expect(verify(signedXml)).to.be.true; + + const tamperedXml = signedXml.replace( + 'xmlns:p="urn:item"', + 'xmlns="urn:attacker" xmlns:p="urn:item"', + ); + expect(tamperedXml).not.to.equal(signedXml); + expect(verify(tamperedXml)).to.be.false; + }); + + it("keeps an element's own xml:lang over its ancestor's value", function () { + const signer = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: exclusiveC14n, + signatureAlgorithm: rsaSha256, + }); + signer.addReference({ + xpath: "//*[local-name(.)='item']", + transforms: [inclusiveC14n], + digestAlgorithm: sha256, + }); + signer.computeSignature('value'); + + const signedXml = signer.getSignedXml(); + expect(verify(signedXml)).to.be.true; + + const changedAncestor = signedXml.replace('xml:lang="en"', 'xml:lang="de"'); + expect(changedAncestor).not.to.equal(signedXml); + expect(verify(changedAncestor)).to.be.true; + + const changedElement = signedXml.replace('xml:lang="fr"', 'xml:lang="de"'); + expect(changedElement).not.to.equal(signedXml); + expect(verify(changedElement)).to.be.false; + }); + it("canonicalizes inherited xml:lang through getCanonXml", function () { + const doc = new xmldom.DOMParser().parseFromString( + 'value', + ); + const item = doc.getElementsByTagName("item")[0]; + + expect(new SignedXml().getCanonXml([inclusiveC14n], item)).to.equal( + 'value', + ); + }); + it("keeps a local default namespace over an ancestor's value", function () { + const signer = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: exclusiveC14n, + signatureAlgorithm: rsaSha256, + }); + signer.addReference({ + xpath: "//*[local-name(.)='item']", + transforms: [exclusiveC14n], + digestAlgorithm: sha256, + inclusiveNamespacesPrefixList: ["#default"], + }); + signer.computeSignature( + 'value', + ); + + const signedXml = signer.getSignedXml(); + expect(verify(signedXml)).to.be.true; + + const changedAncestor = signedXml.replace('xmlns="urn:ancestor"', 'xmlns="urn:other"'); + expect(changedAncestor).not.to.equal(signedXml); + expect(verify(changedAncestor)).to.be.true; + + const changedElement = signedXml.replace('xmlns="urn:local"', 'xmlns="urn:other"'); + expect(changedElement).not.to.equal(signedXml); + expect(verify(changedElement)).to.be.false; + }); + it("retains the default namespace on prefixed descendants until explicitly reset", function () { + const doc = new xmldom.DOMParser().parseFromString( + 'onetwo', + ); + + expect( + new SignedXml().getCanonXml([exclusiveC14n], doc.documentElement, { + inclusiveNamespacesPrefixList: ["#default"], + }), + ).to.equal( + 'onetwo', + ); + }); +}); diff --git a/test/c14n-non-exclusive-unit-tests.spec.ts b/test/c14n-non-exclusive-unit-tests.spec.ts index 678cb320..447a69ae 100644 --- a/test/c14n-non-exclusive-unit-tests.spec.ts +++ b/test/c14n-non-exclusive-unit-tests.spec.ts @@ -165,6 +165,13 @@ describe("C14N non-exclusive canonicalization tests", function () { test_findAncestorNs(xml, xpath, expected); }); + it("findAncestorNs: Returns no namespaces when the XPath selects nothing", function () { + const xml = ""; + const xpath = "/root/missing"; + + test_findAncestorNs(xml, xpath, []); + }); + // Tests for c14nCanonicalization it("C14n: Correctly picks up root ancestor namespace", function () { const xml = ""; diff --git a/test/constructor-options.spec.ts b/test/constructor-options.spec.ts new file mode 100644 index 00000000..5ce6819d --- /dev/null +++ b/test/constructor-options.spec.ts @@ -0,0 +1,65 @@ +import * as fs from "fs"; +import * as xmldom from "@xmldom/xmldom"; +import { expect } from "chai"; +import { SignedXml, SignedXmlOptions } from "../src/index"; + +const exclusiveC14n = "http://www.w3.org/2001/10/xml-exc-c14n#"; +const envelopedSignature = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"; + +function sign(xml: string, transforms: string[], options: SignedXmlOptions = {}) { + const signer = new SignedXml({ + ...options, + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: exclusiveC14n, + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + signer.addReference({ + xpath: "//*[local-name(.)='item']", + transforms, + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + }); + signer.computeSignature(xml); + return signer.getSignedXml(); +} + +function verify(signedXml: string, options: SignedXmlOptions = {}) { + const verifier = new SignedXml({ + ...options, + publicCert: fs.readFileSync("./test/static/client_public.pem"), + }); + verifier.loadSignature( + verifier.findSignatures(new xmldom.DOMParser().parseFromString(signedXml))[0], + ); + const valid = verifier.checkSignature(signedXml); + + return { valid, signedReferences: verifier.getSignedReferences() }; +} + +describe("SignedXml constructor options", function () { + it("reuses the ID in idAttribute when signing and finds it when verifying", function () { + const options = { idAttribute: "AssertionID" }; + const signedXml = sign( + 'trusted', + [exclusiveC14n], + options, + ); + + const result = verify(signedXml, options); + + expect(result.valid).to.be.true; + expect(result.signedReferences).to.deep.equal(['trusted']); + }); + + it("applies implicitTransforms when verifying", function () { + const embedded = sign('trusted', [ + envelopedSignature, + ]).replace("", ''); + + const withoutImplicit = verify(embedded); + const withImplicit = verify(embedded, { implicitTransforms: [exclusiveC14n] }); + + expect(withoutImplicit.valid).to.be.false; + expect(withImplicit.valid).to.be.true; + expect(withImplicit.signedReferences).to.deep.equal(['trusted']); + }); +}); diff --git a/test/document-level-processing-instructions.spec.ts b/test/document-level-processing-instructions.spec.ts new file mode 100644 index 00000000..4c063ad8 --- /dev/null +++ b/test/document-level-processing-instructions.spec.ts @@ -0,0 +1,65 @@ +import { expect } from "chai"; +import * as fs from "fs"; +import * as xmldom from "@xmldom/xmldom"; +import { SignedXml } from "../src/index"; + +const exclusiveC14n = "http://www.w3.org/2001/10/xml-exc-c14n#"; +const envelopedSignature = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"; + +describe("document-level processing instructions in an empty-URI reference", function () { + const verify = (xml: string) => { + const verifier = new SignedXml({ + publicCert: fs.readFileSync("./test/static/client_public.pem"), + }); + const doc = new xmldom.DOMParser().parseFromString(xml); + verifier.loadSignature(verifier.findSignatures(doc)[0]); + return verifier.checkSignature(xml); + }; + + const sign = (xml: string, transforms: string[]) => { + const signer = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: exclusiveC14n, + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + signer.addReference({ + xpath: "/*", + isEmptyUri: true, + transforms, + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + }); + signer.computeSignature(xml); + return signer.getSignedXml(); + }; + + for (const { description, transforms } of [ + { + description: "enveloped signature before canonicalization", + transforms: [envelopedSignature, exclusiveC14n], + }, + { + description: "canonicalization before enveloped signature", + transforms: [exclusiveC14n, envelopedSignature], + }, + ]) { + it(`rejects a changed stylesheet instruction with ${description}`, function () { + const signedXml = sign( + '\ntrusted', + transforms, + ); + expect(verify(signedXml)).to.be.true; + + const tamperedXml = signedXml.replace("trusted.xsl", "attacker.xsl"); + expect(tamperedXml).not.to.equal(signedXml); + expect(verify(tamperedXml)).to.be.false; + }); + + it(`rejects an added stylesheet instruction with ${description}`, function () { + const signedXml = sign("trusted", transforms); + expect(verify(signedXml)).to.be.true; + + const tamperedXml = `\n${signedXml}`; + expect(verify(tamperedXml)).to.be.false; + }); + } +}); diff --git a/test/exclusive-prefixlist-integrity.spec.ts b/test/exclusive-prefixlist-integrity.spec.ts new file mode 100644 index 00000000..9721094a --- /dev/null +++ b/test/exclusive-prefixlist-integrity.spec.ts @@ -0,0 +1,44 @@ +import * as fs from "fs"; +import * as xmldom from "@xmldom/xmldom"; +import { expect } from "chai"; +import { SignedXml } from "../src/index"; + +const exclusiveC14n = "http://www.w3.org/2001/10/xml-exc-c14n#"; + +function verify(xml: string): boolean { + const verifier = new SignedXml({ + publicCert: fs.readFileSync("./test/static/client_public.pem"), + }); + const doc = new xmldom.DOMParser().parseFromString(xml); + verifier.loadSignature(verifier.findSignatures(doc)[0]); + return verifier.checkSignature(xml); +} + +describe("exclusive canonicalization PrefixList integrity", function () { + it("rejects a namespace declaration impersonated by an ordinary prefixed attribute", function () { + const signer = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: exclusiveC14n, + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + signer.addReference({ + xpath: "//*[local-name(.)='item']", + transforms: [exclusiveC14n], + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + inclusiveNamespacesPrefixList: ["bar"], + }); + signer.computeSignature( + '', + ); + + const signedXml = signer.getSignedXml(); + expect(verify(signedXml)).to.be.true; + + const tamperedXml = signedXml.replace( + 'type="bar:Admin"', + 'xmlns:bar="urn:value" type="bar:Admin"', + ); + expect(tamperedXml).not.to.equal(signedXml); + expect(verify(tamperedXml)).to.be.false; + }); +}); diff --git a/test/key-info-tests.spec.ts b/test/key-info-tests.spec.ts index 19c8f4a7..dd42b723 100644 --- a/test/key-info-tests.spec.ts +++ b/test/key-info-tests.spec.ts @@ -42,4 +42,39 @@ describe("KeyInfo tests", function () { expect(keyInfo).to.be.undefined; }); + + it("passes getCertFromKeyInfo the Signature's own KeyInfo, not one inside an Object", function () { + const sig = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + sig.addReference({ + xpath: "//*[local-name(.)='x']", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + }); + sig.computeSignature(""); + const signedXml = sig + .getSignedXml() + .replace( + "", + "nested", + ); + + const keyInfos: (Node | null | undefined)[] = []; + const verifier = new SignedXml({ + publicCert: fs.readFileSync("./test/static/client_public.pem"), + getCertFromKeyInfo: (keyInfo) => { + keyInfos.push(keyInfo); + return null; + }, + }); + verifier.loadSignature( + verifier.findSignatures(new xmldom.DOMParser().parseFromString(signedXml))[0], + ); + + expect(verifier.checkSignature(signedXml)).to.be.true; + expect(keyInfos.map((keyInfo) => keyInfo?.toString() ?? null)).to.deep.equal([null]); + }); }); diff --git a/test/namespace-uri-escaping.spec.ts b/test/namespace-uri-escaping.spec.ts new file mode 100644 index 00000000..c96c634a --- /dev/null +++ b/test/namespace-uri-escaping.spec.ts @@ -0,0 +1,61 @@ +import * as fs from "fs"; +import * as xmldom from "@xmldom/xmldom"; +import { expect } from "chai"; +import { SignedXml } from "../src/index"; + +const canonicalizations = [ + ["inclusive", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"], + ["exclusive", "http://www.w3.org/2001/10/xml-exc-c14n#"], +] as const; + +const namespaceCases = [ + { name: "default", element: "item", declaration: "xmlns" }, + { name: "prefixed", element: "p:item", declaration: "xmlns:p" }, +] as const; + +describe("Namespace declarations in signed references", function () { + for (const [algorithmName, canonicalizationAlgorithm] of canonicalizations) { + for (const { name, element, declaration } of namespaceCases) { + it(`rejects hidden Access in ${name} namespace under ${algorithmName} c14n`, function () { + const signer = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm, + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + signer.addReference({ + xpath: "//*[local-name(.)='item']", + transforms: [canonicalizationAlgorithm], + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + }); + signer.computeSignature( + `<${element} ${declaration}="urn:test" Access="user">value`, + ); + + const signedXml = signer.getSignedXml(); + const original = `${declaration}="urn:test" Access="user"`; + expect(signedXml).to.include(original); + const tampered = signedXml.replace( + original, + `${declaration}="urn:test" Access="user"`, + ); + const item = new xmldom.DOMParser() + .parseFromString(tampered) + .getElementsByTagName(element)[0]; + expect(item.hasAttribute("Access")).to.be.false; + expect(item.namespaceURI).to.equal('urn:test" Access="user'); + + const createVerifier = () => { + const verifier = new SignedXml({ + publicCert: fs.readFileSync("./test/static/client_public.pem"), + }); + verifier.loadSignature(signer.getSignatureXml()); + return verifier; + }; + expect(createVerifier().checkSignature(signedXml)).to.be.true; + const tamperedVerifier = createVerifier(); + expect(tamperedVerifier.checkSignature(tampered)).to.be.false; + expect(tamperedVerifier.getSignedReferences()).to.be.empty; + }); + } + } +}); diff --git a/test/processing-instruction-nameid.spec.ts b/test/processing-instruction-nameid.spec.ts new file mode 100644 index 00000000..f58f54b3 --- /dev/null +++ b/test/processing-instruction-nameid.spec.ts @@ -0,0 +1,50 @@ +import * as fs from "fs"; +import * as xmldom from "@xmldom/xmldom"; +import { expect } from "chai"; +import { SignedXml } from "../src/index"; + +const assertionNamespace = "urn:oasis:names:tc:SAML:2.0:assertion"; +const exclusiveC14n = "http://www.w3.org/2001/10/xml-exc-c14n#"; +const envelopedSignature = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"; + +describe("processing instructions in a signed SAML assertion", function () { + it("rejects a NameID whose textContent changes after signing", function () { + const signer = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: exclusiveC14n, + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + signer.addReference({ + xpath: "//*[local-name(.)='Assertion']", + transforms: [envelopedSignature, exclusiveC14n], + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + }); + signer.computeSignature( + 'https://idp.exampleadmin@victim.com.evil.com', + { location: { reference: "//*[local-name(.)='Issuer']", action: "after" } }, + ); + + const signedXml = signer.getSignedXml(); + const tamperedXml = signedXml.replace( + "admin@victim.com.evil.com", + "admin@victim.com", + ); + expect(tamperedXml).not.to.equal(signedXml); + + const tamperedDoc = new xmldom.DOMParser().parseFromString(tamperedXml); + const nameId = tamperedDoc.getElementsByTagNameNS(assertionNamespace, "NameID")[0]; + expect(nameId.textContent).to.equal("admin@victim.com"); + + const verify = (xml: string) => { + const verifier = new SignedXml({ + publicCert: fs.readFileSync("./test/static/client_public.pem"), + }); + const doc = new xmldom.DOMParser().parseFromString(xml); + verifier.loadSignature(verifier.findSignatures(doc)[0]); + return verifier.checkSignature(xml); + }; + + expect(verify(signedXml)).to.be.true; + expect(verify(tamperedXml)).to.be.false; + }); +}); diff --git a/test/processing-instructions.spec.ts b/test/processing-instructions.spec.ts new file mode 100644 index 00000000..0f4d866a --- /dev/null +++ b/test/processing-instructions.spec.ts @@ -0,0 +1,72 @@ +import { expect } from "chai"; +import * as fs from "fs"; +import * as xmldom from "@xmldom/xmldom"; +import { + C14nCanonicalization, + C14nCanonicalizationWithComments, + ExclusiveCanonicalization, + ExclusiveCanonicalizationWithComments, + SignedXml, +} from "../src/index"; + +const canonicalizers = [ + C14nCanonicalization, + C14nCanonicalizationWithComments, + ExclusiveCanonicalization, + ExclusiveCanonicalizationWithComments, +]; + +describe("processing instructions in signed references", function () { + for (const Canonicalization of canonicalizers) { + const algorithm = new Canonicalization().getAlgorithmName(); + + it(`rejects text replaced by a processing instruction with ${algorithm}`, function () { + const signer = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: algorithm, + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + signer.addReference({ + xpath: "//item", + transforms: [algorithm], + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + }); + signer.computeSignature("payload"); + + const verify = (xml: string) => { + const verifier = new SignedXml({ + publicCert: fs.readFileSync("./test/static/client_public.pem"), + }); + verifier.loadSignature(signer.getSignatureXml()); + return verifier.checkSignature(xml); + }; + + const signedXml = signer.getSignedXml(); + expect(verify(signedXml)).to.be.true; + const tamperedXml = signedXml.replace(">payload", ">"); + expect(verify(tamperedXml)).to.be.false; + }); + + it(`preserves processing instruction markup and data with ${algorithm}`, function () { + const doc = new xmldom.DOMParser().parseFromString( + "", + ); + + expect(new Canonicalization().process(doc.documentElement, {})).to.equal( + "", + ); + }); + + it(`canonicalizes processing instructions outside the document element with ${algorithm}`, function () { + const doc = new xmldom.DOMParser().parseFromString( + '\n\n\nHello, world!\n\n\n', + ); + const stylesheet = ''; + const expected = algorithm.endsWith("#WithComments") + ? `${stylesheet}\nHello, world!\n\n\n` + : `${stylesheet}\nHello, world!\n`; + + expect(new Canonicalization().process(doc, {})).to.equal(expected); + }); + } +}); diff --git a/test/signature-integration-tests.spec.ts b/test/signature-integration-tests.spec.ts index a848f103..50b8ef93 100644 --- a/test/signature-integration-tests.spec.ts +++ b/test/signature-integration-tests.spec.ts @@ -1,6 +1,12 @@ import * as xpath from "xpath"; import * as xmldom from "@xmldom/xmldom"; -import { SignedXml, SignedXmlOptions } from "../src/index"; +import { + C14nCanonicalization, + ComputeSignatureOptionsLocation, + SignedXml, + SignedXmlOptions, +} from "../src/index"; +import * as crypto from "crypto"; import * as fs from "fs"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; @@ -447,6 +453,143 @@ describe("Signature integration tests", function () { }); }); + describe("InclusiveNamespaces on a reference", function () { + const envelopedSignature = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"; + const exclusiveC14n = "http://www.w3.org/2001/10/xml-exc-c14n#"; + const exclusiveC14nWithComments = "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"; + const xml = 'text'; + // Nothing uses `b`, so exclusive canonicalization renders it only when it reads the PrefixList. + const signedReference = xml; + + function verify(signedXml: string, configure?: (verifier: SignedXml) => void) { + const verifier = new SignedXml({ + publicCert: fs.readFileSync("./test/static/client_public.pem"), + }); + configure?.(verifier); + verifier.loadSignature( + verifier.findSignatures(new xmldom.DOMParser().parseFromString(signedXml))[0], + ); + const valid = verifier.checkSignature(signedXml); + + return { valid, signedReferences: verifier.getSignedReferences() }; + } + + for (const fixture of [ + "inclusive_namespaces_exc_c14n.xml", + "inclusive_namespaces_exc_c14n_with_comments.xml", + "inclusive_namespaces_enveloped_signature_after_exc_c14n.xml", + // Signed by xml-crypto 6.3.2, which wrote InclusiveNamespaces outside the exc-c14n namespace: + // https://github.com/node-saml/xml-crypto/issues/633 + "inclusive_namespaces_in_with_comments_namespace.xml", + // PrefixList="a b" is white space delimited: https://www.w3.org/TR/xml-exc-c14n/#sec-Use + "inclusive_namespaces_prefix_list_with_tab.xml", + ]) { + it(`should verify ${fixture}`, function () { + const result = verify(fs.readFileSync(`./test/static/${fixture}`, "utf8")); + + expect(result.valid).to.be.true; + expect(result.signedReferences).to.deep.equal([signedReference]); + }); + } + + it("should verify a PrefixList that only a later exclusive canonicalization carries", function () { + // The first transform has no PrefixList, so it drops the unused `b`, and the second parses its + // octets, so its PrefixList has nothing to keep: + // https://www.w3.org/TR/xml-exc-c14n/#sec-Specification + // https://www.w3.org/TR/xmldsig-core1/#sec-ReferenceProcessingModel + const result = verify( + fs.readFileSync("./test/static/inclusive_namespaces_on_second_of_two_exc_c14n.xml", "utf8"), + ); + + expect(result.valid).to.be.true; + expect(result.signedReferences).to.deep.equal(["text"]); + }); + + it("should give each transform its own PrefixList when a custom transform sits between", function () { + // The first exclusive canonicalization drops the unused `b`, the custom transform declares it + // again, and the last one's PrefixList keeps it: + // https://www.w3.org/TR/xmldsig-core1/#sec-Transforms + class DeclareB { + process(node: Node) { + (node as Element).setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:b", "urn:b"); + return node; + } + + getAlgorithmName() { + return "urn:xml-crypto:test:declare-b"; + } + } + + const result = verify( + fs.readFileSync("./test/static/inclusive_namespaces_after_custom_transform.xml", "utf8"), + (verifier) => { + verifier.CanonicalizationAlgorithms["urn:xml-crypto:test:declare-b"] = DeclareB; + }, + ); + + expect(result.valid).to.be.true; + expect(result.signedReferences).to.deep.equal(['text']); + }); + + it("should sign a loaded reference with its current PrefixList", function () { + const signedXml = fs.readFileSync("./test/static/inclusive_namespaces_exc_c14n.xml", "utf8"); + const sig = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + publicCert: fs.readFileSync("./test/static/client_public.pem"), + canonicalizationAlgorithm: exclusiveC14n, + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + sig.loadSignature(sig.findSignatures(new xmldom.DOMParser().parseFromString(signedXml))[0]); + expect(sig.checkSignature(signedXml)).to.be.true; + const [reference] = sig.getReferences(); + reference.xpath = "/*"; + reference.isEmptyUri = true; + reference.inclusiveNamespacesPrefixList = ["c"]; + sig.computeSignature('text'); + + const result = verify(sig.getSignedXml()); + + expect(result.valid).to.be.true; + expect(result.signedReferences).to.deep.equal(['text']); + }); + + for (const { description, transforms } of [ + { + description: "exclusive canonicalization after enveloped-signature", + transforms: [envelopedSignature, exclusiveC14n], + }, + { + description: "exclusive canonicalization with comments", + transforms: [envelopedSignature, exclusiveC14nWithComments], + }, + { + description: "enveloped-signature after exclusive canonicalization", + transforms: [exclusiveC14n, envelopedSignature], + }, + ]) { + it(`should verify its own signature for ${description}`, function () { + const sig = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: exclusiveC14n, + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + sig.addReference({ + xpath: "/*", + isEmptyUri: true, + transforms, + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + inclusiveNamespacesPrefixList: ["b"], + }); + sig.computeSignature(xml); + + const result = verify(sig.getSignedXml()); + + expect(result.valid).to.be.true; + expect(result.signedReferences).to.deep.equal([signedReference]); + }); + } + }); + it("should still verify a loaded signature after signing another document fails", function () { const signedXml = fs.readFileSync("./test/static/valid_signature.xml", "utf8"); const doc = new xmldom.DOMParser().parseFromString(signedXml); @@ -474,6 +617,21 @@ describe("Signature integration tests", function () { [exclusiveCanonicalization, envelopedSignature], ]; + const signatureValueSpellings: Record string> = { + "line-wrapped": (value) => `${value.slice(0, 40)}\n${value.slice(40)}`, + "without padding": (value) => value.replace(/=+$/, ""), + "in the base64url alphabet": (value) => value.replace(/\+/g, "-").replace(/\//g, "_"), + }; + + function respellSignatureValue(xml: string, respell: (value: string) => string) { + const respelled = xml.replace( + /([^<]*)<\/SignatureValue>/, + (_, value: string) => `${respell(value)}`, + ); + expect(respelled, "the respelling must change the SignatureValue").not.to.equal(xml); + return respelled; + } + describe("copies of the enveloped signature", function () { function sign(xml: string, reference: string, transforms: string[]) { const signer = new SignedXml({ @@ -505,6 +663,8 @@ describe("Signature integration tests", function () { const lastSignatureIn = (xml: string) => new SignedXml().findSignatures(new xmldom.DOMParser().parseFromString(xml)).pop() as Node; + const firstSignatureIn = (xml: string) => + new SignedXml().findSignatures(new xmldom.DOMParser().parseFromString(xml))[0]; for (const transforms of envelopedTransformOrders) { const order = transforms[0] === envelopedSignature ? "before" : "after"; @@ -543,6 +703,41 @@ describe("Signature integration tests", function () { ); expect(verifier.getSignedReferences()).to.be.empty; }); + + for (const [spelling, respell] of Object.entries(signatureValueSpellings)) { + it(`should reject a copy whose SignatureValue is ${spelling}, enveloped-signature ${order} canonicalization`, function () { + const { signedXml, signatureXml } = sign( + "user", + "//assertion", + transforms, + ); + const respelled = respellSignatureValue(signatureXml, respell); + const tampered = injectCopy(signedXml, respelled, "user"); + + const verifier = createVerifier(firstSignatureIn(tampered)); + expect(() => verifier.checkSignature(tampered)).to.throw( + "Cannot validate a document which contains multiple Signature elements with the same SignatureValue", + ); + expect(verifier.getSignedReferences()).to.be.empty; + }); + } + + it(`should reject a copy that nests the genuine SignatureValue ahead of its own, enveloped-signature ${order} canonicalization`, function () { + const { signedXml, signatureXml } = sign( + "user", + "//assertion", + transforms, + ); + const nested = signatureXml.replace( + /[^<]*<\/SignatureValue>/, + (genuine) => `${genuine}AAAA`, + ); + const tampered = injectCopy(signedXml, nested, "user"); + + const verifier = createVerifier(firstSignatureIn(tampered)); + expect(() => verifier.checkSignature(tampered)).to.throw(/invalid signature/); + expect(verifier.getSignedReferences()).to.be.empty; + }); } }); @@ -1186,4 +1381,201 @@ describe("Signature integration tests", function () { /in order to prevent signature wrapping attack/, ); }); + + it("rejects a loaded signature without a SignatureValue, even after loading one with it", function () { + const sig = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + sig.addReference({ + xpath: "//*[local-name(.)='x']", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + }); + sig.computeSignature("trusted"); + const withoutValue = sig + .getSignatureXml() + .replace(/[^<]*<\/SignatureValue>/, ""); + + const verifier = new SignedXml({ + publicCert: fs.readFileSync("./test/static/client_public.pem"), + }); + verifier.loadSignature(sig.getSignatureXml()); + verifier.loadSignature(withoutValue); + + expect(() => verifier.checkSignature(sig.getSignedXml())).to.throw(/invalid signature/); + }); + + it("rejects a document whose copy of the loaded signature has no SignedInfo", function () { + const sig = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: "http://www.w3.org/TR/2001/REC-xml-c14n-20010315", + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + sig.addReference({ + xpath: "/*", + transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"], + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + }); + sig.computeSignature("trusted"); + + const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml()); + const signedInfo = xpath.select1("//*[local-name(.)='SignedInfo']", doc); + isDomNode.assertIsNodeLike(signedInfo); + signedInfo.parentNode?.removeChild(signedInfo); + const tampered = doc.toString(); + + const verifier = new SignedXml({ + publicCert: fs.readFileSync("./test/static/client_public.pem"), + }); + verifier.loadSignature(sig.getSignatureXml()); + + expect(() => verifier.checkSignature(tampered)).to.throw( + /could not find SignedInfo element in the message/, + ); + }); + + describe("a loaded signature checked against a document that spells its SignatureValue differently", function () { + const publicCert = fs.readFileSync("./test/static/client_public.pem"); + + function signWithInheritedNamespace() { + const sig = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: "http://www.w3.org/TR/2001/REC-xml-c14n-20010315", + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + sig.addReference({ + xpath: "//*[local-name(.)='item']", + transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + }); + sig.computeSignature('trusted'); + return sig; + } + + function loadFromOriginal(sig: SignedXml) { + const verifier = new SignedXml({ publicCert }); + verifier.loadSignature( + verifier.findSignatures(new xmldom.DOMParser().parseFromString(sig.getSignedXml()))[0], + ); + return verifier; + } + + for (const [spelling, respell] of Object.entries(signatureValueSpellings)) { + it(`verifies it in the namespace context it has in the checked document, SignatureValue ${spelling}`, function () { + const sig = signWithInheritedNamespace(); + const verifier = new SignedXml({ publicCert }); + verifier.loadSignature(sig.getSignatureXml()); + + expect(verifier.checkSignature(respellSignatureValue(sig.getSignedXml(), respell))).to.be + .true; + }); + + it(`rejects it when it is valid only in the loaded copy's namespace context, SignatureValue ${spelling}`, function () { + const sig = signWithInheritedNamespace(); + const withoutNamespace = sig.getSignedXml().replace(' xmlns:q="urn:q"', ""); + + expect(() => + loadFromOriginal(sig).checkSignature(respellSignatureValue(withoutNamespace, respell)), + ).to.throw(/invalid signature/); + }); + } + + it("rejects it when it is valid only in the loaded copy's namespace context and the checked document's SignatureValue differs", function () { + const sig = signWithInheritedNamespace(); + const withoutNamespace = sig.getSignedXml().replace(' xmlns:q="urn:q"', ""); + const otherValue = respellSignatureValue( + withoutNamespace, + (value) => `AAAA${value.slice(4)}`, + ); + + expect(() => loadFromOriginal(sig).checkSignature(otherValue)).to.throw(/invalid signature/); + }); + }); + + describe("inclusive canonicalization of SignedInfo in a document with two signatures", function () { + const c14n = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"; + const samlp = "urn:oasis:names:tc:SAML:2.0:protocol"; + const saml = "urn:oasis:names:tc:SAML:2.0:assertion"; + const unsigned = + `` + + "idp" + + '' + + "idp"; + + function sign(xml: string, xpath: string, location: ComputeSignatureOptionsLocation) { + const sig = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: c14n, + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + sig.addReference({ + xpath, + transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature", c14n], + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + }); + sig.computeSignature(xml, { prefix: "ds", location }); + return sig.getSignedXml(); + } + + function signAssertion() { + return sign(unsigned, "//*[local-name(.)='Assertion']", { + reference: "//*[local-name(.)='Assertion']/*[local-name(.)='Issuer']", + action: "after", + }); + } + + function checkEachSignature(xml: string) { + const doc = new xmldom.DOMParser().parseFromString(xml); + return new SignedXml().findSignatures(doc).map((signature) => { + const verifier = new SignedXml({ + publicCert: fs.readFileSync("./test/static/client_public.pem"), + }); + verifier.loadSignature(signature); + return verifier.checkSignature(xml); + }); + } + + it("verifies a signature whose SignedInfo is not the first in the document", function () { + const signed = sign(signAssertion(), "/*", { + reference: "/*/*[local-name(.)='Issuer']", + action: "after", + }); + + expect(checkEachSignature(signed)).to.deep.equal([true, true]); + }); + + it("signs SignedInfo with the namespaces in scope of its own Signature", function () { + const signed = sign(signAssertion(), "/*", { reference: "/*", action: "append" }); + + const doc = new xmldom.DOMParser().parseFromString(signed); + const responseSignature = xpath.select1("/*/*[local-name(.)='Signature']", doc); + isDomNode.assertIsElementNode(responseSignature); + const signedInfo = xpath.select1("./*[local-name(.)='SignedInfo']", responseSignature); + const signatureValue = xpath.select1( + "string(./*[local-name(.)='SignatureValue'])", + responseSignature, + ); + isDomNode.assertIsNodeLike(signedInfo); + expect(signatureValue).to.be.a("string"); + + const canonicalSignedInfo = new C14nCanonicalization().process(signedInfo, { + ancestorNamespaces: [ + { prefix: "samlp", namespaceURI: samlp }, + { prefix: "saml", namespaceURI: saml }, + ], + }); + expect( + crypto.verify( + "sha256", + Buffer.from(canonicalSignedInfo), + fs.readFileSync("./test/static/client_public.pem"), + Buffer.from(String(signatureValue), "base64"), + ), + "SignatureValue must cover SignedInfo without the Assertion's xmlns:xs", + ).to.be.true; + expect(checkEachSignature(signed)).to.deep.equal([true, true]); + }); + }); }); diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index 42363b96..f1006b8e 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -432,6 +432,28 @@ describe("Signature unit tests", function () { ); }); + for (const action of ["before", "after"] as const) { + it(`signer rejects inserting signature ${action} a node without a parent`, function () { + const sig = new SignedXml(); + + sig.privateKey = fs.readFileSync("./test/static/client.pem"); + sig.addReference({ + xpath: "//*[local-name(.)='repository']", + 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"; + + expect(() => + sig.computeSignature('github', { + location: { reference: "//@id", action }, + }), + ).to.throw(`selects a node without a parent, so we can't insert \`${action}\``); + }); + } + it("signer creates signature with correct structure", function () { class DummyDigest { getHash = function () { @@ -1251,41 +1273,83 @@ describe("Signature unit tests", function () { expect(result.includes(assertionId)).to.be.true; }); - it("creates InclusiveNamespaces element when inclusiveNamespacesPrefixList is set on Reference", function () { - const xml = ""; - const sig = new SignedXml(); - sig.privateKey = fs.readFileSync("./test/static/client.pem"); - - sig.addReference({ - xpath: "//*[local-name(.)='root']", - transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"], - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - uri: "", - digestValue: "", - inclusiveNamespacesPrefixList: ["prefix1", "prefix2"], - }); - - 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 signedXml = sig.getSignedXml(); - - const doc = new xmldom.DOMParser().parseFromString(signedXml); - const inclusiveNamespaces = xpath.select( - "//*[local-name(.)='Reference']/*[local-name(.)='Transforms']/*[local-name(.)='Transform']/*[local-name(.)='InclusiveNamespaces']", - doc.documentElement, - ); - isDomNode.assertIsArrayOfNodes(inclusiveNamespaces); - expect(inclusiveNamespaces.length, "InclusiveNamespaces element should exist").to.equal(1); + describe("InclusiveNamespaces on a reference", function () { + const envelopedSignature = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"; + const exclusiveC14n = "http://www.w3.org/2001/10/xml-exc-c14n#"; + const exclusiveC14nWithComments = "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"; + const c14n = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"; + // A Transform carries only its own algorithm's parameters, and PrefixList belongs to exclusive + // canonicalization, in its namespace for both identifiers: + // https://www.w3.org/TR/xmldsig-core1/#sec-Transforms + // https://www.w3.org/TR/xml-exc-c14n/#sec-Use + const prefixList = `{${exclusiveC14n}}InclusiveNamespaces PrefixList="prefix1 prefix2"`; + + const cases: { description: string; parameters: [string, string[]][] }[] = [ + { + description: "exclusive canonicalization after enveloped-signature", + parameters: [ + [envelopedSignature, []], + [exclusiveC14n, [prefixList]], + ], + }, + { + description: "exclusive canonicalization with comments", + parameters: [ + [envelopedSignature, []], + [exclusiveC14nWithComments, [prefixList]], + ], + }, + { + description: "enveloped-signature after exclusive canonicalization", + parameters: [ + [exclusiveC14n, [prefixList]], + [envelopedSignature, []], + ], + }, + { + description: "inclusive canonicalization", + parameters: [ + [envelopedSignature, []], + [c14n, []], + ], + }, + ]; - const firstNamespace = inclusiveNamespaces[0]; - isDomNode.assertIsElementNode(firstNamespace); + for (const { description, parameters } of cases) { + it(`writes the PrefixList only as a parameter of exclusive canonicalization, for ${description}`, function () { + const sig = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: exclusiveC14n, + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + sig.addReference({ + xpath: "/*", + isEmptyUri: true, + transforms: parameters.map(([algorithm]) => algorithm), + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + inclusiveNamespacesPrefixList: ["prefix1", "prefix2"], + }); + sig.computeSignature(""); - const prefixListAttribute = firstNamespace.getAttribute("PrefixList"); - expect( - prefixListAttribute, - "InclusiveNamespaces element should have the correct PrefixList attribute value", - ).to.equal("prefix1 prefix2"); + const transforms = xpath.select( + "//*[local-name(.)='Reference']/*[local-name(.)='Transforms']/*[local-name(.)='Transform']", + new xmldom.DOMParser().parseFromString(sig.getSignatureXml()), + ); + isDomNode.assertIsArrayOfNodes(transforms); + expect( + transforms + .filter(isDomNode.isElementNode) + .map((transform) => [ + transform.getAttribute("Algorithm"), + Array.from(transform.childNodes).map((child) => + isDomNode.isElementNode(child) + ? `{${child.namespaceURI}}${child.localName} PrefixList="${child.getAttribute("PrefixList")}"` + : child.toString(), + ), + ]), + ).to.deep.equal(parameters); + }); + } }); it("does not create InclusiveNamespaces element when inclusiveNamespacesPrefixList is not set on Reference", function () { diff --git a/test/static/inclusive_namespaces_after_custom_transform.xml b/test/static/inclusive_namespaces_after_custom_transform.xml new file mode 100644 index 00000000..23013a51 --- /dev/null +++ b/test/static/inclusive_namespaces_after_custom_transform.xml @@ -0,0 +1 @@ +textJ/oTNUtEvzFcy+pV4Z07TYkufQV5N8nccDdeW/Abn80=QFUhrdJGOEx1hyaS/FH6y3ZuPji+O4fnYUZ6/h+oJk5DHQwaQ/eMAjMdI9BGJuKa5eHuekr4yzwI/tP9Tsz5f5iGsHz7LRKNRkxPTMMFKbaD0cuRBZuVa5rdAvbLgwSSMT6e437qN0xVmcmW3oiQ61sIIQlceStFgE9FjsiShnY= \ No newline at end of file diff --git a/test/static/inclusive_namespaces_enveloped_signature_after_exc_c14n.xml b/test/static/inclusive_namespaces_enveloped_signature_after_exc_c14n.xml new file mode 100644 index 00000000..2c50cebc --- /dev/null +++ b/test/static/inclusive_namespaces_enveloped_signature_after_exc_c14n.xml @@ -0,0 +1 @@ +textn/NO5ufuSTA7dkgzfXLtp1wMn2kVx1bTPw7v01eomJU=SnzDUl9CnQxdR2NG7AqHESVWwsvJ61JNiEp33Tzh1CJWqEmv8FYcgLpu/a5MBaYaykbDt+cfUocz3YmsVD6P17hXyTEK/xYtSoW4xcFjH3+41BV+XPjaq5Q+pkLnR7xemUm5H8zxSMrvepdIP0NP0UHdFmmN23MNTQ56x8inYM8= \ No newline at end of file diff --git a/test/static/inclusive_namespaces_exc_c14n.xml b/test/static/inclusive_namespaces_exc_c14n.xml new file mode 100644 index 00000000..a164637e --- /dev/null +++ b/test/static/inclusive_namespaces_exc_c14n.xml @@ -0,0 +1 @@ +textn/NO5ufuSTA7dkgzfXLtp1wMn2kVx1bTPw7v01eomJU=V+cIDAwPxtVA68g3hvqwyRI9qOwdVcPwsBsMGhy5DCn4pyLiUZup4GR4AutoxlHlCiovKeYUXXymynJ8f0C1Dqyzt6q4C7ZxaBVgL9CVpje0t5JM+EtaJnSbuEUDmqPj/f/YwskhQCJ8HGEpabaAUa0igZPp07GmeM1g1Oi7oGU= \ No newline at end of file diff --git a/test/static/inclusive_namespaces_exc_c14n_with_comments.xml b/test/static/inclusive_namespaces_exc_c14n_with_comments.xml new file mode 100644 index 00000000..38bec1d8 --- /dev/null +++ b/test/static/inclusive_namespaces_exc_c14n_with_comments.xml @@ -0,0 +1 @@ +textn/NO5ufuSTA7dkgzfXLtp1wMn2kVx1bTPw7v01eomJU=THeQJYN9U2SBAyGy3AqFcUhZhDQYBba2OlnewaMbOZTyTpH/ple3kJVDcHirGQAOwaA2i0JMdbZ543m3p5qKxG6tSXnvTBuMv+8jzOf7IwMjdXFadvmAuYDwdUMWi+GmDGqjdetFsUSDaY4HIMeCf6fscI0jDu7ZJnJ4OU1Z9RA= \ No newline at end of file diff --git a/test/static/inclusive_namespaces_in_with_comments_namespace.xml b/test/static/inclusive_namespaces_in_with_comments_namespace.xml new file mode 100644 index 00000000..d0f78c9e --- /dev/null +++ b/test/static/inclusive_namespaces_in_with_comments_namespace.xml @@ -0,0 +1 @@ +textn/NO5ufuSTA7dkgzfXLtp1wMn2kVx1bTPw7v01eomJU=gkke75Ilg5ctv7tpeVyhtM5Ldb4A4T6yBNZ3rpEeaj7qx6qqcvrBJsgLhgG26HTNVaa4Hjwi1OcjSOMeM2DY4q7JnQ23auGzA1Q3mmV4AqLOVF/TJ+jWLmX/liRVQ8e7yK2CBva1on7UX1plZQpSUeL+VzCd1OXPx9kLSOmq4Nc= \ No newline at end of file diff --git a/test/static/inclusive_namespaces_on_second_of_two_exc_c14n.xml b/test/static/inclusive_namespaces_on_second_of_two_exc_c14n.xml new file mode 100644 index 00000000..6823ed9b --- /dev/null +++ b/test/static/inclusive_namespaces_on_second_of_two_exc_c14n.xml @@ -0,0 +1 @@ +textglmCUKylqgcySS6P32i8OrDA/ho+RJOJqlDBDfJZFgM=UM4uH7+dT+59K1V/amb6rcu8WgbJsFKBTVp3RYpNmt1bFvxhNMaUPEx2QTQ3DbgRv0KAtZoYYHK/JCey68m7p9CYI6V6cc6lWMV8aCfis9rW/+4r/gXYefpQcnT3qvyaJOhIYtctqAmcdvd7JmbslBLAE5ktkYI7ANLQnDsFhLs= \ No newline at end of file diff --git a/test/static/inclusive_namespaces_prefix_list_with_tab.xml b/test/static/inclusive_namespaces_prefix_list_with_tab.xml new file mode 100644 index 00000000..c4cb7822 --- /dev/null +++ b/test/static/inclusive_namespaces_prefix_list_with_tab.xml @@ -0,0 +1 @@ +textn/NO5ufuSTA7dkgzfXLtp1wMn2kVx1bTPw7v01eomJU=XOI25p9pCqC2FWqHFnWmnA2LYPxnQTyBgfkgYdd19c2sxp0cAKnuQz+V/lR8Qb9CO4PqYWbBE526ZvkT8lZ3j1BC5xqLwZu+31C0LP6PHCwnxl4NaSCWP+9hX4rUnz0F2ADvV32qD6sS3fulv5VbUewdfrf2V9no9bfn8z47OP4= \ No newline at end of file diff --git a/test/validators/XmlCryptoJava/pom.xml b/test/validators/XmlCryptoJava/pom.xml deleted file mode 100644 index dd19e2a3..00000000 --- a/test/validators/XmlCryptoJava/pom.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - 4.0.0 - XML Crypto Java Tests - org.nodejs - xml-crypto-java - 1.0.0-SNAPSHOT - jar - 2015 - - - MIT License - http://www.opensource.org/licenses/mit-license.php - - - - - fcorneli - Frank Cornelis - info@e-contract.be - - - - - junit - junit - 4.13.1 - test - - - org.slf4j - slf4j-api - 1.7.12 - test - - - org.slf4j - slf4j-log4j12 - 1.7.12 - test - - - log4j - log4j - 1.2.17 - test - - - commons-io - commons-io - 2.7 - test - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.3 - - 1.7 - 1.7 - UTF-8 - - - - - - UTF-8 - - diff --git a/test/validators/XmlCryptoJava/src/test/java/org/nodejs/xmlcrypto/HMACTest.java b/test/validators/XmlCryptoJava/src/test/java/org/nodejs/xmlcrypto/HMACTest.java deleted file mode 100644 index b1000254..00000000 --- a/test/validators/XmlCryptoJava/src/test/java/org/nodejs/xmlcrypto/HMACTest.java +++ /dev/null @@ -1,119 +0,0 @@ -package org.nodejs.xmlcrypto; - -import java.io.File; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import javax.xml.crypto.dsig.CanonicalizationMethod; -import javax.xml.crypto.dsig.DigestMethod; -import javax.xml.crypto.dsig.Reference; -import javax.xml.crypto.dsig.SignatureMethod; -import javax.xml.crypto.dsig.SignedInfo; -import javax.xml.crypto.dsig.Transform; -import javax.xml.crypto.dsig.XMLSignature; -import javax.xml.crypto.dsig.XMLSignatureFactory; -import javax.xml.crypto.dsig.dom.DOMSignContext; -import javax.xml.crypto.dsig.keyinfo.KeyInfo; -import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; -import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; -import org.apache.commons.io.FileUtils; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -public class HMACTest { - - public static final String NamespaceSpecNS = "http://www.w3.org/2000/xmlns/"; - - private static final Logger LOGGER = LoggerFactory - .getLogger(HMACTest.class); - - @Test - public void testCreateHMACSignature() throws Exception { - // generate key - KeyGenerator keyGenerator = KeyGenerator.getInstance("HmacSHA1"); - SecretKey secretKey = keyGenerator.generateKey(); - - // generate DOM document - DocumentBuilderFactory documentBuilderFactory - = DocumentBuilderFactory.newInstance(); - documentBuilderFactory.setNamespaceAware(true); - DocumentBuilder documentBuilder = documentBuilderFactory - .newDocumentBuilder(); - Document document = documentBuilder.newDocument(); - Element rootElement = document.createElementNS("urn:test", - "test:Root"); - rootElement.setAttributeNS(NamespaceSpecNS, - "xmlns:test", "urn:test"); - document.appendChild(rootElement); - - XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory - .getInstance("DOM"); - - // XML Signature references - List references = new LinkedList<>(); - List transforms = new LinkedList<>(); - Transform envTransform = xmlSignatureFactory.newTransform( - CanonicalizationMethod.ENVELOPED, - (C14NMethodParameterSpec) null); - transforms.add(envTransform); - Transform exclTransform = xmlSignatureFactory.newTransform( - CanonicalizationMethod.EXCLUSIVE, - (C14NMethodParameterSpec) null); - transforms.add(exclTransform); - Reference reference = xmlSignatureFactory.newReference("", - xmlSignatureFactory.newDigestMethod(DigestMethod.SHA256, - null), transforms, null, null); - references.add(reference); - - // XML Signature SignedInfo - SignedInfo signedInfo = xmlSignatureFactory.newSignedInfo( - xmlSignatureFactory.newCanonicalizationMethod( - CanonicalizationMethod.EXCLUSIVE, - (C14NMethodParameterSpec) null), - xmlSignatureFactory.newSignatureMethod( - SignatureMethod.HMAC_SHA1, - null), references); - - // XML Signature KeyInfo - KeyInfoFactory keyInfoFactory - = xmlSignatureFactory.getKeyInfoFactory(); - KeyInfo keyInfo = keyInfoFactory.newKeyInfo(Collections - .singletonList(keyInfoFactory.newKeyName("some-key-name"))); - - Element parentElement = document.getDocumentElement(); - DOMSignContext domSignContext = new DOMSignContext( - secretKey, parentElement); - domSignContext.setDefaultNamespacePrefix("ds"); - XMLSignature xmlSignature = xmlSignatureFactory.newXMLSignature( - signedInfo, keyInfo); - xmlSignature.sign(domSignContext); - - File tmpFile = File.createTempFile("xml-signature-hmac-", ".xml"); - LOGGER.debug("XML signature file: {}", tmpFile.getAbsolutePath()); - toFile(document, tmpFile); - - File tmpKeyFile = File.createTempFile("hmac-", ".key"); - FileUtils.writeByteArrayToFile(tmpKeyFile, secretKey.getEncoded()); - LOGGER.debug("key file: {}", tmpKeyFile.getAbsolutePath()); - - } - - private void toFile(Node node, File file) throws Exception { - TransformerFactory transformerFactory = TransformerFactory - .newInstance(); - Transformer transformer = transformerFactory.newTransformer(); - transformer.transform(new DOMSource(node), new StreamResult(file)); - } -} diff --git a/test/validators/XmlCryptoJava/src/test/resources/log4j.xml b/test/validators/XmlCryptoJava/src/test/resources/log4j.xml deleted file mode 100644 index 277b7d09..00000000 --- a/test/validators/XmlCryptoJava/src/test/resources/log4j.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities.sln b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities.sln deleted file mode 100644 index 9f997a1e..00000000 --- a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities.sln +++ /dev/null @@ -1,20 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ValidateSignature", "XmlCryptoUtilities\ValidateSignature.csproj", "{CAA449E3-720D-4CD6-B03B-94991E11FF25}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x86 = Debug|x86 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {CAA449E3-720D-4CD6-B03B-94991E11FF25}.Debug|x86.ActiveCfg = Debug|x86 - {CAA449E3-720D-4CD6-B03B-94991E11FF25}.Debug|x86.Build.0 = Debug|x86 - {CAA449E3-720D-4CD6-B03B-94991E11FF25}.Release|x86.ActiveCfg = Release|x86 - {CAA449E3-720D-4CD6-B03B-94991E11FF25}.Release|x86.Build.0 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities.suo b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities.suo deleted file mode 100644 index 0c402ac6..00000000 Binary files a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities.suo and /dev/null differ diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/Program.cs b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/Program.cs deleted file mode 100644 index 6c4c48e6..00000000 --- a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/Program.cs +++ /dev/null @@ -1,140 +0,0 @@ -// -// This example signs an XML file using an -// envelope signature. It then verifies the -// signed XML. -// -using System; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Security.Cryptography.Xml; -using System.Text; -using System.Xml; -using System.IO; -using System.Reflection; - -public class SignVerifyEnvelope -{ - - public static void Main(String[] args) - { - var exe = Assembly.GetExecutingAssembly().Location; - var folder = Path.GetDirectoryName(exe); - var pfx = Path.Combine(folder, "ClientPrivate.pfx"); - var c = new X509Certificate2(File.ReadAllBytes(pfx), "wse2qs"); - - if (args[0] == "verify") - { - Console.WriteLine("verifying signature..."); - var file = Path.Combine(folder, "SignedExample.xml"); - bool b = VerifyXmlFile(file, (RSA)c.PublicKey.Key); - Console.WriteLine("signature is " + (b ? "valid" : "not valid!")); - if (!b) Environment.Exit(-1); - } - else if (args[0] == "sign") - { - Console.WriteLine("generating signature..."); - var xmlFile = Path.Combine(folder, "Example.xml"); - var sigFile = Path.Combine(folder, "signedExample.xml"); - CreateSomeXml(xmlFile); - SignXmlFile(xmlFile, sigFile, (RSA)c.PrivateKey); - Console.WriteLine("done"); - } - } - - // Sign an XML file and save the signature in a new file. This method does not - // save the public key within the XML file. This file cannot be verified unless - // the verifying code has the key with which it was signed. - public static void SignXmlFile(string FileName, string SignedFileName, RSA Key) - { - // Create a new XML document. - XmlDocument doc = new XmlDocument(); - - // Load the passed XML file using its name. - doc.Load(new XmlTextReader(FileName)); - - // Create a SignedXml object. - SignedXml signedXml = new SignedXml(doc); - - // Add the key to the SignedXml document. - signedXml.SigningKey = Key; - - // Create a reference to be signed. - Reference reference = new Reference(); - reference.Uri = ""; - - // Add an enveloped transformation to the reference. - XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform(); - reference.AddTransform(env); - - // Add the reference to the SignedXml object. - signedXml.AddReference(reference); - - // Compute the signature. - signedXml.ComputeSignature(); - - // Get the XML representation of the signature and save - // it to an XmlElement object. - XmlElement xmlDigitalSignature = signedXml.GetXml(); - - // Append the element to the XML document. - doc.DocumentElement.AppendChild(doc.ImportNode(xmlDigitalSignature, true)); - - if (doc.FirstChild is XmlDeclaration) - { - doc.RemoveChild(doc.FirstChild); - } - - // Save the signed XML document to a file specified - // using the passed string. - XmlTextWriter xmltw = new XmlTextWriter(SignedFileName, new UTF8Encoding(false)); - doc.WriteTo(xmltw); - xmltw.Close(); - } - - // Verify the signature of an XML file against an asymetric - // algorithm and return the result. - public static Boolean VerifyXmlFile(String Name, RSA Key) - { - // Create a new XML document. - XmlDocument xmlDocument = new XmlDocument(); - - // Load the passed XML file into the document. - xmlDocument.Load(Name); - - // Create a new SignedXml object and pass it - // the XML document class. - SignedXml signedXml = new SignedXml(xmlDocument); - - // Find the "Signature" node and create a new - // XmlNodeList object. - XmlNodeList nodeList = xmlDocument.GetElementsByTagName("Signature"); - - // Load the signature node. - signedXml.LoadXml((XmlElement)nodeList[0]); - - // Check the signature and return the result. - return signedXml.CheckSignature(Key); - } - - - // Create example data to sign. - public static void CreateSomeXml(string FileName) - { - // Create a new XmlDocument object. - XmlDocument document = new XmlDocument(); - - // Create a new XmlNode object. - XmlNode node = document.CreateNode(XmlNodeType.Element, "", "MyElement", "samples"); - - // Add some text to the node. - node.InnerText = "Example text to be signed."; - - // Append the node to the document. - document.AppendChild(node); - - // Save the XML document to the file name specified. - XmlTextWriter xmltw = new XmlTextWriter(FileName, new UTF8Encoding(false)); - document.WriteTo(xmltw); - xmltw.Close(); - } -} diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/Properties/AssemblyInfo.cs b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/Properties/AssemblyInfo.cs deleted file mode 100644 index 4f4c9558..00000000 --- a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("XmlCryptoUtilities")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("XmlCryptoUtilities")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2012")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("8305ddd7-a079-45b3-8db1-83728172475e")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/ValidateSignature.csproj b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/ValidateSignature.csproj deleted file mode 100644 index f549d460..00000000 --- a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/ValidateSignature.csproj +++ /dev/null @@ -1,59 +0,0 @@ - - - - Debug - x86 - 8.0.30703 - 2.0 - {CAA449E3-720D-4CD6-B03B-94991E11FF25} - Exe - Properties - XmlCryptoUtilities - XmlCryptoUtilities - v4.0 - Client - 512 - - - x86 - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - x86 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/ClientPrivate.pfx b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/ClientPrivate.pfx deleted file mode 100644 index c0e53156..00000000 Binary files a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/ClientPrivate.pfx and /dev/null differ diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/Example.xml b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/Example.xml deleted file mode 100644 index 93e8344a..00000000 --- a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/Example.xml +++ /dev/null @@ -1 +0,0 @@ -Example text to be signed. \ No newline at end of file diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/XmlCryptoUtilities.exe b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/XmlCryptoUtilities.exe deleted file mode 100644 index 89068c26..00000000 Binary files a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/XmlCryptoUtilities.exe and /dev/null differ diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/XmlCryptoUtilities.pdb b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/XmlCryptoUtilities.pdb deleted file mode 100644 index 584aebb1..00000000 Binary files a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/XmlCryptoUtilities.pdb and /dev/null differ diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/XmlCryptoUtilities.vshost.exe b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/XmlCryptoUtilities.vshost.exe deleted file mode 100644 index bb84a51a..00000000 Binary files a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/XmlCryptoUtilities.vshost.exe and /dev/null differ diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/XmlCryptoUtilities.vshost.exe.manifest b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/XmlCryptoUtilities.vshost.exe.manifest deleted file mode 100644 index 061c9ca9..00000000 --- a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/XmlCryptoUtilities.vshost.exe.manifest +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/signedExample.xml b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/signedExample.xml deleted file mode 100644 index f37ef429..00000000 --- a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/bin/Debug/signedExample.xml +++ /dev/null @@ -1 +0,0 @@ -Harry PotterJoanne KRowlingVXkWV6BNrUKqdmwcxhIgRa86HUI=Y71mpTZ0Ba5qED2TxHLp34tq8Pa9X41M5PuABwPq7r2GaL5Ib28ELkhMtnZuZboO50bxh/9fwK6DPApP92zUuOTZi4htlPcGJRP5kvHznbwzvq7aO5FWAcpUTX+yMrlERIutiJi7mjChX+mIKF5/4DK66JfIlY5vlq61TX2gaJw= \ No newline at end of file diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/obj/x86/Debug/DesignTimeResolveAssemblyReferencesInput.cache b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/obj/x86/Debug/DesignTimeResolveAssemblyReferencesInput.cache deleted file mode 100644 index f1e0ed56..00000000 Binary files a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/obj/x86/Debug/DesignTimeResolveAssemblyReferencesInput.cache and /dev/null differ diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/obj/x86/Debug/ValidateSignature.csproj.FileListAbsolute.txt b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/obj/x86/Debug/ValidateSignature.csproj.FileListAbsolute.txt deleted file mode 100644 index b3460171..00000000 --- a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/obj/x86/Debug/ValidateSignature.csproj.FileListAbsolute.txt +++ /dev/null @@ -1,5 +0,0 @@ -C:\Users\naveh\Documents\features\projects\xml-crypto\test\validators\XmlCryptoUtilities\XmlCryptoUtilities\bin\Debug\XmlCryptoUtilities.exe -C:\Users\naveh\Documents\features\projects\xml-crypto\test\validators\XmlCryptoUtilities\XmlCryptoUtilities\bin\Debug\XmlCryptoUtilities.pdb -C:\Users\naveh\Documents\features\projects\xml-crypto\test\validators\XmlCryptoUtilities\XmlCryptoUtilities\obj\x86\Debug\ResolveAssemblyReference.cache -C:\Users\naveh\Documents\features\projects\xml-crypto\test\validators\XmlCryptoUtilities\XmlCryptoUtilities\obj\x86\Debug\XmlCryptoUtilities.exe -C:\Users\naveh\Documents\features\projects\xml-crypto\test\validators\XmlCryptoUtilities\XmlCryptoUtilities\obj\x86\Debug\XmlCryptoUtilities.pdb diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/obj/x86/Debug/XmlCryptoUtilities.exe b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/obj/x86/Debug/XmlCryptoUtilities.exe deleted file mode 100644 index 89068c26..00000000 Binary files a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/obj/x86/Debug/XmlCryptoUtilities.exe and /dev/null differ diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/obj/x86/Debug/XmlCryptoUtilities.pdb b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/obj/x86/Debug/XmlCryptoUtilities.pdb deleted file mode 100644 index 584aebb1..00000000 Binary files a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/obj/x86/Debug/XmlCryptoUtilities.pdb and /dev/null differ diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/program-repro-misc-validation-and-canon.cs b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/program-repro-misc-validation-and-canon.cs deleted file mode 100644 index f3eca86e..00000000 --- a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/program-repro-misc-validation-and-canon.cs +++ /dev/null @@ -1,278 +0,0 @@ -// -// This example signs an XML file using an -// envelope signature. It then verifies the -// signed XML. -// -using System; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Security.Cryptography.Xml; -using System.Text; -using System.Xml; -using System.IO; -using System.Reflection; - -public class SignVerifyEnvelope -{ - public sealed class RSAPKCS1SHA256SignatureDescription : SignatureDescription - { - public RSAPKCS1SHA256SignatureDescription() - { - base.KeyAlgorithm = typeof(RSACryptoServiceProvider).FullName; - base.DigestAlgorithm = typeof(SHA256Managed).FullName; - base.FormatterAlgorithm = typeof(RSAPKCS1SignatureFormatter).FullName; - base.DeformatterAlgorithm = typeof(RSAPKCS1SignatureDeformatter).FullName; - } - - public override AsymmetricSignatureDeformatter CreateDeformatter(AsymmetricAlgorithm key) - { - if (key == null) - { - throw new ArgumentNullException("key"); - } - - RSAPKCS1SignatureDeformatter deformatter = new RSAPKCS1SignatureDeformatter(key); - deformatter.SetHashAlgorithm("SHA256"); - return deformatter; - } - - public override AsymmetricSignatureFormatter CreateFormatter(AsymmetricAlgorithm key) - { - if (key == null) - { - throw new ArgumentNullException("key"); - } - - RSAPKCS1SignatureFormatter formatter = new RSAPKCS1SignatureFormatter(key); - formatter.SetHashAlgorithm("SHA256"); - return formatter; - } - - } - - - static bool ValidateXml(XmlDocument receipt, X509Certificate2 certificate) - { - // Create the signed XML object. - SignedXml sxml = new SignedXml(receipt); - - // Get the XML Signature node and load it into the signed XML object. - XmlNode dsig = receipt.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl)[0]; - if (dsig == null) - { - // If signature is not found return false - System.Console.WriteLine("Signature not found."); - return false; - } - - sxml.LoadXml((XmlElement)dsig); - - // Check the signature - bool isValid = sxml.CheckSignature(certificate, true); - - - FieldInfo field = sxml.GetType().GetField("m_signature", - BindingFlags.NonPublic | - BindingFlags.Instance); - - var sig = (Signature)field.GetValue(sxml); - var _ref = (Reference)sig.SignedInfo.References[0]; - - //var pre = Type.GetType("System.Security.Cryptography.Xml.Utils").GetMethod("PreProcessDocumentInput"); - //pre.Invoke(null, new[] { }); - - - var enveloped = (XmlDsigEnvelopedSignatureTransform)_ref.TransformChain[0]; - - enveloped.LoadInput(receipt); - var outputstream = enveloped.GetOutput(); - - var securityUrl = receipt.BaseURI; - var resolver = new XmlSecureResolver(new XmlUrlResolver(), securityUrl); - //TransformToOctetStream(Stream input, XmlResolver resolver, string baseUri) - MethodInfo trans = _ref.TransformChain.GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)[2]; - - var stream = trans.Invoke(_ref.TransformChain, new object[] {receipt, resolver, securityUrl}); - - - var canontype = sig.GetType().Assembly.GetType("System.Security.Cryptography.Xml.CanonicalXml"); - var foo = Activator.CreateInstance(canontype, BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] {receipt, resolver}, null); - - - - - - - MethodInfo method = _ref.GetType().GetMethod("CalculateHashValue", - BindingFlags.NonPublic | - BindingFlags.Instance); - - FieldInfo refs = sig.GetType().GetField("m_referencedItems", - BindingFlags.NonPublic | - BindingFlags.Instance); - var refs1 = refs.GetValue(sig); - - var res = method.Invoke(_ref, new [] {receipt, refs1}); - var str = Convert.ToBase64String((byte[])res); - - return isValid; - } - - - public static void Main(String[] args) - { - - - //calculate caninicalized xml - - var t = new XmlDsigEnvelopedSignatureTransform(false); - XmlDocument doc = new XmlDocument(); - //doc.PreserveWhitespace = true; - doc.Load(@"c:\temp\x.xml"); - t.LoadInput(doc); - - - FieldInfo field = t.GetType().GetField("_signaturePosition", - BindingFlags.NonPublic | - BindingFlags.Instance); - - - field.SetValue(t, 1); - - var res = (XmlDocument)t.GetOutput(); - var s = res.OuterXml; - - var c14 = new XmlDsigC14NTransform(); - c14.LoadInput(res); - var mem = (MemoryStream)c14.GetOutput(); - - var sha = new SHA256Managed(); - - var byte1 = c14.GetDigestedOutput(new SHA256Managed()); - var digest1 = Convert.ToBase64String(byte1); - var byte2 = sha.ComputeHash(mem.ToArray()); - var digest2 = Convert.ToBase64String(byte2); - - - var s1 = System.Text.Encoding.UTF8.GetString(mem.ToArray()); - var byte3 = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(s1)); - var digest3 = Convert.ToBase64String(byte3); - - //return; - - - - //validate signature - - CryptoConfig.AddAlgorithm(typeof(RSAPKCS1SHA256SignatureDescription), "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"); - XmlDocument xmlDoc = new XmlDocument(); - xmlDoc.Load(@"c:\temp\x.xml"); - XmlNode node = xmlDoc.DocumentElement; - X509Certificate2 cert = new X509Certificate2(File.ReadAllBytes(@"c:\temp\x.cer")); - bool isValid = ValidateXml(xmlDoc, cert); - //return; - - - //calc hash - var sha1 = new SHA256Managed(); - var b1 = sha1.ComputeHash(System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(@"c:\temp\x_no_sig.xml"))); - var b64 = Convert.ToBase64String(b1); - } - - // Sign an XML file and save the signature in a new file. This method does not - // save the public key within the XML file. This file cannot be verified unless - // the verifying code has the key with which it was signed. - public static void SignXmlFile(string FileName, string SignedFileName, RSA Key) - { - // Create a new XML document. - XmlDocument doc = new XmlDocument(); - - // Load the passed XML file using its name. - doc.Load(new XmlTextReader(FileName)); - - // Create a SignedXml object. - SignedXml signedXml = new SignedXml(doc); - - // Add the key to the SignedXml document. - signedXml.SigningKey = Key; - - // Create a reference to be signed. - Reference reference = new Reference(); - reference.Uri = ""; - - // Add an enveloped transformation to the reference. - XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform(); - reference.AddTransform(env); - - // Add the reference to the SignedXml object. - signedXml.AddReference(reference); - - // Compute the signature. - signedXml.ComputeSignature(); - - // Get the XML representation of the signature and save - // it to an XmlElement object. - XmlElement xmlDigitalSignature = signedXml.GetXml(); - - // Append the element to the XML document. - doc.DocumentElement.AppendChild(doc.ImportNode(xmlDigitalSignature, true)); - - if (doc.FirstChild is XmlDeclaration) - { - doc.RemoveChild(doc.FirstChild); - } - - // Save the signed XML document to a file specified - // using the passed string. - XmlTextWriter xmltw = new XmlTextWriter(SignedFileName, new UTF8Encoding(false)); - doc.WriteTo(xmltw); - xmltw.Close(); - } - - // Verify the signature of an XML file against an asymetric - // algorithm and return the result. - public static Boolean VerifyXmlFile(String Name, RSA Key) - { - // Create a new XML document. - XmlDocument xmlDocument = new XmlDocument(); - - // Load the passed XML file into the document. - xmlDocument.Load(Name); - - // Create a new SignedXml object and pass it - // the XML document class. - SignedXml signedXml = new SignedXml(xmlDocument); - - // Find the "Signature" node and create a new - // XmlNodeList object. - XmlNodeList nodeList = xmlDocument.GetElementsByTagName("Signature"); - - // Load the signature node. - signedXml.LoadXml((XmlElement)nodeList[0]); - - // Check the signature and return the result. - return signedXml.CheckSignature(Key); - } - - - // Create example data to sign. - public static void CreateSomeXml(string FileName) - { - // Create a new XmlDocument object. - XmlDocument document = new XmlDocument(); - - // Create a new XmlNode object. - XmlNode node = document.CreateNode(XmlNodeType.Element, "", "MyElement", "samples"); - - // Add some text to the node. - node.InnerText = "Example text to be signed."; - - // Append the node to the document. - document.AppendChild(node); - - // Save the XML document to the file name specified. - XmlTextWriter xmltw = new XmlTextWriter(FileName, new UTF8Encoding(false)); - document.WriteTo(xmltw); - xmltw.Close(); - } -} diff --git a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/utilities.cs b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/utilities.cs deleted file mode 100644 index 5ed16f80..00000000 --- a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/utilities.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Security.Cryptography.Xml; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Xml; -using System.IO; - -namespace ConsoleApplication31 -{ - /* - class Program - { - static void Main(string[] args) - { - GetCanonization(); - //GetSignature(); - - } - - static void GetSignature() - { - XmlDocument doc = new XmlDocument(); - //doc.LoadXml(""); - doc.LoadXml(""); - SignedXml signedXml = new SignedXml(doc); - - var c = new X509Certificate2( - File.ReadAllBytes(@"C:\Program Files\Microsoft WSE\v2.0\Samples\Sample Test Certificates\Client Private.pfx"), "wse2qs"); - - signedXml.SigningKey = c.PrivateKey; - signedXml.Signature.SignedInfo.CanonicalizationMethod = "http://www.w3.org/2001/10/xml-exc-c14n#"; - - Reference ref0 = new Reference(); - ref0.Uri = "#_0"; - Reference ref1 = new Reference(); - ref1.Uri = "#_1"; - Reference ref2 = new Reference(); - ref2.Uri = "#_2"; - - var t = new XmlDsigExcC14NTransform(); - ref0.AddTransform(t); - ref1.AddTransform(t); - ref2.AddTransform(t); - - signedXml.AddReference(ref0); - signedXml.AddReference(ref1); - signedXml.AddReference(ref2); - - signedXml.ComputeSignature(); - var xmlDigitalSignature = signedXml.GetXml(); - var s = xmlDigitalSignature.OuterXml; - } - - static void GetCanonization() - { - var c = new XmlDsigExcC14NTransform(true, ""); - var doc = new XmlDocument(); - doc.PreserveWhitespace = true; - doc.LoadXml(""); - var node = doc.SelectSingleNode("//*[local-name(.)='x']"); - var nodes = node.SelectNodes(".|.//*|.//text()|.//@*"); - c.LoadInput(nodes); - - var h = new SHA1CryptoServiceProvider(); - var b = c.GetDigestedOutput(h); - var b64 = Convert.ToBase64String(b); - - var res = c.GetOutput() as MemoryStream; - string s2 = System.Text.Encoding.UTF8.GetString(res.ToArray()); - } - }*/ -} diff --git a/test/xmlns-attributes.spec.ts b/test/xmlns-attributes.spec.ts new file mode 100644 index 00000000..bf69e027 --- /dev/null +++ b/test/xmlns-attributes.spec.ts @@ -0,0 +1,55 @@ +import * as fs from "fs"; +import * as xmldom from "@xmldom/xmldom"; +import { expect } from "chai"; +import { findAncestorNs, SignedXml } from "../src/index"; + +const inclusiveC14n = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"; +const exclusiveC14n = "http://www.w3.org/2001/10/xml-exc-c14n#"; + +function verify(xml: string) { + const verifier = new SignedXml({ + publicCert: fs.readFileSync("./test/static/client_public.pem"), + }); + verifier.loadSignature(verifier.findSignatures(new xmldom.DOMParser().parseFromString(xml))[0]); + return { + valid: verifier.checkSignature(xml), + signedReferences: verifier.getSignedReferences(), + }; +} + +describe("attributes whose names begin with xmlns", function () { + it("does not treat an ordinary ancestor attribute as a namespace declaration", function () { + const doc = new xmldom.DOMParser().parseFromString( + '', + ); + + expect(findAncestorNs(doc, "/root/item")).to.deep.equal([ + { prefix: "p", namespaceURI: "urn:test" }, + ]); + }); + + for (const transform of [inclusiveC14n, exclusiveC14n]) { + it(`rejects changes to an ordinary attribute under ${transform}`, function () { + const signer = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: exclusiveC14n, + signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + }); + signer.addReference({ + xpath: "//*[local-name(.)='item']", + transforms: [transform], + digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + }); + signer.computeSignature(''); + + const signedXml = signer.getSignedXml(); + expect(verify(signedXml).valid).to.be.true; + + const tamperedXml = signedXml.replace('xmlnsRole="user"', 'xmlnsRole="admin"'); + expect(tamperedXml).not.to.equal(signedXml); + const result = verify(tamperedXml); + expect(result.valid).to.be.false; + expect(result.signedReferences).to.be.empty; + }); + } +});