diff --git a/README.md b/README.md index 9e25f6f1..3e8eefb7 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,30 @@ The error for a value the parser cannot read is `Invalid PEM format.`, in place as that file rather than base64-encoded, which is what made it a message with a body of base64-encoded PEM. Both forms of PEM the parser does read produce the same canonical output. +### Input that 7.0 rejects + +Each of these works in 6.x, prints a `DeprecationWarning`, and is rejected in 7.0. + +When verifying: + +- a `Reference` whose `URI` is neither empty nor `#` followed by an ID. It is resolved as a + same-document ID all the same, though only those two forms are + [same-document references](https://www.w3.org/TR/xmldsig-core1/#sec-ReferenceProcessingModel). +- a `Transform` with no `Algorithm` attribute, which is skipped. The + [schema](https://www.w3.org/TR/xmldsig-core1/#sec-Transforms) requires the attribute. +- an `InclusiveNamespaces` element outside the `http://www.w3.org/2001/10/xml-exc-c14n#` + [namespace](https://www.w3.org/TR/xml-exc-c14n/#sec-Use), whose `PrefixList` is applied all the + same. xml-crypto 6.3.2 and earlier wrote one when signing with `xml-exc-c14n#WithComments` and a + prefix list, so documents signed that way need signing again. + +When signing: + +- a `location.reference` that selects a text node, a comment or a processing instruction, which + `before` and `after` place the `Signature` next to. +- `isEmptyUri: true` on a reference whose `xpath` selects anything but the document element. + `URI=""` identifies [the whole document](https://www.w3.org/TR/xmldsig-core1/#sec-Same-Document), + so the signature this produces does not verify. + ### Deprecated ahead of 7.0 These exports are deprecated and will be removed in 7.0: diff --git a/src/exclusive-canonicalization.ts b/src/exclusive-canonicalization.ts index 7b61bbf6..251bff97 100644 --- a/src/exclusive-canonicalization.ts +++ b/src/exclusive-canonicalization.ts @@ -5,6 +5,20 @@ import type { } from "./types"; import * as utils from "./utils"; import * as isDomNode from "@xmldom/is-dom-node"; +import { deprecate } from "util"; + +const warnInclusiveNamespacesNamespace = deprecate( + () => {}, + "Reading an `InclusiveNamespaces` element that is not in the `http://www.w3.org/2001/10/xml-exc-c14n#` namespace is deprecated, and a signature that has one will not verify in version 7.0. xml-crypto 6.3.2 and earlier wrote one when signing with `xml-exc-c14n#WithComments` and a prefix list, so sign those documents again.", + "XML_CRYPTO_INCLUSIVE_NAMESPACES_NAMESPACE", +); + +// https://www.w3.org/TR/xml-exc-c14n/#sec-Use +export function warnUnlessExclusiveC14nNamespace(inclusiveNamespaces: Element): void { + if (inclusiveNamespaces.namespaceURI !== "http://www.w3.org/2001/10/xml-exc-c14n#") { + warnInclusiveNamespacesNamespace(); + } +} export class ExclusiveCanonicalization implements CanonicalizationOrTransformationAlgorithm { protected includeComments = false; @@ -320,6 +334,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati "InclusiveNamespaces", ); if (inclusiveNamespaces.length !== 0) { + warnUnlessExclusiveC14nNamespace(inclusiveNamespaces[0]); inclusiveNamespacesPrefixList = ( inclusiveNamespaces[0].getAttribute("PrefixList") || "" ).split(" "); diff --git a/src/signed-xml.ts b/src/signed-xml.ts index d181c6b8..bbad4e61 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -64,6 +64,30 @@ function decodeSignatureValue(signature: Node): Buffer { return Buffer.from(findSignatureValue(signature) ?? "", "base64"); } +const warnReferenceUriWithoutFragment = deprecate( + () => {}, + 'Resolving a `Reference` whose `URI` is neither empty nor starts with "#" as a same-document ID is deprecated, and such a signature will not verify in version 7.0. A same-document `URI` is empty, or "#" followed by an ID.', + "XML_CRYPTO_REFERENCE_URI_WITHOUT_FRAGMENT", +); + +const warnTransformWithoutAlgorithm = deprecate( + () => {}, + "Skipping a `Transform` that has no `Algorithm` attribute is deprecated, and a signature that has one will not verify in version 7.0.", + "XML_CRYPTO_TRANSFORM_WITHOUT_ALGORITHM", +); + +const warnLocationReferenceNotElement = deprecate( + () => {}, + "A `location.reference` that selects a node other than an element is deprecated and will throw in version 7.0. Select the element to place the signature in or next to.", + "XML_CRYPTO_LOCATION_REFERENCE_NOT_ELEMENT", +); + +const warnEmptyUriNotDocumentElement = deprecate( + () => {}, + '`isEmptyUri: true` on a reference whose `xpath` selects anything but the document element is deprecated and will throw in version 7.0. `URI=""` identifies the whole document, so the signature this produces does not verify.', + "XML_CRYPTO_EMPTY_URI_NOT_DOCUMENT_ELEMENT", +); + function certificatesToPublish(publicCert: string): string[] { const certificates = utils.pemCertificates(publicCert); if (certificates.length > 0) { @@ -619,6 +643,12 @@ export class SignedXml { const uri = ref.uri?.[0] === "#" ? ref.uri.substring(1) : ref.uri; let elem: xpath.SelectSingleReturnType = null; + // Only an empty URI and a fragment are same-document references: + // https://www.w3.org/TR/xmldsig-core1/#sec-ReferenceProcessingModel + if (uri !== "" && uri === ref.uri) { + warnReferenceUriWithoutFragment(); + } + if (uri === "") { elem = utils.hasDocumentLevelProcessingInstruction(doc) ? doc : xpath.select1("//*", doc); } else if (uri?.indexOf("'") !== -1) { @@ -817,16 +847,19 @@ export class SignedXml { if (transformAttr) { transforms.push(transformAttr.value); + const inclusiveNamespaces = utils.findChildren(transform, "InclusiveNamespaces"); + inclusiveNamespaces.forEach(execC14n.warnUnlessExclusiveC14nNamespace); // PrefixList is NMTOKENS, delimited by XML white space: // https://www.w3.org/TR/xml-exc-c14n/#sec-Use prefixLists.push( - utils - .findChildren(transform, "InclusiveNamespaces") + inclusiveNamespaces .flatMap((namespace) => (namespace.getAttribute("PrefixList") ?? "").split(/[ \t\r\n]+/), ) .filter((value) => value.length > 0), ); + } else { + warnTransformWithoutAlgorithm(); } } } @@ -1105,6 +1138,10 @@ export class SignedXml { } } + if (!isDomNode.isElementNode(referenceNode)) { + warnLocationReferenceNotElement(); + } + const previousSignatureNode = this.signatureNode; this.signatureNode = signatureElem; try { @@ -1277,6 +1314,9 @@ export class SignedXml { let targetUri: string; if (ref.isEmptyUri) { targetUri = ""; + if (node !== node.ownerDocument.documentElement) { + warnEmptyUriNotDocumentElement(); + } } else { const id = this.ensureHasId(node); ref.uri = id;