From 5200d71457bab9a14c98ee596e3fe13ed6bcae4a Mon Sep 17 00:00:00 2001 From: shunkica Date: Sat, 25 Oct 2025 10:46:20 +0200 Subject: [PATCH 01/12] feat: add XmlDSigVerifier wrapper for SignedXml Types: (old types are re-exported and deprecated - remove deprecated types in next breaking change) - separate CanonicalizationOrTransformationAlgorithm into CanonicalizationAlgorithm and TransformAlgorithm - separate CanonicalizationOrTransformationAlgorithmProcessOptions into CanonicalizationAlgorithmOptions and TransformAlgorithmOptions - separate CanonicalizationOrTransformAlgorithmType into CanonicalizationAlgorithmURI and TransformAlgorithmURI - rename CanonicalizationAlgorithmType into CanonicalizationAlgorithmURI - rename SignatureAlgorithmType into SignatureAlgorithmURI - rename SignatureAlgorithmType into SignatureAlgorithmURI - rename HashAlgorithmType into HashAlgorithmURI - introduce Record maps: CanonicalizationAlgorithmMap, HashAlgorithmMap, SignatureAlgorithmMap, TransformAlgorithmMap - introduce VerificationIdAttributeType, SignatureIdAttributeType, IdAttributeType - introduce KeySelectorFunction type - introduce XmlDsigVerifier specific types: CertificateKeySelector, KeyInfoKeySelector, SharedSecretKeySelector, KeySelector, XmlDSigVerifierOptionsBase, XmlDSigVerifierSecurityOptions, KeyInfoXmlDSigSecurityOptions, KeyInfoXmlDSigVerifierOptions, PublicCertXmlDSigVerifierOptions, SharedSecretXmlDSigVerifierOptions, XmlDSigVerifierOptions, SuccessfulXmlDsigVerificationResult, FailedXmlDsigVerificationResult, XmlDsigVerificationResult Constants: - replace string literal URIs with constants from xmldsig-uris.ts (see XMLDSIG_URIS) SignedXml: - add maxTransforms option - set maximum number of allowed transforms on a reference (throws if limit is exceeded) - add idAttributes option - to override default id attributes - can use use fully qualified idAttributes (see IdAttributeType) - use first idAttribute in idAttributes array when generating new id attribute during signature - if "null" is set as the namespaceURI of an id attribute, it will only look for attributes without a namespace - introduce getDefaultCanonicalizationAlgorithms(), getDefaultHashAlgorithms(), getDefaultAsymmetricSignatureAlgorithms(), getDefaultSymmetricSignatureAlgorithms(), getDefaultTransformAlgorithms(), getDefaultIdAttributes() - add allowedSignatureAlgorithms option - overrides default signature algorithms - add allowedHashAlgorithms option - overrides default hash algorithms - add allowedCanonicalizationAlgorithms option - overrides default canonicalization algorithms - add allowedTransformAlgorithms option - overrides default transform algorithms - split findCanonicalizationAlgorithm into findCanonicalizationAlgorithm and findTransformAlgorithm ( change implementation of findTransformAlgorithm in next breaking change ) XmlDSigVerifier: - introduce the XmlDSigVerifier wrapper for SignedXml, a safer and simpler way to verify XML signatures Docs: - create the XMLDSIG_VERIFIER.md file with detailed instructions on using the XmlDSigVerifier Tests: - introduce the xmldsig-verifier.spec.ts tests for the XmlDSigVerifier Development: - introduce default development node version via .nvmrc Dependencies: - update xpath from 0.0.33 to 0.0.34 - the previous version of xpath had a bug where namespace-uri(.)='' would not find a non-namespaced attribute, instead it would find it using namespace-uri(.)='undefined' (the literal string undefined) --- .nvmrc | 1 + README.md | 25 + XMLDSIG_VERIFIER.md | 157 +++ package-lock.json | 15 +- package.json | 2 +- src/c14n-canonicalization.ts | 19 +- src/enveloped-signature.ts | 18 +- src/exclusive-canonicalization.ts | 20 +- src/hash-algorithms.ts | 7 +- src/index.ts | 2 + src/signature-algorithms.ts | 11 +- src/signed-xml.ts | 318 ++++-- src/types.ts | 410 +++++-- src/utils.ts | 21 +- src/xmldsig-uris.ts | 60 ++ src/xmldsig-verifier.ts | 312 ++++++ test/c14n-non-exclusive-unit-tests.spec.ts | 2 +- test/c14nWithComments-unit-tests.spec.ts | 12 +- test/canonicalization-unit-tests.spec.ts | 17 +- test/document-tests.spec.ts | 6 +- test/hmac-tests.spec.ts | 16 +- test/key-info-tests.spec.ts | 14 +- test/saml-response-tests.spec.ts | 24 +- test/signature-integration-tests.spec.ts | 30 +- test/signature-object-tests.spec.ts | 152 ++- test/signature-unit-tests.spec.ts | 233 ++-- test/static/chain_client.crt.pem | 21 + test/static/chain_client.key.pem | 28 + test/static/chain_root.crt.pem | 21 + test/static/chain_root.crt.srl | 1 + test/static/chain_root.key.pem | 28 + test/static/expired_certificate.crt.pem | 21 + test/static/expired_certificate.key.pem | 28 + test/static/future_certificate.crt.pem | 18 + test/static/future_certificate.csr.pem | 15 + test/static/future_certificate.key.pem | 28 + test/utils-tests.spec.ts | 56 + ...program-repro-misc-validation-and-canon.cs | 50 +- test/wsfed-metadata-tests.spec.ts | 4 +- test/xmldsig-verifier.spec.ts | 999 ++++++++++++++++++ 40 files changed, 2705 insertions(+), 517 deletions(-) create mode 100644 .nvmrc create mode 100644 XMLDSIG_VERIFIER.md create mode 100644 src/xmldsig-uris.ts create mode 100644 src/xmldsig-verifier.ts create mode 100644 test/static/chain_client.crt.pem create mode 100644 test/static/chain_client.key.pem create mode 100644 test/static/chain_root.crt.pem create mode 100644 test/static/chain_root.crt.srl create mode 100644 test/static/chain_root.key.pem create mode 100644 test/static/expired_certificate.crt.pem create mode 100644 test/static/expired_certificate.key.pem create mode 100644 test/static/future_certificate.crt.pem create mode 100644 test/static/future_certificate.csr.pem create mode 100644 test/static/future_certificate.key.pem create mode 100644 test/xmldsig-verifier.spec.ts diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..aebd91c5 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v22.16.0 diff --git a/README.md b/README.md index dfe164f2..5d6f77cb 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,31 @@ Note: The xml-crypto api requires you to supply it separately the xml signature ("<Signature>...</Signature>", in loadSignature) and the signed xml (in checkSignature). The signed xml may or may not contain the signature in it, but you are still required to supply the signature separately. +### Secure Verification with XmlDSigVerifier (Recommended) + +For a more secure and streamlined verification experience, use the `XmlDSigVerifier` class. It provides built-in checks for certificate expiration, truststore validation, and easier configuration. + +```javascript +const { XmlDSigVerifier } = require("xml-crypto"); +const fs = require("fs"); + +const xml = fs.readFileSync("signed.xml", "utf-8"); +const publicCert = fs.readFileSync("client_public.pem", "utf-8"); + +const result = XmlDSigVerifier.verifySignature(xml, { + keySelector: { publicCert: publicCert }, +}); + +if (result.success) { + console.log("Valid signature!"); + console.log("Signed content:", result.signedReferences); +} else { + console.error("Invalid signature:", result.error); +} +``` + +For detailed usage instructions, see [XMLDSIG_VERIFIER.md](./XMLDSIG_VERIFIER.md). + ### Caring for Implicit transform If you fail to verify signed XML, then one possible cause is that there are some hidden implicit transforms(#). diff --git a/XMLDSIG_VERIFIER.md b/XMLDSIG_VERIFIER.md new file mode 100644 index 00000000..14fb938d --- /dev/null +++ b/XMLDSIG_VERIFIER.md @@ -0,0 +1,157 @@ +# XmlDSigVerifier Usage Guide + +`XmlDSigVerifier` provides a focused, secure, and easy-to-use API for verifying XML signatures. It is designed to replace direct usage of `SignedXml` for verification scenarios, offering built-in security checks and a simplified configuration. + +## Features + +- **Type-Safe Configuration:** Explicit options for different key retrieval strategies (Public Certificate, KeyInfo, Shared Secret). +- **Enhanced Security:** Built-in checks for certificate expiration, truststore validation, and limits on transform complexity. +- **Flexible Error Handling:** Choose between throwing errors or returning a result object. + +## Installation + +Ensure you have `xml-crypto` installed: + +```bash +npm install xml-crypto +``` + +## Quick Start + +### 1. Verifying with a Public Certificate + +If you already have the public certificate or key and want to verify a document signed with the corresponding private key: + +```typescript +import { XmlDSigVerifier } from "xml-crypto"; +import * as fs from "fs"; + +const xml = fs.readFileSync("signed_document.xml", "utf-8"); +const publicCert = fs.readFileSync("public_key.pem", "utf-8"); + +const result = XmlDSigVerifier.verifySignature(xml, { + keySelector: { publicCert: publicCert }, +}); + +if (result.success) { + console.log("Signature valid!"); + // Access the signed content securely + console.log("Signed references:", result.signedReferences); +} else { + console.error("Verification failed:", result.error); +} +``` + +### 2. Verifying using KeyInfo (with Truststore) + +When the XML document contains the certificate in a `` element, you can verify it while ensuring the certificate is trusted and valid. + +```typescript +import { XmlDSigVerifier, SignedXml } from "xml-crypto"; +import * as fs from "fs"; + +const xml = fs.readFileSync("signed_with_keyinfo.xml", "utf-8"); +const trustedRootCert = fs.readFileSync("root_ca.pem", "utf-8"); + +const result = XmlDSigVerifier.verifySignature(xml, { + keySelector: { + // Extract the certificate from KeyInfo + getCertFromKeyInfo: (keyInfo) => SignedXml.getCertFromKeyInfo(keyInfo), + }, + security: { + // Ensure the certificate is trusted by your root CA + truststore: [trustedRootCert], + // Automatically check if the certificate is expired + checkCertExpiration: true, + }, +}); + +if (result.success) { + console.log("Signature is valid and trusted."); +} else { + console.log("Verification failed:", result.error); +} +``` + +## Advanced Usage + +### Reusing the Verifier Instance + +For better performance when verifying multiple documents with the same configuration, create an instance of `XmlDSigVerifier`. + +```typescript +const verifier = new XmlDSigVerifier({ + keySelector: { publicCert: myPublicCert }, + // Global security options + security: { maxTransforms: 2 }, +}); + +const result1 = verifier.verifySignature(xml1); +const result2 = verifier.verifySignature(xml2); +``` + +### Verification Options + +The `verifySignature` method accepts an options object with the following structure: + +```typescript +interface XmlDSigVerifierOptions { + // STRATEGY: Choose one of the following key selectors + keySelector: + | { publicCert: string | Buffer } // Direct public key/cert + | { getCertFromKeyInfo: (node) => string | null } // Extract from XML + | { sharedSecretKey: string | Buffer }; // HMAC + + // CONFIGURATION + idAttributes?: string[]; // e.g., ['Id', 'ID'] + throwOnError?: boolean; // Default: false (returns result object) + + // SECURITY + security?: { + maxTransforms?: number; // Limit transforms (DoS protection) + checkCertExpiration?: boolean; // Check NotBefore/NotAfter (KeyInfo only) + truststore?: (string | Buffer)[]; // List of trusted CAs (KeyInfo only) + + // Algorithm allow-lists + signatureAlgorithms?: Record; + hashAlgorithms?: Record; + // ... + }; +} +``` + +### Error Handling + +By default, `verifySignature` returns a result object. If you prefer to handle exceptions: + +```typescript +try { + const result = XmlDSigVerifier.verifySignature(xml, { + keySelector: { publicCert }, + throwOnError: true, // Will throw Error on failure + }); + // If code reaches here, signature is valid +} catch (e) { + console.error("Signature invalid:", e.message); +} +``` + +### Handling Multiple Signatures + +If a document contains multiple signatures, you must specify which one to verify by passing the signature node. + +```typescript +import { DOMParser } from "@xmldom/xmldom"; + +const doc = new DOMParser().parseFromString(xml, "application/xml"); +const signatures = doc.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature"); + +// Verify the second signature +const result = XmlDSigVerifier.verifySignature( + xml, + { + keySelector: { publicCert }, + }, + signatures[1], +); +``` diff --git a/package-lock.json b/package-lock.json index dc9511ea..b96d2f40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@xmldom/is-dom-node": "^1.0.1", "@xmldom/xmldom": "^0.8.10", - "xpath": "^0.0.33" + "xpath": "^0.0.34" }, "devDependencies": { "@cjbarth/github-release-notes": "^4.2.0", @@ -11882,9 +11882,10 @@ } }, "node_modules/xpath": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.33.tgz", - "integrity": "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==", + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", + "integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==", + "license": "MIT", "engines": { "node": ">=0.6.0" } @@ -20474,9 +20475,9 @@ "dev": true }, "xpath": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.33.tgz", - "integrity": "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==" + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", + "integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==" }, "y18n": { "version": "5.0.8", diff --git a/package.json b/package.json index 8bd4caa0..1383f143 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "dependencies": { "@xmldom/is-dom-node": "^1.0.1", "@xmldom/xmldom": "^0.8.10", - "xpath": "^0.0.33" + "xpath": "^0.0.34" }, "devDependencies": { "@cjbarth/github-release-notes": "^4.2.0", diff --git a/src/c14n-canonicalization.ts b/src/c14n-canonicalization.ts index dd9d7788..a037042d 100644 --- a/src/c14n-canonicalization.ts +++ b/src/c14n-canonicalization.ts @@ -1,13 +1,15 @@ import type { - CanonicalizationOrTransformationAlgorithm, - CanonicalizationOrTransformationAlgorithmProcessOptions, + CanonicalizationAlgorithmURI, + CanonicalizationAlgorithm, + TransformAlgorithmOptions, NamespacePrefix, RenderedNamespace, } from "./types"; import * as utils from "./utils"; import * as isDomNode from "@xmldom/is-dom-node"; +import { XMLDSIG_URIS } from "./xmldsig-uris"; -export class C14nCanonicalization implements CanonicalizationOrTransformationAlgorithm { +export class C14nCanonicalization implements CanonicalizationAlgorithm { protected includeComments = false; constructor() { @@ -252,9 +254,10 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg * Perform canonicalization of the given node * * @param node + * @param options * @api public */ - process(node: Node, options: CanonicalizationOrTransformationAlgorithmProcessOptions): string { + process(node: Node, options: TransformAlgorithmOptions): string { options = options || {}; const defaultNs = options.defaultNs || ""; const defaultNsForPrefix = options.defaultNsForPrefix || {}; @@ -275,8 +278,8 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg return res; } - getAlgorithmName() { - return "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"; + getAlgorithmName(): CanonicalizationAlgorithmURI { + return XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.C14N; } } @@ -289,7 +292,7 @@ export class C14nCanonicalizationWithComments extends C14nCanonicalization { this.includeComments = true; } - getAlgorithmName() { - return "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"; + getAlgorithmName(): CanonicalizationAlgorithmURI { + return XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.C14N_WITH_COMMENTS; } } diff --git a/src/enveloped-signature.ts b/src/enveloped-signature.ts index 5c74c362..389a0627 100644 --- a/src/enveloped-signature.ts +++ b/src/enveloped-signature.ts @@ -1,23 +1,23 @@ import * as xpath from "xpath"; import * as isDomNode from "@xmldom/is-dom-node"; - +import { XMLDSIG_URIS } from "./xmldsig-uris"; import type { - CanonicalizationOrTransformationAlgorithm, - CanonicalizationOrTransformationAlgorithmProcessOptions, + TransformAlgorithmOptions, CanonicalizationOrTransformAlgorithmType, + TransformAlgorithm, } from "./types"; -export class EnvelopedSignature implements CanonicalizationOrTransformationAlgorithm { +export class EnvelopedSignature implements TransformAlgorithm { protected includeComments = false; constructor() { this.includeComments = false; } - process(node: Node, options: CanonicalizationOrTransformationAlgorithmProcessOptions): Node { + process(node: Node, options: TransformAlgorithmOptions): Node { if (null == options.signatureNode) { const signature = xpath.select1( - "./*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `./*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, node, ); if (isDomNode.isNodeLike(signature) && signature.parentNode) { @@ -34,7 +34,7 @@ export class EnvelopedSignature implements CanonicalizationOrTransformationAlgor const expectedSignatureValueData = expectedSignatureValue.data; const signatures = xpath.select( - ".//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `.//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, node, ); for (const nodeSignature of Array.isArray(signatures) ? signatures : []) { @@ -55,7 +55,9 @@ export class EnvelopedSignature implements CanonicalizationOrTransformationAlgor return node; } + // eslint-disable-next-line deprecation/deprecation getAlgorithmName(): CanonicalizationOrTransformAlgorithmType { - return "http://www.w3.org/2000/09/xmldsig#enveloped-signature"; + // TODO: replace with TransformAlgorithmURI in next breaking change + return XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE; } } diff --git a/src/exclusive-canonicalization.ts b/src/exclusive-canonicalization.ts index ea88aa2c..12ee4565 100644 --- a/src/exclusive-canonicalization.ts +++ b/src/exclusive-canonicalization.ts @@ -1,10 +1,12 @@ import type { - CanonicalizationOrTransformationAlgorithm, - CanonicalizationOrTransformationAlgorithmProcessOptions, + CanonicalizationAlgorithmURI, + CanonicalizationAlgorithm, + TransformAlgorithmOptions, NamespacePrefix, } from "./types"; import * as utils from "./utils"; import * as isDomNode from "@xmldom/is-dom-node"; +import { XMLDSIG_URIS } from "./xmldsig-uris"; function isPrefixInScope(prefixesInScope, prefix, namespaceURI) { let ret = false; @@ -17,7 +19,7 @@ function isPrefixInScope(prefixesInScope, prefix, namespaceURI) { return ret; } -export class ExclusiveCanonicalization implements CanonicalizationOrTransformationAlgorithm { +export class ExclusiveCanonicalization implements CanonicalizationAlgorithm { protected includeComments = false; constructor() { @@ -265,7 +267,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati * * @api public */ - process(elem: Element, options: CanonicalizationOrTransformationAlgorithmProcessOptions): string { + process(elem: Element, options: TransformAlgorithmOptions): string { options = options || {}; let inclusiveNamespacesPrefixList = options.inclusiveNamespacesPrefixList || []; const defaultNs = options.defaultNs || ""; @@ -299,7 +301,7 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati ancestorNamespaces.forEach(function (ancestorNamespace) { if (prefix === ancestorNamespace.prefix) { elem.setAttributeNS( - "http://www.w3.org/2000/xmlns/", + XMLDSIG_URIS.NAMESPACES.xmlns, `xmlns:${prefix}`, ancestorNamespace.namespaceURI, ); @@ -319,8 +321,8 @@ export class ExclusiveCanonicalization implements CanonicalizationOrTransformati return res; } - getAlgorithmName() { - return "http://www.w3.org/2001/10/xml-exc-c14n#"; + getAlgorithmName(): CanonicalizationAlgorithmURI { + return XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; } } @@ -330,7 +332,7 @@ export class ExclusiveCanonicalizationWithComments extends ExclusiveCanonicaliza this.includeComments = true; } - getAlgorithmName() { - return "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"; + getAlgorithmName(): CanonicalizationAlgorithmURI { + return XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N_WITH_COMMENTS; } } diff --git a/src/hash-algorithms.ts b/src/hash-algorithms.ts index eeeb8b27..e75838e1 100644 --- a/src/hash-algorithms.ts +++ b/src/hash-algorithms.ts @@ -1,5 +1,6 @@ import * as crypto from "crypto"; import type { HashAlgorithm } from "./types"; +import { XMLDSIG_URIS } from "./xmldsig-uris"; export class Sha1 implements HashAlgorithm { getHash = function (xml) { @@ -10,7 +11,7 @@ export class Sha1 implements HashAlgorithm { }; getAlgorithmName = function () { - return "http://www.w3.org/2000/09/xmldsig#sha1"; + return XMLDSIG_URIS.HASH_ALGORITHMS.SHA1; }; } @@ -23,7 +24,7 @@ export class Sha256 implements HashAlgorithm { }; getAlgorithmName = function () { - return "http://www.w3.org/2001/04/xmlenc#sha256"; + return XMLDSIG_URIS.HASH_ALGORITHMS.SHA256; }; } @@ -36,6 +37,6 @@ export class Sha512 implements HashAlgorithm { }; getAlgorithmName = function () { - return "http://www.w3.org/2001/04/xmlenc#sha512"; + return XMLDSIG_URIS.HASH_ALGORITHMS.SHA512; }; } diff --git a/src/index.ts b/src/index.ts index 3c82b7a8..a370bfef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,5 +4,7 @@ export { ExclusiveCanonicalizationWithComments, } from "./exclusive-canonicalization"; export { SignedXml } from "./signed-xml"; +export { XmlDSigVerifier } from "./xmldsig-verifier"; +export { XMLDSIG_URIS } from "./xmldsig-uris"; export * from "./types"; export * from "./utils"; diff --git a/src/signature-algorithms.ts b/src/signature-algorithms.ts index 52e09280..e8512cf8 100644 --- a/src/signature-algorithms.ts +++ b/src/signature-algorithms.ts @@ -1,5 +1,6 @@ import * as crypto from "crypto"; import { type SignatureAlgorithm, createOptionalCallbackFunction } from "./types"; +import { XMLDSIG_URIS } from "./xmldsig-uris"; export class RsaSha1 implements SignatureAlgorithm { getSignature = createOptionalCallbackFunction( @@ -23,7 +24,7 @@ export class RsaSha1 implements SignatureAlgorithm { ); getAlgorithmName = () => { - return "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + return XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1; }; } @@ -49,7 +50,7 @@ export class RsaSha256 implements SignatureAlgorithm { ); getAlgorithmName = () => { - return "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; + return XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA256; }; } @@ -96,7 +97,7 @@ export class RsaSha256Mgf1 implements SignatureAlgorithm { ); getAlgorithmName = () => { - return "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1"; + return XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA256_MGF1; }; } @@ -122,7 +123,7 @@ export class RsaSha512 implements SignatureAlgorithm { ); getAlgorithmName = () => { - return "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"; + return XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA512; }; } @@ -158,6 +159,6 @@ export class HmacSha1 implements SignatureAlgorithm { ); getAlgorithmName = () => { - return "http://www.w3.org/2000/09/xmldsig#hmac-sha1"; + return XMLDSIG_URIS.SIGNATURE_ALGORITHMS.HMAC_SHA1; }; } diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 663d3d0e..4bcb2c56 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -1,18 +1,21 @@ import type { - CanonicalizationAlgorithmType, - CanonicalizationOrTransformAlgorithmType, - CanonicalizationOrTransformationAlgorithm, - CanonicalizationOrTransformationAlgorithmProcessOptions, + CanonicalizationAlgorithmURI, + TransformAlgorithmOptions, ComputeSignatureOptions, ErrorFirstCallback, GetKeyInfoContentArgs, - HashAlgorithm, - HashAlgorithmType, + HashAlgorithmURI, + IdAttributeType, ObjectAttributes, Reference, - SignatureAlgorithm, - SignatureAlgorithmType, + SignatureAlgorithmURI, SignedXmlOptions, + HashAlgorithmMap, + SignatureAlgorithmMap, + CanonicalizationAlgorithmMap, + TransformAlgorithmMap, + VerificationIdAttributeType, + CanonicalizationOrTransformAlgorithmType, } from "./types"; import * as isDomNode from "@xmldom/is-dom-node"; @@ -26,10 +29,30 @@ import * as execC14n from "./exclusive-canonicalization"; import * as hashAlgorithms from "./hash-algorithms"; import * as signatureAlgorithms from "./signature-algorithms"; import * as utils from "./utils"; +import { XMLDSIG_URIS } from "./xmldsig-uris"; +const { + CANONICALIZATION_ALGORITHMS, + HASH_ALGORITHMS, + SIGNATURE_ALGORITHMS, + TRANSFORM_ALGORITHMS, + NAMESPACES, +} = XMLDSIG_URIS; export class SignedXml { + /** + * Specifies the mode to use when searching for ID attributes. + * Planned for deprecation. Use `idAttributes` instead with value [{ prefix: "wsu", localName: "Id", namespaceUri: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" }] + */ idMode?: "wssecurity"; - idAttributes: string[]; + /** + * Specifies the Id attributes which will be used to resolve reference URIs. + * When signing, if no Id attribute is found on the element to be signed the first one from this list will be added. + * If idAttribute is also specified, it will be added to the start of this list. + * + * @default {@link SignedXml.getDefaultIdAttributes()} + * @example [{localName: "Id", namespaceUri: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" }] + */ + idAttributes: IdAttributeType[]; /** * A {@link Buffer} or pem encoded {@link String} containing your private key */ @@ -37,15 +60,16 @@ export class SignedXml { publicCert?: crypto.KeyLike; /** * One of the supported signature algorithms. - * @see {@link SignatureAlgorithmType} + * @see {@link SignatureAlgorithmURI} */ - signatureAlgorithm?: SignatureAlgorithmType = undefined; + signatureAlgorithm?: SignatureAlgorithmURI = undefined; /** * Rules used to convert an XML document into its canonical form. */ - canonicalizationAlgorithm?: CanonicalizationAlgorithmType = undefined; + canonicalizationAlgorithm?: CanonicalizationAlgorithmURI = undefined; /** * It specifies a list of namespace prefixes that should be considered "inclusive" during the canonicalization process. + * Only applicable when using exclusive canonicalization. */ inclusiveNamespacesPrefixList: string[] = []; namespaceResolver: XPathNSResolver = { @@ -53,7 +77,10 @@ export class SignedXml { throw new Error("Not implemented"); }, }; - implicitTransforms: ReadonlyArray = []; + + maxTransforms: number | null; + // eslint-disable-next-line deprecation/deprecation + implicitTransforms: ReadonlyArray = []; // TODO: replace with TransformAlgorithmURI in next breaking change keyInfoAttributes: { [attrName: string]: string } = {}; getKeyInfoContent = SignedXml.getKeyInfoContent; getCertFromKeyInfo = SignedXml.getCertFromKeyInfo; @@ -83,52 +110,69 @@ export class SignedXml { private signedReferences: string[] = []; /** - * To add a new transformation algorithm create a new class that implements the {@link TransformationAlgorithm} interface, and register it here. More info: {@link https://github.com/node-saml/xml-crypto#customizing-algorithms|Customizing Algorithms} + * To add a new canonicalization algorithm create a new class that implements the {@link CanonicalizationAlgorithm} interface, and register it here. More info: {@link https://github.com/node-saml/xml-crypto#customizing-algorithms|Customizing Algorithms} + * @internal Use {@link allowedCanonicalizationAlgorithms} instead */ - CanonicalizationAlgorithms: Record< - CanonicalizationOrTransformAlgorithmType, - new () => CanonicalizationOrTransformationAlgorithm - > = { - "http://www.w3.org/TR/2001/REC-xml-c14n-20010315": c14n.C14nCanonicalization, - "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments": - c14n.C14nCanonicalizationWithComments, - "http://www.w3.org/2001/10/xml-exc-c14n#": execC14n.ExclusiveCanonicalization, - "http://www.w3.org/2001/10/xml-exc-c14n#WithComments": - execC14n.ExclusiveCanonicalizationWithComments, - "http://www.w3.org/2000/09/xmldsig#enveloped-signature": envelopedSignatures.EnvelopedSignature, - }; - - // TODO: In v7.x we may consider deprecating sha1 + CanonicalizationAlgorithms: CanonicalizationAlgorithmMap; /** * To add a new hash algorithm create a new class that implements the {@link HashAlgorithm} interface, and register it here. More info: {@link https://github.com/node-saml/xml-crypto#customizing-algorithms|Customizing Algorithms} + * @internal Use {@link allowedHashAlgorithms} instead */ - HashAlgorithms: Record HashAlgorithm> = { - "http://www.w3.org/2000/09/xmldsig#sha1": hashAlgorithms.Sha1, - "http://www.w3.org/2001/04/xmlenc#sha256": hashAlgorithms.Sha256, - "http://www.w3.org/2001/04/xmlenc#sha512": hashAlgorithms.Sha512, - }; - - // TODO: In v7.x we may consider deprecating sha1 + HashAlgorithms: HashAlgorithmMap; /** * To add a new signature algorithm create a new class that implements the {@link SignatureAlgorithm} interface, and register it here. More info: {@link https://github.com/node-saml/xml-crypto#customizing-algorithms|Customizing Algorithms} + * @internal Use {@link allowedSignatureAlgorithms} instead */ - SignatureAlgorithms: Record SignatureAlgorithm> = { - "http://www.w3.org/2000/09/xmldsig#rsa-sha1": signatureAlgorithms.RsaSha1, - "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256": signatureAlgorithms.RsaSha256, - "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1": signatureAlgorithms.RsaSha256Mgf1, - "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512": signatureAlgorithms.RsaSha512, - // Disabled by default due to key confusion concerns. - // 'http://www.w3.org/2000/09/xmldsig#hmac-sha1': SignatureAlgorithms.HmacSha1 - }; + SignatureAlgorithms: SignatureAlgorithmMap; + + /** + * To add a new transformation algorithm create a new class that implements the {@link TransformAlgorithm} interface, and register it here. + * @internal Use {@link allowedTransformAlgorithms} instead + */ + TransformAlgorithms: TransformAlgorithmMap | undefined; static defaultNsForPrefix = { - ds: "http://www.w3.org/2000/09/xmldsig#", + ds: NAMESPACES.ds, }; static noop = () => null; + static readonly getDefaultCanonicalizationAlgorithms = (): CanonicalizationAlgorithmMap => ({ + [CANONICALIZATION_ALGORITHMS.C14N]: c14n.C14nCanonicalization, + [CANONICALIZATION_ALGORITHMS.C14N_WITH_COMMENTS]: c14n.C14nCanonicalizationWithComments, + [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N]: execC14n.ExclusiveCanonicalization, + [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N_WITH_COMMENTS]: + execC14n.ExclusiveCanonicalizationWithComments, + // TODO: separate TransformAlgorithms from CanonicalizationAlgorithms + [TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE]: envelopedSignatures.EnvelopedSignature, + }); + + static readonly getDefaultHashAlgorithms = (): HashAlgorithmMap => ({ + // TODO: In v7.x we may consider removing sha1 from defaults + [HASH_ALGORITHMS.SHA1]: hashAlgorithms.Sha1, + [HASH_ALGORITHMS.SHA256]: hashAlgorithms.Sha256, + [HASH_ALGORITHMS.SHA512]: hashAlgorithms.Sha512, + }); + + static readonly getDefaultAsymmetricSignatureAlgorithms = (): SignatureAlgorithmMap => ({ + // TODO: In v7.x we may consider removing rsa-sha1 from defaults + [SIGNATURE_ALGORITHMS.RSA_SHA1]: signatureAlgorithms.RsaSha1, + [SIGNATURE_ALGORITHMS.RSA_SHA256]: signatureAlgorithms.RsaSha256, + [SIGNATURE_ALGORITHMS.RSA_SHA256_MGF1]: signatureAlgorithms.RsaSha256Mgf1, + [SIGNATURE_ALGORITHMS.RSA_SHA512]: signatureAlgorithms.RsaSha512, + }); + + static readonly getDefaultSymmetricSignatureAlgorithms = (): SignatureAlgorithmMap => ({ + [SIGNATURE_ALGORITHMS.HMAC_SHA1]: signatureAlgorithms.HmacSha1, + }); + + static readonly getDefaultTransformAlgorithms = (): TransformAlgorithmMap => + SignedXml.getDefaultCanonicalizationAlgorithms(); + + static readonly getDefaultIdAttributes = (): VerificationIdAttributeType[] => ["Id", "ID", "id"]; + /** * The SignedXml constructor provides an abstraction for sign and verify xml documents. The object is constructed using * @param options {@link SignedXmlOptions} @@ -137,21 +181,27 @@ export class SignedXml { const { idMode, idAttribute, + idAttributes, privateKey, publicCert, signatureAlgorithm, canonicalizationAlgorithm, inclusiveNamespacesPrefixList, + maxTransforms, implicitTransforms, keyInfoAttributes, getKeyInfoContent, getCertFromKeyInfo, objects, + allowedSignatureAlgorithms, + allowedHashAlgorithms, + allowedCanonicalizationAlgorithms, + allowedTransformAlgorithms, } = options; // Options this.idMode = idMode; - this.idAttributes = ["Id", "ID", "id"]; + this.idAttributes = idAttributes ?? SignedXml.getDefaultIdAttributes(); if (idAttribute) { this.idAttributes.unshift(idAttribute); } @@ -164,14 +214,18 @@ export class SignedXml { } else if (utils.isArrayHasLength(inclusiveNamespacesPrefixList)) { this.inclusiveNamespacesPrefixList = inclusiveNamespacesPrefixList; } + this.maxTransforms = maxTransforms ?? null; this.implicitTransforms = implicitTransforms ?? this.implicitTransforms; this.keyInfoAttributes = keyInfoAttributes ?? this.keyInfoAttributes; this.getKeyInfoContent = getKeyInfoContent ?? this.getKeyInfoContent; this.getCertFromKeyInfo = getCertFromKeyInfo ?? SignedXml.noop; this.objects = objects; - this.CanonicalizationAlgorithms; - this.HashAlgorithms; - this.SignatureAlgorithms; + this.CanonicalizationAlgorithms = + allowedCanonicalizationAlgorithms ?? SignedXml.getDefaultCanonicalizationAlgorithms(); + this.HashAlgorithms = allowedHashAlgorithms ?? SignedXml.getDefaultHashAlgorithms(); + this.SignatureAlgorithms = + allowedSignatureAlgorithms ?? SignedXml.getDefaultAsymmetricSignatureAlgorithms(); + this.TransformAlgorithms = allowedTransformAlgorithms; // TODO: use default transform algorithms (breaking change) } /** @@ -180,9 +234,8 @@ export class SignedXml { * This enables HMAC and disables other signing algorithms. */ enableHMAC(): void { - this.SignatureAlgorithms = { - "http://www.w3.org/2000/09/xmldsig#hmac-sha1": signatureAlgorithms.HmacSha1, - }; + // eslint-disable-next-line deprecation/deprecation + this.SignatureAlgorithms = SignedXml.getDefaultSymmetricSignatureAlgorithms(); this.getKeyInfoContent = SignedXml.noop; } @@ -404,9 +457,8 @@ export class SignedXml { } if ( - this.canonicalizationAlgorithm === "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" || - this.canonicalizationAlgorithm === - "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" + this.canonicalizationAlgorithm === CANONICALIZATION_ALGORITHMS.C14N || + this.canonicalizationAlgorithm === CANONICALIZATION_ALGORITHMS.C14N_WITH_COMMENTS ) { if (!doc || typeof doc !== "object") { throw new Error( @@ -456,7 +508,7 @@ export class SignedXml { } } - private findSignatureAlgorithm(name?: SignatureAlgorithmType) { + private findSignatureAlgorithm(name?: SignatureAlgorithmURI) { if (name == null) { throw new Error("signatureAlgorithm is required"); } @@ -468,7 +520,7 @@ export class SignedXml { } } - private findCanonicalizationAlgorithm(name: CanonicalizationOrTransformAlgorithmType) { + private findCanonicalizationAlgorithm(name: CanonicalizationAlgorithmURI) { if (name != null) { const algo = this.CanonicalizationAlgorithms[name]; if (algo) { @@ -479,7 +531,7 @@ export class SignedXml { throw new Error(`canonicalization algorithm '${name}' is not supported`); } - private findHashAlgorithm(name: HashAlgorithmType) { + private findHashAlgorithm(name: HashAlgorithmURI) { const algo = this.HashAlgorithms[name]; if (algo) { return new algo(); @@ -488,6 +540,21 @@ export class SignedXml { } } + // eslint-disable-next-line deprecation/deprecation + private findTransformAlgorithm(name: CanonicalizationOrTransformAlgorithmType) { + // TODO: replace with TransformAlgorithmURI in next breaking change + // TODO: remove this fallback (breaking change) + if (this.TransformAlgorithms == null) { + return this.findCanonicalizationAlgorithm(name); + } + const algo = this.TransformAlgorithms[name]; + if (algo) { + return new algo(); + } else { + throw new Error(`transform algorithm '${name}' is not supported`); + } + } + validateElementAgainstReferences(elemOrXpath: Element | string, doc: Document): Reference { let elem: Element; if (typeof elemOrXpath === "string") { @@ -502,11 +569,28 @@ export class SignedXml { for (const ref of this.getReferences()) { const uri = ref.uri?.[0] === "#" ? ref.uri.substring(1) : ref.uri; - for (const attr of this.idAttributes) { - const elemId = elem.getAttribute(attr); - if (uri === elemId) { - ref.xpath = `//*[@*[local-name(.)='${attr}']='${uri}']`; - break; // found the correct element, no need to check further + for (const idAttr of this.idAttributes) { + if (typeof idAttr === "string") { + if (uri === elem.getAttribute(idAttr)) { + // We look for attributes in any namespace or no namespace + ref.xpath = `//*[@*[local-name(.)='${idAttr}']='${uri}']`; + break; // found the correct element, no need to check further + } + } else { + const attr = utils.findAttr(elem, idAttr.localName, idAttr.namespaceUri); + if (attr && uri === attr.value) { + if (typeof idAttr.namespaceUri === "string") { + // When namespaceUri is set, we look for attributes in that specific namespace + ref.xpath = `//*[@*[local-name(.)='${idAttr.localName}' and namespace-uri(.)='${idAttr.namespaceUri}']='${uri}']`; + } else if (idAttr.namespaceUri === null) { + // When namespaceUri is explicitly set to null, we look only for attributes without a namespace + ref.xpath = `//*[@*[local-name(.)='${idAttr.localName}' and namespace-uri(.)='']='${uri}']`; + } else { + // When namespaceUri is undefined, we look for attributes regardless of namespace + ref.xpath = `//*[@*[local-name(.)='${idAttr.localName}']='${uri}']`; + } + break; // found the correct element, no need to check further + } } } @@ -533,8 +617,21 @@ export class SignedXml { throw new Error("Cannot validate a uri with quotes inside it"); } else { let num_elements_for_id = 0; - for (const attr of this.idAttributes) { - const tmp_elemXpath = `//*[@*[local-name(.)='${attr}']='${uri}']`; + for (const idAttr of this.idAttributes) { + let tmp_elemXpath: string; + + if (typeof idAttr === "string") { + tmp_elemXpath = `//*[@*[local-name(.)='${idAttr}']='${uri}']`; + } else { + if (typeof idAttr.namespaceUri === "string") { + tmp_elemXpath = `//*[@*[local-name(.)='${idAttr.localName}' and namespace-uri(.)='${idAttr.namespaceUri}']='${uri}']`; + } else if (idAttr.namespaceUri === null) { + tmp_elemXpath = `//*[@*[local-name(.)='${idAttr.localName}' and namespace-uri(.)='']='${uri}']`; + } else { + tmp_elemXpath = `//*[@*[local-name(.)='${idAttr.localName}']='${uri}']`; + } + } + const tmp_elem = xpath.select(tmp_elemXpath, doc); if (utils.isArrayHasLength(tmp_elem)) { num_elements_for_id += tmp_elem.length; @@ -594,7 +691,7 @@ export class SignedXml { findSignatures(doc: Node): Node[] { const nodes = xpath.select( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `//*[local-name(.)='Signature' and namespace-uri(.)='${NAMESPACES.ds}']`, doc, ); @@ -624,7 +721,13 @@ export class SignedXml { } if (isDomNode.isAttributeNode(node)) { - this.canonicalizationAlgorithm = node.value as CanonicalizationAlgorithmType; + this.canonicalizationAlgorithm = node.value as CanonicalizationAlgorithmURI; + + if (!this.findCanonicalizationAlgorithm(this.canonicalizationAlgorithm)) { + throw new Error( + `unsupported canonicalization algorithm: ${this.canonicalizationAlgorithm}`, + ); + } } const signatureAlgorithm = xpath.select1( @@ -633,7 +736,7 @@ export class SignedXml { ); if (isDomNode.isAttributeNode(signatureAlgorithm)) { - this.signatureAlgorithm = signatureAlgorithm.value as SignatureAlgorithmType; + this.signatureAlgorithm = signatureAlgorithm.value as SignatureAlgorithmURI; } const signedInfoNodes = utils.findChildren(this.signatureNode, "SignedInfo"); @@ -652,12 +755,10 @@ export class SignedXml { let canonicalizationAlgorithmForSignedInfo = this.canonicalizationAlgorithm; if ( !canonicalizationAlgorithmForSignedInfo || - canonicalizationAlgorithmForSignedInfo === - "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" || - canonicalizationAlgorithmForSignedInfo === - "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" + canonicalizationAlgorithmForSignedInfo === CANONICALIZATION_ALGORITHMS.C14N || + canonicalizationAlgorithmForSignedInfo === CANONICALIZATION_ALGORITHMS.C14N_WITH_COMMENTS ) { - canonicalizationAlgorithmForSignedInfo = "http://www.w3.org/2001/10/xml-exc-c14n#"; + canonicalizationAlgorithmForSignedInfo = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; } const temporaryCanonSignedInfo = this.getCanonXml( @@ -773,14 +874,22 @@ export class SignedXml { */ if ( transforms.length === 0 || - transforms[transforms.length - 1] === "http://www.w3.org/2000/09/xmldsig#enveloped-signature" + transforms[transforms.length - 1] === TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE ) { - transforms.push("http://www.w3.org/TR/2001/REC-xml-c14n-20010315"); + transforms.push(CANONICALIZATION_ALGORITHMS.C14N); } const refUri = isDomNode.isElementNode(refNode) ? refNode.getAttribute("URI") || undefined : undefined; + if (this.maxTransforms !== null) { + if (transforms.length > this.maxTransforms) { + throw new Error( + `Number of transforms (${transforms.length}) exceeds the maximum allowed (${this.maxTransforms})`, + ); + } + } + this.addReference({ transforms, digestAlgorithm: digestAlgo, @@ -1095,7 +1204,7 @@ export class SignedXml { } const currentPrefix = prefix ? `${prefix}:` : ""; - const signatureNamespace = "http://www.w3.org/2000/09/xmldsig#"; + const signatureNamespace = XMLDSIG_URIS.NAMESPACES.ds; // Find the SignedInfo element to append to const signedInfoNode = xpath.select1(`./*[local-name(.)='SignedInfo']`, signatureElem); @@ -1161,7 +1270,7 @@ export class SignedXml { ); for (const trans of ref.transforms || []) { - const transform = this.findCanonicalizationAlgorithm(trans); + const transform = this.findTransformAlgorithm(trans); const transformElem = signatureDoc.createElementNS( signatureNamespace, `${currentPrefix}Transform`, @@ -1264,7 +1373,7 @@ export class SignedXml { getCanonXml( transforms: Reference["transforms"], node: Node, - options: CanonicalizationOrTransformationAlgorithmProcessOptions = {}, + options: TransformAlgorithmOptions = {}, ) { options.defaultNsForPrefix = options.defaultNsForPrefix ?? SignedXml.defaultNsForPrefix; options.signatureNode = this.signatureNode; @@ -1275,7 +1384,7 @@ export class SignedXml { transforms.forEach((transformName) => { if (isDomNode.isNodeLike(transformedXml)) { // If, after processing, `transformedNode` is a string, we can't do anymore transforms on it - const transform = this.findCanonicalizationAlgorithm(transformName); + const transform = this.findTransformAlgorithm(transformName); transformedXml = transform.process(transformedXml, options); } //TODO: currently transform.process may return either Node or String value (enveloped transformation returns Node, exclusive-canonicalization returns String). @@ -1298,14 +1407,14 @@ export class SignedXml { let attr; if (this.idMode === "wssecurity") { - attr = utils.findAttr( - node, - "Id", - "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", - ); + attr = utils.findAttr(node, "Id", XMLDSIG_URIS.NAMESPACES.wsu); } else { this.idAttributes.some((idAttribute) => { - attr = utils.findAttr(node, idAttribute); + if (typeof idAttribute === "string") { + attr = utils.findAttr(node, idAttribute); + } else { + attr = utils.findAttr(node, idAttribute.localName, idAttribute.namespaceUri); + } return !!attr; // This will break the loop as soon as a truthy attr is found. }); } @@ -1318,18 +1427,29 @@ export class SignedXml { const id = `_${this.id++}`; if (this.idMode === "wssecurity") { - node.setAttributeNS( - "http://www.w3.org/2000/xmlns/", - "xmlns:wsu", - "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", - ); - node.setAttributeNS( - "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", - "wsu:Id", - id, - ); + node.setAttributeNS(NAMESPACES.xmlns, "xmlns:wsu", NAMESPACES.wsu); + node.setAttributeNS(NAMESPACES.wsu, "wsu:Id", id); } else { - node.setAttribute("Id", id); + // Use the first idAttribute to set the new ID + const firstIdAttr = this.idAttributes[0]; + if (typeof firstIdAttr === "string") { + node.setAttribute(firstIdAttr, id); + } else { + if ("prefix" in firstIdAttr && firstIdAttr.prefix) { + node.setAttributeNS( + NAMESPACES.xmlns, + `xmlns:${firstIdAttr.prefix}`, + firstIdAttr.namespaceUri, + ); + node.setAttributeNS( + firstIdAttr.namespaceUri, + `${firstIdAttr.prefix}:${firstIdAttr.localName}`, + id, + ); + } else { + node.setAttribute(firstIdAttr.localName, id); + } + } } return id; @@ -1345,17 +1465,17 @@ export class SignedXml { "Missing canonicalizationAlgorithm when trying to create signed info for XML", ); } - const transform = this.findCanonicalizationAlgorithm(this.canonicalizationAlgorithm); + const canonicalization = this.findCanonicalizationAlgorithm(this.canonicalizationAlgorithm); const algo = this.findSignatureAlgorithm(this.signatureAlgorithm); const currentPrefix = prefix ? `${prefix}:` : ""; let res = `<${currentPrefix}SignedInfo>`; - res += `<${currentPrefix}CanonicalizationMethod Algorithm="${transform.getAlgorithmName()}"`; + res += `<${currentPrefix}CanonicalizationMethod Algorithm="${canonicalization.getAlgorithmName()}"`; if (utils.isArrayHasLength(this.inclusiveNamespacesPrefixList)) { res += ">"; res += ``; + )}" xmlns="${canonicalization.getAlgorithmName()}"/>`; res += ``; } else { res += " />"; @@ -1384,7 +1504,7 @@ export class SignedXml { const signatureValueXml = `<${prefix}SignatureValue>${this.signatureValue}`; //the canonicalization requires to get a valid xml node. //we need to wrap the info in a dummy signature since it contains the default namespace. - const dummySignatureWrapper = `<${prefix}Signature ${xmlNsAttr}="http://www.w3.org/2000/09/xmldsig#">${signatureValueXml}`; + const dummySignatureWrapper = `<${prefix}Signature ${xmlNsAttr}="${NAMESPACES.ds}">${signatureValueXml}`; const doc = new xmldom.DOMParser().parseFromString(dummySignatureWrapper); diff --git a/src/types.ts b/src/types.ts index 89c0b304..432de774 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,33 +7,22 @@ /// import * as crypto from "crypto"; +import { XMLDSIG_URIS } from "./xmldsig-uris"; +import { KeyLike, X509Certificate } from "node:crypto"; +const { SIGNATURE_ALGORITHMS, HASH_ALGORITHMS, TRANSFORM_ALGORITHMS, CANONICALIZATION_ALGORITHMS } = + XMLDSIG_URIS; export type ErrorFirstCallback = (err: Error | null, result?: T) => void; -export type CanonicalizationAlgorithmType = - | "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" - | "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" - | "http://www.w3.org/2001/10/xml-exc-c14n#" - | "http://www.w3.org/2001/10/xml-exc-c14n#WithComments" - | string; - -export type CanonicalizationOrTransformAlgorithmType = - | CanonicalizationAlgorithmType - | "http://www.w3.org/2000/09/xmldsig#enveloped-signature"; +export type SignatureIdAttributeType = + | string + | { prefix?: undefined; localName: string; namespaceUri?: null } + | { prefix: string; localName: string; namespaceUri: string }; -export type HashAlgorithmType = - | "http://www.w3.org/2000/09/xmldsig#sha1" - | "http://www.w3.org/2001/04/xmlenc#sha256" - | "http://www.w3.org/2001/04/xmlenc#sha512" - | string; - -export type SignatureAlgorithmType = - | "http://www.w3.org/2000/09/xmldsig#rsa-sha1" - | "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" - | "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1" - | "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" - | "http://www.w3.org/2000/09/xmldsig#hmac-sha1" - | string; +export type VerificationIdAttributeType = + | string + | { localName: string; namespaceUri?: string | null }; +export type IdAttributeType = SignatureIdAttributeType | VerificationIdAttributeType; /** * @param cert the certificate as a string or array of strings (@see https://www.w3.org/TR/2008/REC-xmldsig-core-20080610/#sec-X509Data) @@ -59,27 +48,132 @@ export interface ObjectAttributes { [key: string]: string | undefined; } +export type KeySelectorFunction = (keyInfo?: Node | null) => string | null; + +export interface NamespacePrefix { + prefix: string; + namespaceURI: string; +} + +export interface TransformAlgorithmOptions { + defaultNs?: string; + defaultNsForPrefix?: Record; + ancestorNamespaces?: NamespacePrefix[]; + signatureNode?: Node | null; + inclusiveNamespacesPrefixList?: string[]; +} + +export type SignatureAlgorithmURI = + | (typeof SIGNATURE_ALGORITHMS)[keyof typeof SIGNATURE_ALGORITHMS] + | string; + +/** Extend this to create a new SignatureAlgorithm */ +export interface SignatureAlgorithm { + /** + * Sign the given string using the given key + */ + getSignature(signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string; + getSignature( + signedInfo: crypto.BinaryLike, + privateKey: crypto.KeyLike, + callback?: ErrorFirstCallback, + ): void; + /** + * Verify the given signature of the given string using key + * + * @param key a public cert, public key, or private key can be passed here + */ + verifySignature(material: string, key: crypto.KeyLike, signatureValue: string): boolean; + verifySignature( + material: string, + key: crypto.KeyLike, + signatureValue: string, + callback?: ErrorFirstCallback, + ): void; + + getAlgorithmName(): SignatureAlgorithmURI; +} +export type SignatureAlgorithmMap = Record SignatureAlgorithm>; + +export type HashAlgorithmURI = (typeof HASH_ALGORITHMS)[keyof typeof HASH_ALGORITHMS] | string; +/** Implement this to create a new HashAlgorithm */ +export interface HashAlgorithm { + getAlgorithmName(): HashAlgorithmURI; + + getHash(xml: string): string; +} +export type HashAlgorithmMap = Record HashAlgorithm>; + +export type TransformAlgorithmURI = + | (typeof TRANSFORM_ALGORITHMS)[keyof typeof TRANSFORM_ALGORITHMS] + | string; +/** Implement this to create a new TransformAlgorithm */ +export interface TransformAlgorithm { + getAlgorithmName(): TransformAlgorithmURI; + + process(node: Node, options: TransformAlgorithmOptions): string | Node; +} +export type CanonicalizationAlgorithmURI = + | (typeof CANONICALIZATION_ALGORITHMS)[keyof typeof CANONICALIZATION_ALGORITHMS] + | string; +/** Implement this to create a new CanonicalizationAlgorithm */ +export interface CanonicalizationAlgorithm extends TransformAlgorithm { + getAlgorithmName(): CanonicalizationAlgorithmURI; + + // TODO: after canonicalization algorithms algorithms are separated from transform algorithms, + // set process to return string only + process(node: Node, options: TransformAlgorithmOptions): string | Node; +} +export type CanonicalizationAlgorithmMap = Record< + CanonicalizationAlgorithmURI, + new () => CanonicalizationAlgorithm +>; +/** + * @deprecated Use CanonicalizationAlgorithm or TransformAlgorithm instead. + */ +// eslint-disable-next-line deprecation/deprecation +export type CanonicalizationOrTransformationAlgorithm = + | CanonicalizationAlgorithm + | TransformAlgorithm; +export type TransformAlgorithmMap = Record< + TransformAlgorithmURI, + // eslint-disable-next-line deprecation/deprecation + new () => CanonicalizationOrTransformationAlgorithm +>; // TODO: replace with TransformAlgorithm in next breaking change +/** + * @deprecated Use CanonicalizationAlgorithmURI or TransformAlgorithmURI instead. + */ +export type CanonicalizationOrTransformAlgorithmType = + | CanonicalizationAlgorithmURI + | TransformAlgorithmURI; + +/** + * @deprecated Use CanonicalizationAlgorithmURI instead. + */ +export type CanonicalizationAlgorithmType = CanonicalizationAlgorithmURI; /** * Options for the SignedXml constructor. */ export interface SignedXmlOptions { idMode?: "wssecurity"; - idAttribute?: string; + idAttribute?: SignatureIdAttributeType; + idAttributes?: VerificationIdAttributeType[]; privateKey?: crypto.KeyLike; publicCert?: crypto.KeyLike; - signatureAlgorithm?: SignatureAlgorithmType; - canonicalizationAlgorithm?: CanonicalizationAlgorithmType; + signatureAlgorithm?: SignatureAlgorithmURI; + canonicalizationAlgorithm?: CanonicalizationAlgorithmURI; inclusiveNamespacesPrefixList?: string | string[]; - implicitTransforms?: ReadonlyArray; + maxTransforms?: number | null; + // eslint-disable-next-line deprecation/deprecation + implicitTransforms?: ReadonlyArray; // TODO: replace with TransformAlgorithmURI in next breaking change keyInfoAttributes?: Record; getKeyInfoContent?(args?: GetKeyInfoContentArgs): string | null; - getCertFromKeyInfo?(keyInfo?: Node | null): string | null; + getCertFromKeyInfo?: KeySelectorFunction; objects?: Array<{ content: string; attributes?: ObjectAttributes }>; -} - -export interface NamespacePrefix { - prefix: string; - namespaceURI: string; + allowedSignatureAlgorithms?: SignatureAlgorithmMap; + allowedHashAlgorithms?: HashAlgorithmMap; + allowedCanonicalizationAlgorithms?: CanonicalizationAlgorithmMap; + allowedTransformAlgorithms?: TransformAlgorithmMap; } export interface RenderedNamespace { @@ -87,14 +181,6 @@ export interface RenderedNamespace { newDefaultNs: string; } -export interface CanonicalizationOrTransformationAlgorithmProcessOptions { - defaultNs?: string; - defaultNsForPrefix?: Record; - ancestorNamespaces?: NamespacePrefix[]; - signatureNode?: Node | null; - inclusiveNamespacesPrefixList?: string[]; -} - export interface ComputeSignatureOptionsLocation { reference?: string; action?: "append" | "prepend" | "before" | "after"; @@ -127,10 +213,11 @@ export interface Reference { xpath?: string; // An array of transforms to be applied to the data before signing. - transforms: ReadonlyArray; + // eslint-disable-next-line deprecation/deprecation + transforms: ReadonlyArray; // TODO: replace with TransformAlgorithmURI in next breaking change // The algorithm used to calculate the digest value of the data. - digestAlgorithm: HashAlgorithmType; + digestAlgorithm: HashAlgorithmURI; // The URI that identifies the data to be signed. uri: string; @@ -160,57 +247,6 @@ export interface Reference { signedReference?: string; } -/** Implement this to create a new CanonicalizationOrTransformationAlgorithm */ -export interface CanonicalizationOrTransformationAlgorithm { - process( - node: Node, - options: CanonicalizationOrTransformationAlgorithmProcessOptions, - ): Node | string; - - getAlgorithmName(): CanonicalizationOrTransformAlgorithmType; -} - -/** Implement this to create a new HashAlgorithm */ -export interface HashAlgorithm { - getAlgorithmName(): HashAlgorithmType; - - getHash(xml: string): string; -} - -/** Extend this to create a new SignatureAlgorithm */ -export interface SignatureAlgorithm { - /** - * Sign the given string using the given key - */ - getSignature(signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string; - getSignature( - signedInfo: crypto.BinaryLike, - privateKey: crypto.KeyLike, - callback?: ErrorFirstCallback, - ): void; - /** - * Verify the given signature of the given string using key - * - * @param key a public cert, public key, or private key can be passed here - */ - verifySignature(material: string, key: crypto.KeyLike, signatureValue: string): boolean; - verifySignature( - material: string, - key: crypto.KeyLike, - signatureValue: string, - callback?: ErrorFirstCallback, - ): void; - - getAlgorithmName(): SignatureAlgorithmType; -} - -/** Implement this to create a new TransformAlgorithm */ -export interface TransformAlgorithm { - getAlgorithmName(): CanonicalizationOrTransformAlgorithmType; - - process(node: Node): string; -} - /** * ### Sign * #### Properties @@ -268,3 +304,189 @@ export function createOptionalCallbackFunction( (...args: [...A, ErrorFirstCallback]): void; }; } + +/*** XmlDSigVerifier types ***/ + +export type CertificateKeySelector = { + /** Public certificate or key to use for verification */ + publicCert: KeyLike; +}; + +export type KeyInfoKeySelector = { + /** Function to extract the public key from KeyInfo element */ + getCertFromKeyInfo: (keyInfo?: Node | null) => string | null; +}; + +export type SharedSecretKeySelector = { + /** Shared secret key to use for HMAC verification */ + sharedSecretKey: KeyLike; +}; + +export type KeySelector = CertificateKeySelector | KeyInfoKeySelector | SharedSecretKeySelector; + +/** + * Common configuration options for XML-DSig verification (Base). + */ +export interface XmlDSigVerifierOptionsBase { + /** + * Names of XML attributes to treat as element identifiers. + * @default {@link SignedXml.getDefaultIdAttributes()} + */ + idAttributes?: VerificationIdAttributeType[]; + + /** + * Transforms to apply implicitly during canonicalization. + */ + // eslint-disable-next-line deprecation/deprecation + implicitTransforms?: ReadonlyArray; // TODO: replace with TransformAlgorithmURI in next breaking change + + /** + * Whether to throw an exception on verification failure. + * @default false + */ + throwOnError?: boolean; +} + +export interface XmlDSigVerifierSecurityOptions { + /** + * Maximum number of transforms allowed per Reference element. + * Limits complexity to prevent denial-of-service attacks. + * @default {@link SignedXml.DEFAULT_MAX_TRANSFORMS} + */ + maxTransforms?: number; + + /** + * Signature algorithms allowed during verification. + * + * @default {@link SignedXml.getDefaultAsymmetricSignatureAlgorithms()} {@link SignedXml.getDefaultSymmetricSignatureAlgorithms()} + */ + signatureAlgorithms?: SignatureAlgorithmMap; + + /** + * Hash algorithms allowed during verification. + * + * @default {@link SignedXml.getDefaultHashAlgorithms()} + */ + hashAlgorithms?: HashAlgorithmMap; + + /** + * Transform algorithms allowed during verification. (This must include canonicalization algorithms) + * + * @default all algorithms in {@link SignedXml.getDefaultTransformAlgorithms()} + */ + transformAlgorithms?: TransformAlgorithmMap; + + /** + * Canonicalization algorithms allowed during verification. + * + * @default all algorithms in {@link SignedXml.getDefaultCanonicalizationAlgorithms()} + */ + canonicalizationAlgorithms?: CanonicalizationAlgorithmMap; +} + +export interface KeyInfoXmlDSigSecurityOptions extends XmlDSigVerifierSecurityOptions { + /** + * Check certificate expiration dates during verification. + * If true, signatures with expired certificates will be considered invalid. + * This only applies when using KeyInfoKeySelector + * @default true + */ + checkCertExpiration?: boolean; + + /** + * Optional truststore of trusted certificates + * When provided, the certificate used to sign the XML must chain to one of these trusted certificates. + * These must be PEM or DER encoded X509 certificates + */ + truststore?: Array; +} + +/** + * Configuration options for verification using KeyInfo. + * Allows advanced security options for cert validation. + */ +export type KeyInfoXmlDSigVerifierOptions = XmlDSigVerifierOptionsBase & { + /** + * Function to extract the public key from KeyInfo element. + */ + keySelector: KeyInfoKeySelector; + + /** + * Security options for KeyInfo verification. + */ + security?: KeyInfoXmlDSigSecurityOptions; +}; + +/** + * Configuration options for verification using a provided public certificate. + * Certificate validation options (e.g., expiration) are not applicable here. + */ +export type PublicCertXmlDSigVerifierOptions = XmlDSigVerifierOptionsBase & { + /** + * Public certificate or key to use for verification. + */ + keySelector: CertificateKeySelector; + + /** + * Basic security options for verification. + */ + security?: XmlDSigVerifierSecurityOptions; +}; + +/** + * Configuration options for verification using a shared secret (HMAC). + */ +export type SharedSecretXmlDSigVerifierOptions = XmlDSigVerifierOptionsBase & { + /** + * Shared secret key to use for HMAC verification. + */ + keySelector: SharedSecretKeySelector; + + /** + * Basic security options for verification. + */ + security?: XmlDSigVerifierSecurityOptions; +}; + +export type XmlDSigVerifierOptions = + | KeyInfoXmlDSigVerifierOptions + | PublicCertXmlDSigVerifierOptions + | SharedSecretXmlDSigVerifierOptions; + +/** + * Verification result containing the outcome and signed content. + */ +export type SuccessfulXmlDsigVerificationResult = { + /** Whether the signature was successfully verified */ + success: true; + error?: undefined; + /** The canonicalized XML content that passed verification */ + signedReferences: string[]; +}; + +export type FailedXmlDsigVerificationResult = { + /** Whether the signature was successfully verified */ + success: false; + /** Error message if verification failed */ + error: string; + signedReferences?: undefined; +}; + +export type XmlDsigVerificationResult = + | SuccessfulXmlDsigVerificationResult + | FailedXmlDsigVerificationResult; + +/** + * @deprecated Use TransformAlgorithmOptions instead. + */ +export type CanonicalizationOrTransformationAlgorithmProcessOptions = TransformAlgorithmOptions; + +/** + * @deprecated Use SignatureAlgorithmURI instead. + */ +export type SignatureAlgorithmType = SignatureAlgorithmURI; + +/** + * @deprecated Use HashAlgorithmURI instead. + */ +export type HashAlgorithmType = HashAlgorithmURI; diff --git a/src/utils.ts b/src/utils.ts index 466b252e..308f2508 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,18 +6,33 @@ export function isArrayHasLength(array: unknown): array is unknown[] { return Array.isArray(array) && array.length > 0; } -function attrEqualsExplicitly(attr: Attr, localName: string, namespace?: string) { +function attrEqualsExplicitly(attr: Attr, localName: string, namespace?: string | null) { + if (namespace === null) { + return attr.localName === localName && !attr.namespaceURI; + } return attr.localName === localName && (attr.namespaceURI === namespace || namespace == null); } -function attrEqualsImplicitly(attr: Attr, localName: string, namespace?: string, node?: Element) { +function attrEqualsImplicitly( + attr: Attr, + localName: string, + namespace?: string | null, + node?: Element, +) { + if (namespace === null) { + return attr.localName === localName && !attr.namespaceURI; + } return ( attr.localName === localName && ((!attr.namespaceURI && node?.namespaceURI === namespace) || namespace == null) ); } -export function findAttr(element: Element, localName: string, namespace?: string) { +export function findAttr( + element: Element, + localName: string, + namespace?: string | null | undefined, +) { for (let i = 0; i < element.attributes.length; i++) { const attr = element.attributes[i]; diff --git a/src/xmldsig-uris.ts b/src/xmldsig-uris.ts new file mode 100644 index 00000000..0c498716 --- /dev/null +++ b/src/xmldsig-uris.ts @@ -0,0 +1,60 @@ +/** + * Supported canonicalization algorithms + */ +const CANONICALIZATION_ALGORITHMS = { + C14N: "http://www.w3.org/TR/2001/REC-xml-c14n-20010315", + C14N_WITH_COMMENTS: "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments", + EXCLUSIVE_C14N: "http://www.w3.org/2001/10/xml-exc-c14n#", + EXCLUSIVE_C14N_WITH_COMMENTS: "http://www.w3.org/2001/10/xml-exc-c14n#WithComments", +} as const; + +/** + * Supported transform algorithms (includes canonicalization + enveloped signature) + */ +const TRANSFORM_ALGORITHMS = { + ...CANONICALIZATION_ALGORITHMS, + ENVELOPED_SIGNATURE: "http://www.w3.org/2000/09/xmldsig#enveloped-signature", +} as const; + +/** + * Supported digest algorithms + */ +const HASH_ALGORITHMS = { + SHA1: "http://www.w3.org/2000/09/xmldsig#sha1", + SHA256: "http://www.w3.org/2001/04/xmlenc#sha256", + SHA512: "http://www.w3.org/2001/04/xmlenc#sha512", +} as const; + +/** + * Supported signature algorithms + */ +const SIGNATURE_ALGORITHMS = { + RSA_SHA1: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + RSA_SHA256: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + RSA_SHA256_MGF1: "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1", + RSA_SHA512: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", + HMAC_SHA1: "http://www.w3.org/2000/09/xmldsig#hmac-sha1", +} as const; + +/** + * Common XML namespaces + */ +const NAMESPACES = { + xml: "http://www.w3.org/XML/1998/namespace", + xmlns: "http://www.w3.org/2000/xmlns/", + ds: "http://www.w3.org/2000/09/xmldsig#", + wsu: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", + wsse: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", + xades: "http://uri.etsi.org/01903/v1.3.2#", +} as const; + +/** + * XML-DSig URI constants organized by category + */ +export const XMLDSIG_URIS = { + CANONICALIZATION_ALGORITHMS, + TRANSFORM_ALGORITHMS, + HASH_ALGORITHMS, + SIGNATURE_ALGORITHMS, + NAMESPACES, +} as const; diff --git a/src/xmldsig-verifier.ts b/src/xmldsig-verifier.ts new file mode 100644 index 00000000..67db21cb --- /dev/null +++ b/src/xmldsig-verifier.ts @@ -0,0 +1,312 @@ +import { KeyLike, X509Certificate } from "node:crypto"; +import { DOMParser } from "@xmldom/xmldom"; +import { SignedXml } from "./signed-xml"; +import { + KeySelectorFunction, + SignedXmlOptions, + VerificationIdAttributeType, + XmlDSigVerifierOptions, + XmlDsigVerificationResult, + TransformAlgorithmURI, + KeyInfoXmlDSigSecurityOptions, + KeyInfoKeySelector, + SharedSecretKeySelector, + CertificateKeySelector, + XmlDSigVerifierSecurityOptions, + KeyInfoXmlDSigVerifierOptions, + SharedSecretXmlDSigVerifierOptions, + PublicCertXmlDSigVerifierOptions, +} from "./types"; +import { isArrayHasLength } from "./utils"; + +type ResolvedXmlDSigVerifierOptionsBase = { + idAttributes: VerificationIdAttributeType[]; + implicitTransforms?: ReadonlyArray; + throwOnError: boolean; +}; + +type ResolvedKeyInfoOptions = ResolvedXmlDSigVerifierOptionsBase & { + optionsType: "keyinfo"; + keySelector: KeyInfoKeySelector; + security: Required; +}; + +type ResolvedCertificateOptions = ResolvedXmlDSigVerifierOptionsBase & { + optionsType: "certificate"; + keySelector: CertificateKeySelector; + security: Required; +}; + +type ResolvedSharedSecretOptions = ResolvedXmlDSigVerifierOptionsBase & { + optionsType: "sharedsecret"; + keySelector: SharedSecretKeySelector; + security: Required; +}; + +type ResolvedXmlDsigVerifierOptions = + | ResolvedKeyInfoOptions + | ResolvedCertificateOptions + | ResolvedSharedSecretOptions; + +const isResolvedKeyInfoOptions = ( + options: ResolvedXmlDsigVerifierOptions, +): options is ResolvedKeyInfoOptions => options.optionsType === "keyinfo"; + +const isResolvedPublicCertOptions = ( + options: ResolvedXmlDsigVerifierOptions, +): options is ResolvedCertificateOptions => options.optionsType === "certificate"; + +const isResolvedSharedSecretOptions = ( + options: ResolvedXmlDsigVerifierOptions, +): options is ResolvedSharedSecretOptions => options.optionsType === "sharedsecret"; + +const isKeyInfoSelector = ( + options: XmlDSigVerifierOptions, +): options is KeyInfoXmlDSigVerifierOptions => "getCertFromKeyInfo" in options.keySelector; + +const isSharedSecretSelector = ( + options: XmlDSigVerifierOptions, +): options is SharedSecretXmlDSigVerifierOptions => "sharedSecretKey" in options.keySelector; + +const isPublicCertSelector = ( + options: XmlDSigVerifierOptions, +): options is PublicCertXmlDSigVerifierOptions => "publicCert" in options.keySelector; + +/** + * A focused API for XML signature verification with enhanced security. + */ +export class XmlDSigVerifier { + private readonly signedXml: SignedXml; + private readonly options: ResolvedXmlDsigVerifierOptions; + + public static readonly DEFAULT_MAX_TRANSFORMS = 4; + public static readonly DEFAULT_CHECK_CERT_EXPIRATION = true; + public static readonly DEFAULT_THROW_ON_ERROR = false; + + /** + * Creates a new XmlDSigVerifier instance. The instance can be reused for multiple verifications. + * + * @param options Configuration options for verification + */ + constructor(options: XmlDSigVerifierOptions) { + this.options = XmlDSigVerifier.resolveOptions(options); + + this.signedXml = XmlDSigVerifier.createSignedXml(this.options); + } + + /** + * Verifies an XML signature. Static convenience method for one-off verifications. + * + * @param xml The signed XML document to validate + * @param options Configuration options for verification + * @param signatureNode Optional specific Signature node to validate + */ + public static verifySignature( + xml: string, + options: XmlDSigVerifierOptions, + signatureNode?: Node, + ): XmlDsigVerificationResult { + try { + return new XmlDSigVerifier(options).verifySignature(xml, signatureNode); + } catch (error) { + return XmlDSigVerifier.handleError( + error, + options.throwOnError ?? XmlDSigVerifier.DEFAULT_THROW_ON_ERROR, + ); + } + } + + /** + * Validates an XML signature using the pre-configured options. + * + * @param xml The signed XML document to validate + * @param signatureNode Optional specific Signature node to validate + * @returns Verification result with signed references if successful + */ + public verifySignature(xml: string, signatureNode?: Node): XmlDsigVerificationResult { + try { + // Load the signature node + if (signatureNode) { + // Use the provided signature node + this.signedXml.loadSignature(signatureNode); + } else { + // Auto-detect signature if exactly one signature is found in the document + const doc = new DOMParser().parseFromString(xml, "application/xml"); + const signatureNodes = this.signedXml.findSignatures(doc); + + if (signatureNodes.length === 0) { + return XmlDSigVerifier.handleError( + "No Signature element found in the provided XML document.", + this.options.throwOnError, + ); + } else if (signatureNodes.length > 1) { + return XmlDSigVerifier.handleError( + "Multiple Signature elements found in the provided XML document. Please provide the specific signatureNode parameter to validate.", + this.options.throwOnError, + ); + } + + // Load the single found signature + this.signedXml.loadSignature(signatureNodes[0]); + } + + // Perform cryptographic verification + const isValid = this.signedXml.checkSignature(xml); + + if (!isValid) { + throw new Error("Signature verification failed"); + } + + return { + success: isValid, + signedReferences: this.signedXml.getSignedReferences(), + }; + } catch (error) { + return XmlDSigVerifier.handleError(error, this.options.throwOnError); + } + } + + private static resolveOptions(options: XmlDSigVerifierOptions): ResolvedXmlDsigVerifierOptions { + const defaults = { + idAttributes: SignedXml.getDefaultIdAttributes(), + maxTransforms: XmlDSigVerifier.DEFAULT_MAX_TRANSFORMS, + checkCertExpiration: XmlDSigVerifier.DEFAULT_CHECK_CERT_EXPIRATION, + truststore: [], + signatureAlgorithms: isSharedSecretSelector(options) + ? SignedXml.getDefaultSymmetricSignatureAlgorithms() + : SignedXml.getDefaultAsymmetricSignatureAlgorithms(), + hashAlgorithms: SignedXml.getDefaultHashAlgorithms(), + transformAlgorithms: SignedXml.getDefaultTransformAlgorithms(), + canonicalizationAlgorithms: SignedXml.getDefaultCanonicalizationAlgorithms(), + }; + + const baseOptions = { + idAttributes: options.idAttributes ?? defaults.idAttributes, + implicitTransforms: options.implicitTransforms, + throwOnError: options.throwOnError ?? XmlDSigVerifier.DEFAULT_THROW_ON_ERROR, + }; + + const baseSecurity = { + maxTransforms: options.security?.maxTransforms ?? defaults.maxTransforms, + signatureAlgorithms: options.security?.signatureAlgorithms ?? defaults.signatureAlgorithms, + hashAlgorithms: options.security?.hashAlgorithms ?? defaults.hashAlgorithms, + transformAlgorithms: options.security?.transformAlgorithms ?? defaults.transformAlgorithms, + canonicalizationAlgorithms: + options.security?.canonicalizationAlgorithms ?? defaults.canonicalizationAlgorithms, + }; + + if (isKeyInfoSelector(options)) { + return { + optionsType: "keyinfo", + ...baseOptions, + keySelector: options.keySelector, + security: { + ...baseSecurity, + checkCertExpiration: + options.security?.checkCertExpiration ?? defaults.checkCertExpiration, + truststore: options.security?.truststore ?? defaults.truststore, + }, + }; + } else if (isSharedSecretSelector(options)) { + return { + optionsType: "sharedsecret", + ...baseOptions, + keySelector: options.keySelector, + security: baseSecurity, + }; + } else if (isPublicCertSelector(options)) { + return { + optionsType: "certificate", + ...baseOptions, + keySelector: options.keySelector, + security: baseSecurity, + }; + } else { + throw new Error("XmlDSigVerifier requires a valid keySelector option."); + } + } + + private static createSignedXml(options: ResolvedXmlDsigVerifierOptions): SignedXml { + const signedXmlOptions: SignedXmlOptions = { + publicCert: undefined as KeyLike | undefined, + getCertFromKeyInfo: undefined as KeySelectorFunction | undefined, + idAttributes: options.idAttributes, + maxTransforms: options.security.maxTransforms, + implicitTransforms: options.implicitTransforms, + allowedSignatureAlgorithms: options.security.signatureAlgorithms, + allowedHashAlgorithms: options.security.hashAlgorithms, + allowedTransformAlgorithms: options.security.transformAlgorithms, + allowedCanonicalizationAlgorithms: options.security.canonicalizationAlgorithms, + }; + + if (isResolvedKeyInfoOptions(options)) { + const keySelector = options.keySelector; + + if (typeof keySelector.getCertFromKeyInfo !== "function") { + throw new Error("XmlDSigVerifier requires a valid getCertFromKeyInfo function."); + } + + const getCertFromKeyInfo = keySelector.getCertFromKeyInfo; + const truststore = options.security.truststore.map((cert) => { + if (typeof cert === "string" || Buffer.isBuffer(cert)) { + const x509 = new X509Certificate(cert); + return x509.publicKey; + } + return cert.publicKey; + }); + const checkCertExpiration = options.security.checkCertExpiration; + + signedXmlOptions.getCertFromKeyInfo = (keyInfo?: Node | null): string | null => { + const certPem = getCertFromKeyInfo(keyInfo); + if (!certPem) { + return null; + } + + if (checkCertExpiration || isArrayHasLength(truststore)) { + const x509 = new X509Certificate(certPem); + if (checkCertExpiration) { + const now = new Date(); + if (x509.validTo && new Date(x509.validTo) < now) { + throw new Error("The certificate used to sign the XML has expired."); + } + if (x509.validFrom && new Date(x509.validFrom) > now) { + throw new Error("The certificate used to sign the XML is not yet valid."); + } + } + if (isArrayHasLength(truststore)) { + const isTrusted = truststore.some((trustedCert) => { + if (trustedCert.equals?.(x509.publicKey) || x509.verify(trustedCert)) { + return true; + } + return false; + }); + if (!isTrusted) { + throw new Error("The certificate used to sign the XML is not trusted."); + } + } + } + return certPem; + }; + } else if (isResolvedPublicCertOptions(options)) { + signedXmlOptions.publicCert = options.keySelector.publicCert; + } else if (isResolvedSharedSecretOptions(options)) { + signedXmlOptions.privateKey = options.keySelector.sharedSecretKey; + } + + return new SignedXml(signedXmlOptions); + } + + private static handleError(error: unknown, throwOnError: boolean): XmlDsigVerificationResult { + if (throwOnError) { + throw error; + } + + const errorMessage = + error instanceof Error ? error.message : `Verification error occurred: ${String(error)}`; + + return { + success: false, + error: errorMessage, + }; + } +} diff --git a/test/c14n-non-exclusive-unit-tests.spec.ts b/test/c14n-non-exclusive-unit-tests.spec.ts index ee7f2ba4..73505052 100644 --- a/test/c14n-non-exclusive-unit-tests.spec.ts +++ b/test/c14n-non-exclusive-unit-tests.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; -import { C14nCanonicalization } from "../src/c14n-canonicalization"; +import { C14nCanonicalization } from "../src"; import * as xmldom from "@xmldom/xmldom"; import * as xpath from "xpath"; import * as utils from "../src/utils"; diff --git a/test/c14nWithComments-unit-tests.spec.ts b/test/c14nWithComments-unit-tests.spec.ts index fbf36895..2ef98f34 100644 --- a/test/c14nWithComments-unit-tests.spec.ts +++ b/test/c14nWithComments-unit-tests.spec.ts @@ -3,7 +3,7 @@ import { expect } from "chai"; import { ExclusiveCanonicalizationWithComments as c14nWithComments } from "../src/exclusive-canonicalization"; import * as xmldom from "@xmldom/xmldom"; import * as xpath from "xpath"; -import { SignedXml } from "../src/index"; +import { SignedXml, XMLDSIG_URIS } from "../src"; import * as isDomNode from "@xmldom/is-dom-node"; const compare = function (xml, xpathArg, expected, inclusiveNamespacesPrefixList?: string[]) { @@ -216,7 +216,7 @@ describe("Exclusive canonicalization with comments", function () { ); }); - /* + /* TODO: Uncomment this when this issue is fixed it("Exclusive canonicalization removal of whitespace between PITarget and its data", function () { compare( @@ -242,7 +242,7 @@ describe("Exclusive canonicalization with comments", function () { ); }); - /* + /* TODO: Uncomment this when this issue is fixed it("The XML declaration and document type declaration (DTD) are removed, stylesheet retained", function () { compare( @@ -356,8 +356,8 @@ describe("Exclusive canonicalization with comments", function () { const sig = new SignedXml(); const res = sig.getCanonXml( [ - "http://www.w3.org/2000/09/xmldsig#enveloped-signature", - "http://www.w3.org/2001/10/xml-exc-c14n#", + XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE, + XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, ], node, ); @@ -373,7 +373,7 @@ describe("Exclusive canonicalization with comments", function () { const doc = new xmldom.DOMParser().parseFromString(xml); const node = xpath.select1("//*[local-name(.)='y']", doc); const sig = new SignedXml(); - const transforms = ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"]; + const transforms = [XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE]; isDomNode.assertIsNodeLike(node); const res = sig.getCanonXml(transforms, node); expect(res).to.equal(""); diff --git a/test/canonicalization-unit-tests.spec.ts b/test/canonicalization-unit-tests.spec.ts index 7a39f168..97d4d71e 100644 --- a/test/canonicalization-unit-tests.spec.ts +++ b/test/canonicalization-unit-tests.spec.ts @@ -1,9 +1,8 @@ import { expect } from "chai"; -import { ExclusiveCanonicalization } from "../src/exclusive-canonicalization"; import * as xmldom from "@xmldom/xmldom"; import * as xpath from "xpath"; -import { SignedXml } from "../src/index"; +import { SignedXml, ExclusiveCanonicalization, XMLDSIG_URIS } from "../src"; import * as isDomNode from "@xmldom/is-dom-node"; const compare = function ( @@ -58,7 +57,7 @@ describe("Canonicalization unit tests", function () { "//*[local-name(.)='SignedInfo']", '', undefined, - { ds: "http://www.w3.org/2000/09/xmldsig#" }, + { ds: XMLDSIG_URIS.NAMESPACES.ds }, ); }); @@ -407,8 +406,8 @@ describe("Canonicalization unit tests", function () { const sig = new SignedXml(); const res = sig.getCanonXml( [ - "http://www.w3.org/2000/09/xmldsig#enveloped-signature", - "http://www.w3.org/2001/10/xml-exc-c14n#", + XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE, + XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, ], node, ); @@ -426,12 +425,12 @@ describe("Canonicalization unit tests", function () { const sig = new SignedXml(); const res1 = sig.getCanonXml( [ - "http://www.w3.org/2001/10/xml-exc-c14n#", - "http://www.w3.org/2000/09/xmldsig#enveloped-signature", + XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE, ], node1, ); - const res2 = sig.getCanonXml(["http://www.w3.org/2001/10/xml-exc-c14n#"], node2); + const res2 = sig.getCanonXml([XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], node2); expect(res1) .to.equal(res2) .to.equal( @@ -450,7 +449,7 @@ describe("Canonicalization unit tests", function () { isDomNode.assertIsNodeLike(node); const sig = new SignedXml(); - const transforms = ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"]; + const transforms = [XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE]; const res = sig.getCanonXml(transforms, node); expect(res).to.equal(""); }); diff --git a/test/document-tests.spec.ts b/test/document-tests.spec.ts index b8311994..84392ec5 100644 --- a/test/document-tests.spec.ts +++ b/test/document-tests.spec.ts @@ -1,4 +1,4 @@ -import { SignedXml } from "../src/index"; +import { SignedXml, XMLDSIG_URIS } from "../src"; import * as xpath from "xpath"; import * as xmldom from "@xmldom/xmldom"; import * as fs from "fs"; @@ -10,7 +10,7 @@ describe("Document tests", function () { const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); const node = xpath.select1( - "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); @@ -28,7 +28,7 @@ describe("Document tests", function () { const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); const node = xpath.select1( - "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); diff --git a/test/hmac-tests.spec.ts b/test/hmac-tests.spec.ts index 6573ca6b..9ebaa2dd 100644 --- a/test/hmac-tests.spec.ts +++ b/test/hmac-tests.spec.ts @@ -1,4 +1,4 @@ -import { SignedXml } from "../src/index"; +import { SignedXml, XMLDSIG_URIS } from "../src"; import * as xpath from "xpath"; import * as xmldom from "@xmldom/xmldom"; import * as fs from "fs"; @@ -10,7 +10,7 @@ describe("HMAC tests", function () { const xml = fs.readFileSync("./test/static/hmac_signature.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); const signature = xpath.select1( - "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(signature); @@ -29,7 +29,7 @@ describe("HMAC tests", function () { const xml = fs.readFileSync("./test/static/hmac_signature.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); const signature = xpath.select1( - "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(signature); @@ -48,18 +48,18 @@ describe("HMAC tests", function () { const sig = new SignedXml(); sig.enableHMAC(); sig.privateKey = fs.readFileSync("./test/static/hmac.key"); - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#hmac-sha1"; + sig.signatureAlgorithm = XMLDSIG_URIS.SIGNATURE_ALGORITHMS.HMAC_SHA1; sig.addReference({ xpath: "//*[local-name(.)='book']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, + transforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.canonicalizationAlgorithm = XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; sig.computeSignature(xml); const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml()); const signature = xpath.select1( - "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(signature); diff --git a/test/key-info-tests.spec.ts b/test/key-info-tests.spec.ts index 19c8f4a7..4ba648a1 100644 --- a/test/key-info-tests.spec.ts +++ b/test/key-info-tests.spec.ts @@ -1,7 +1,7 @@ import * as xmldom from "@xmldom/xmldom"; import * as fs from "fs"; import * as xpath from "xpath"; -import { SignedXml } from "../src/index"; +import { SignedXml, XMLDSIG_URIS } from "../src"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; @@ -11,8 +11,8 @@ describe("KeyInfo tests", function () { const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); sig.publicCert = fs.readFileSync("./test/static/client_public.pem"); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getSignedXml(); const doc = new xmldom.DOMParser().parseFromString(signedXml); @@ -27,14 +27,14 @@ describe("KeyInfo tests", function () { const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/hmac.key"); sig.publicCert = fs.readFileSync("./test/static/hmac.key"); - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#hmac-sha1"; + sig.signatureAlgorithm = XMLDSIG_URIS.SIGNATURE_ALGORITHMS.HMAC_SHA1; sig.enableHMAC(); sig.addReference({ xpath: "//*[local-name(.)='book']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, + transforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.canonicalizationAlgorithm = XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; sig.computeSignature(xml); const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml()); diff --git a/test/saml-response-tests.spec.ts b/test/saml-response-tests.spec.ts index eb349098..1677a703 100644 --- a/test/saml-response-tests.spec.ts +++ b/test/saml-response-tests.spec.ts @@ -1,4 +1,4 @@ -import { SignedXml } from "../src/index"; +import { SignedXml, XMLDSIG_URIS } from "../src"; import * as xpath from "xpath"; import * as xmldom from "@xmldom/xmldom"; import * as fs from "fs"; @@ -10,7 +10,7 @@ describe("SAML response tests", function () { const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); const signature = xpath.select1( - "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(signature); @@ -27,7 +27,7 @@ describe("SAML response tests", function () { const xml = fs.readFileSync("./test/static/valid_saml_sha256_rsa_mgf1.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); const signature = xpath.select1( - "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(signature); @@ -43,7 +43,7 @@ describe("SAML response tests", function () { const xml = fs.readFileSync("./test/static/invalid_saml_sha256_rsa_mgf1.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); const signature = xpath.select1( - "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(signature); @@ -61,7 +61,7 @@ describe("SAML response tests", function () { const assertion = xpath.select1("//*[local-name(.)='Assertion']", doc); isDomNode.assertIsNodeLike(assertion); const signature = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, assertion, ); isDomNode.assertIsNodeLike(signature); @@ -83,7 +83,7 @@ describe("SAML response tests", function () { const xml = fs.readFileSync("./test/static/saml_external_ns.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); const signature = xpath.select1( - "//*//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `//*//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(signature); @@ -101,7 +101,7 @@ describe("SAML response tests", function () { const assertion = xpath.select1("//*[local-name(.)='Assertion']", doc); isDomNode.assertIsNodeLike(assertion); const signature = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, assertion, ); isDomNode.assertIsNodeLike(signature); @@ -117,7 +117,7 @@ describe("SAML response tests", function () { const xml = fs.readFileSync("./test/static/valid_saml_withcomments.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); const signature = xpath.select1( - "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(signature); @@ -133,7 +133,7 @@ describe("SAML response tests", function () { const xml = fs.readFileSync("./test/static/invalid_saml_no_signed_info.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); const node = xpath.select1( - "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); @@ -151,7 +151,7 @@ describe("SAML response tests", function () { const assertion = xpath.select1("//*[local-name(.)='Assertion']", doc); isDomNode.assertIsNodeLike(assertion); const signature = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, assertion, ); isDomNode.assertIsNodeLike(signature); @@ -172,7 +172,7 @@ describe("SAML response tests", function () { const assertion = xpath.select1("//*[local-name(.)='Assertion'][1]", doc); isDomNode.assertIsNodeLike(assertion); const signature = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, assertion, ); isDomNode.assertIsNodeLike(signature); @@ -191,7 +191,7 @@ describe("SAML response tests", function () { const assertion = xpath.select1("//*[local-name(.)='Assertion']", doc); isDomNode.assertIsNodeLike(assertion); const signature = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, assertion, ); isDomNode.assertIsNodeLike(signature); diff --git a/test/signature-integration-tests.spec.ts b/test/signature-integration-tests.spec.ts index 02da0949..12962337 100644 --- a/test/signature-integration-tests.spec.ts +++ b/test/signature-integration-tests.spec.ts @@ -1,6 +1,6 @@ import * as xpath from "xpath"; import * as xmldom from "@xmldom/xmldom"; -import { SignedXml } from "../src/index"; +import { SignedXml, XMLDSIG_URIS } from "../src"; import * as fs from "fs"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; @@ -10,16 +10,16 @@ describe("Signature integration tests", function () { const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); - xpath.map(function (n) { + xpath.forEach(function (n) { sig.addReference({ xpath: n, - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, + transforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); }); sig.canonicalizationAlgorithm = canonicalizationAlgorithm; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.signatureAlgorithm = XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signed = sig.getSignedXml(); @@ -34,7 +34,7 @@ describe("Signature integration tests", function () { xml, "./test/static/integration/expectedVerify.xml", ["//*[local-name(.)='x']", "//*[local-name(.)='y']", "//*[local-name(.)='w']"], - "http://www.w3.org/2001/10/xml-exc-c14n#", + XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, ); }); @@ -54,7 +54,7 @@ describe("Signature integration tests", function () { xml, "./test/static/integration/expectedVerifyComplex.xml", ["//*[local-name(.)='book']"], - "http://www.w3.org/2001/10/xml-exc-c14n#", + XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, ); }); @@ -101,7 +101,7 @@ describe("Signature integration tests", function () { const childXml = doc.firstChild?.toString(); const signature = xpath.select1( - "//*//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `//*//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(signature); @@ -120,7 +120,7 @@ describe("Signature integration tests", function () { const childXml = doc.firstChild?.toString(); const signature = xpath.select1( - "//*//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `//*//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(signature); @@ -142,7 +142,7 @@ describe("Signature integration tests", function () { const childXml = doc.firstChild?.toString(); const signature = xpath.select1( - "//*//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `//*//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(signature); @@ -164,7 +164,7 @@ describe("Signature integration tests", function () { const childXml = doc.firstChild?.toString(); const signature = xpath.select1( - "//*//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `//*//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(signature); @@ -183,12 +183,12 @@ describe("Signature integration tests", function () { const sig = new SignedXml(); sig.addReference({ xpath: "//*[local-name(.)='book']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, + transforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); sig.privateKey = fs.readFileSync("./test/static/client.pem"); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signed = sig.getSignedXml(); diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index 75126c23..fdd45e7f 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -3,7 +3,7 @@ import { expect, assert } from "chai"; import * as xpath from "xpath"; import * as xmldom from "@xmldom/xmldom"; import * as isDomNode from "@xmldom/is-dom-node"; -import { SignedXml } from "../src"; +import { SignedXml, XMLDSIG_URIS } from "../src"; import { Sha256 } from "../src/hash-algorithms"; const privateKey = fs.readFileSync("./test/static/client.pem", "utf-8"); @@ -11,13 +11,13 @@ const publicCert = fs.readFileSync("./test/static/client_public.pem", "utf-8"); const publicCertDer = fs.readFileSync("./test/static/client_public.der"); const selectNs = (expression: string, node: Node, ns?: Record) => xpath.useNamespaces({ - ds: "http://www.w3.org/2000/09/xmldsig#", + ds: XMLDSIG_URIS.NAMESPACES.ds, xades: "http://uri.etsi.org/01903/v1.3.2#", ...ns, })(expression, node, false); const select1Ns = (expression: string, node: Node, ns?: Record) => xpath.useNamespaces({ - ds: "http://www.w3.org/2000/09/xmldsig#", + ds: XMLDSIG_URIS.NAMESPACES.ds, xades: "http://uri.etsi.org/01903/v1.3.2#", ...ns, })(expression, node, true); @@ -44,8 +44,8 @@ describe("ds:Object support in XML signatures", function () { const sig = new SignedXml({ privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1, objects: [ { content: "Test data in Object element", @@ -75,8 +75,8 @@ describe("ds:Object support in XML signatures", function () { sig.addReference({ xpath: "//*[local-name(.)='x']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, + transforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); sig.computeSignature(xml); @@ -124,8 +124,8 @@ describe("ds:Object support in XML signatures", function () { const sig = new SignedXml({ privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1, objects: [ { content: "Test data", @@ -139,8 +139,8 @@ describe("ds:Object support in XML signatures", function () { sig.addReference({ xpath: "/*", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, + transforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); // When we add a prefix to the signature, there is no default namespace @@ -159,15 +159,15 @@ describe("ds:Object support in XML signatures", function () { // Test with undefined objects const sigWithNull = new SignedXml({ privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1, objects: undefined, }); sigWithNull.addReference({ xpath: "/*", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, + transforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); sigWithNull.computeSignature(xml); @@ -182,15 +182,15 @@ describe("ds:Object support in XML signatures", function () { // Test with empty array objects const sigWithEmpty = new SignedXml({ privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1, objects: [], }); sigWithEmpty.addReference({ xpath: "/*", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, + transforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); sigWithEmpty.computeSignature(xml); @@ -208,8 +208,8 @@ describe("ds:Object support in XML signatures", function () { const sig = new SignedXml({ privateKey: privateKey, - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1, + canonicalizationAlgorithm: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, objects: [ { content: @@ -224,8 +224,8 @@ describe("ds:Object support in XML signatures", function () { sig.addReference({ xpath: "//*[local-name(.)='Object' and @Id='object1']", inclusiveNamespacesPrefixList: ["ns1", "ns2"], - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, + transforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); sig.computeSignature(xml); @@ -246,12 +246,12 @@ describe("ds:Object support in XML signatures", function () { const transformEl = select1Ns("ds:Transforms/ds:Transform", referenceEl); isDomNode.assertIsElementNode(transformEl); expect(transformEl.getAttribute("Algorithm")).to.equal( - "http://www.w3.org/2001/10/xml-exc-c14n#", + XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, ); // Verify that the InclusiveNamespacesPrefixList is set correctly const inclusiveNamespacesEl = select1Ns("ec:InclusiveNamespaces", transformEl, { - ec: "http://www.w3.org/2001/10/xml-exc-c14n#", + ec: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, }); isDomNode.assertIsElementNode(inclusiveNamespacesEl); expect(inclusiveNamespacesEl.getAttribute("PrefixList")).to.equal("ns1 ns2"); @@ -259,9 +259,7 @@ describe("ds:Object support in XML signatures", function () { // Verify that the Reference contains the correct DigestMethod const digestMethodEl = select1Ns("ds:DigestMethod", referenceEl); isDomNode.assertIsElementNode(digestMethodEl); - expect(digestMethodEl.getAttribute("Algorithm")).to.equal( - "http://www.w3.org/2000/09/xmldsig#sha1", - ); + expect(digestMethodEl.getAttribute("Algorithm")).to.equal(XMLDSIG_URIS.HASH_ALGORITHMS.SHA1); // Verify that the Reference contains a non-empty DigestValue const digestValueEl = select1Ns("ds:DigestValue", referenceEl); @@ -276,8 +274,8 @@ describe("Valid signatures with ds:Object elements", function () { const sig = new SignedXml({ privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1, objects: [ { content: "Test data in Object element", @@ -291,10 +289,10 @@ describe("Valid signatures with ds:Object elements", function () { sig.addReference({ xpath: "/*", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, transforms: [ - "http://www.w3.org/2000/09/xmldsig#enveloped-signature", - "http://www.w3.org/2001/10/xml-exc-c14n#", + XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE, + XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, ], }); @@ -312,8 +310,8 @@ describe("Valid signatures with ds:Object elements", function () { const sig = new SignedXml({ privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1, inclusiveNamespacesPrefixList: ["ns1", "ns2"], objects: [ { @@ -329,19 +327,19 @@ describe("Valid signatures with ds:Object elements", function () { sig.addReference({ xpath: "/*", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, transforms: [ - "http://www.w3.org/2000/09/xmldsig#enveloped-signature", - "http://www.w3.org/2001/10/xml-exc-c14n#", + XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE, + XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, ], }); sig.addReference({ xpath: "//*[local-name(.)='Object' and @Id='object1']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, transforms: [ - "http://www.w3.org/2000/09/xmldsig#enveloped-signature", - "http://www.w3.org/2001/10/xml-exc-c14n#", + XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE, + XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, ], inclusiveNamespacesPrefixList: ["ns1", "ns3"], }); @@ -371,8 +369,8 @@ describe("Valid signatures with ds:Object elements", function () { const xml = ""; const sig = new SignedXml({ privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1, objects: [ { content: "Test data in Object element", @@ -382,10 +380,10 @@ describe("Valid signatures with ds:Object elements", function () { sig.addReference({ xpath: "//*[local-name(.)='Data']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, transforms: [ - "http://www.w3.org/2000/09/xmldsig#enveloped-signature", - "http://www.w3.org/2001/10/xml-exc-c14n#", + XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE, + XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, ], }); @@ -415,8 +413,8 @@ describe("Should successfuly sign references to ds:KeyInfo elements", function ( const xml = ""; const sig = new SignedXml({ privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1, keyInfoAttributes: { Id: "key-info-1", }, @@ -425,10 +423,10 @@ describe("Should successfuly sign references to ds:KeyInfo elements", function ( sig.addReference({ xpath: "//*[local-name(.)='KeyInfo']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, transforms: [ - "http://www.w3.org/2000/09/xmldsig#enveloped-signature", - "http://www.w3.org/2001/10/xml-exc-c14n#", + XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE, + XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, ], }); @@ -453,17 +451,17 @@ describe("Should successfuly sign references to ds:KeyInfo elements", function ( const xml = ""; const sig = new SignedXml({ privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1, getKeyInfoContent: () => "", }); sig.addReference({ xpath: "//*[local-name(.)='KeyInfo']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, transforms: [ - "http://www.w3.org/2000/09/xmldsig#enveloped-signature", - "http://www.w3.org/2001/10/xml-exc-c14n#", + XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE, + XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, ], }); @@ -503,8 +501,8 @@ describe("XAdES Object support in XML signatures", function () { const sig = new SignedXml({ publicCert, privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + canonicalizationAlgorithm: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA256, objects: [ { content: @@ -526,18 +524,18 @@ describe("XAdES Object support in XML signatures", function () { sig.addReference({ xpath: `/*`, isEmptyUri: true, - digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA256, transforms: [ - "http://www.w3.org/2000/09/xmldsig#enveloped-signature", - "http://www.w3.org/2001/10/xml-exc-c14n#", + XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE, + XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, ], }); sig.addReference({ xpath: `//*[@Id='${signedPropertiesId}']`, type: "http://uri.etsi.org/01903#SignedProperties", - digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA256, + transforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); sig.computeSignature(xml, { @@ -581,14 +579,14 @@ describe("Signature self-reference prevention", function () { const sig = new SignedXml({ privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1, }); sig.addReference({ xpath: ".//*[local-name(.)='Signature']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, + transforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); expect(() => { @@ -601,14 +599,14 @@ describe("Signature self-reference prevention", function () { const sig = new SignedXml({ privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1, }); sig.addReference({ xpath: ".//*[local-name(.)='SignedInfo']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, + transforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); expect(() => { @@ -621,20 +619,20 @@ describe("Signature self-reference prevention", function () { const sig = new SignedXml({ privateKey, - canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - signatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1, }); sig.addReference({ xpath: "/*", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, + transforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); sig.addReference({ xpath: ".//*[local-name(.)='Reference']/*", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, + transforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); expect(() => { diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index c0dcf136..5f21b2fb 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -1,16 +1,19 @@ import * as xpath from "xpath"; import * as xmldom from "@xmldom/xmldom"; -import { SignedXml, createOptionalCallbackFunction } from "../src/index"; +import { SignedXml, createOptionalCallbackFunction, XMLDSIG_URIS } from "../src"; import * as fs from "fs"; import * as crypto from "crypto"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; +const { SIGNATURE_ALGORITHMS, HASH_ALGORITHMS, CANONICALIZATION_ALGORITHMS, NAMESPACES } = + XMLDSIG_URIS; + const signatureAlgorithms = [ - "http://www.w3.org/2000/09/xmldsig#rsa-sha1", - "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", - "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1", - "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", + SIGNATURE_ALGORITHMS.RSA_SHA1, + SIGNATURE_ALGORITHMS.RSA_SHA256, + SIGNATURE_ALGORITHMS.RSA_SHA256_MGF1, + SIGNATURE_ALGORITHMS.RSA_SHA512, ]; describe("Signature unit tests", function () { @@ -23,11 +26,11 @@ describe("Signature unit tests", function () { sig.addReference({ xpath: "//*[local-name(.)='x']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; sig.signatureAlgorithm = signatureAlgorithm; sig.computeSignature(xml); return sig.getSignedXml(); @@ -36,7 +39,7 @@ describe("Signature unit tests", function () { function loadSignature(xml: string): SignedXml { const doc = new xmldom.DOMParser().parseFromString(xml); const node = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(node); @@ -92,22 +95,22 @@ describe("Signature unit tests", function () { sig.addReference({ xpath: "//*[local-name(.)='x']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); sig.addReference({ xpath: "//*[local-name(.)='y']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); sig.addReference({ xpath: "//*[local-name(.)='w']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getOriginalXmlWithIds(); const doc = new xmldom.DOMParser().parseFromString(signedXml); @@ -140,15 +143,15 @@ describe("Signature unit tests", function () { sig.addReference({ xpath: "//*[@wsu:Id]", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml, { existingPrefixes: { - wsu: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", + wsu: NAMESPACES.wsu, }, }); @@ -166,11 +169,11 @@ describe("Signature unit tests", function () { sig.privateKey = fs.readFileSync("./test/static/client.pem"); sig.addReference({ xpath: "//*[local-name(.)='x']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getOriginalXmlWithIds(); const doc = new xmldom.DOMParser().parseFromString(signedXml); @@ -201,12 +204,12 @@ describe("Signature unit tests", function () { sig.addReference({ xpath: "//*[local-name(.)='name']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml, { attrs: attrs, }); @@ -228,7 +231,7 @@ describe("Signature unit tests", function () { expect( signatureNode.getAttribute("xmlns"), 'xmlns attribute is not equal to the expected value: "http://www.w3.org/2000/09/xmldsig#"', - ).to.equal("http://www.w3.org/2000/09/xmldsig#"); + ).to.equal(XMLDSIG_URIS.NAMESPACES.ds); }); it("signer appends signature to the root node by default", function () { @@ -238,11 +241,11 @@ describe("Signature unit tests", function () { sig.privateKey = fs.readFileSync("./test/static/client.pem"); sig.addReference({ xpath: "//*[local-name(.)='name']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml()); @@ -262,12 +265,12 @@ describe("Signature unit tests", function () { 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#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml, { location: { reference: "/root/name", @@ -294,12 +297,12 @@ describe("Signature unit tests", function () { 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#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml, { location: { reference: "/root/name", @@ -325,12 +328,12 @@ describe("Signature unit tests", function () { 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#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml, { location: { reference: "/root/name", @@ -357,12 +360,12 @@ describe("Signature unit tests", function () { 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#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml, { location: { reference: "/root/name", @@ -713,22 +716,22 @@ describe("Signature unit tests", function () { sig.addReference({ xpath: "//*[local-name(.)='x']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); sig.addReference({ xpath: "//*[local-name(.)='y']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); sig.addReference({ xpath: "//*[local-name(.)='w']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getSignedXml(); const expected = @@ -781,7 +784,7 @@ describe("Signature unit tests", function () { ); getAlgorithmName = function () { - return "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + return SIGNATURE_ALGORITHMS.RSA_SHA1; }; } @@ -794,21 +797,21 @@ describe("Signature unit tests", function () { sig.addReference({ xpath: "//*[local-name(.)='x']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); sig.addReference({ xpath: "//*[local-name(.)='y']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); sig.addReference({ xpath: "//*[local-name(.)='w']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; sig.computeSignature(xml, function () { const signedXml = sig.getSignedXml(); const expected = @@ -852,7 +855,7 @@ describe("Signature unit tests", function () { const xml = fs.readFileSync(file, "utf8"); const doc = new xmldom.DOMParser().parseFromString(xml); const signature = xpath.select1( - "/*//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `/*//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsElementNode(signature); @@ -860,11 +863,11 @@ describe("Signature unit tests", function () { sig.loadSignature(toString ? signature.toString() : signature); expect(sig.canonicalizationAlgorithm, "wrong canonicalization method").to.equal( - "http://www.w3.org/2001/10/xml-exc-c14n#", + CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, ); expect(sig.signatureAlgorithm, "wrong signature method").to.equal( - "http://www.w3.org/2000/09/xmldsig#rsa-sha1", + SIGNATURE_ALGORITHMS.RSA_SHA1, ); sig.getCertFromKeyInfo = (keyInfo) => { @@ -909,9 +912,9 @@ describe("Signature unit tests", function () { `wrong uri for index ${i}. expected: ${expectedUri} actual: ${ref.uri}`, ).to.equal(expectedUri); expect(ref.transforms.length).to.equal(1); - expect(ref.transforms[0]).to.equal("http://www.w3.org/2001/10/xml-exc-c14n#"); + expect(ref.transforms[0]).to.equal(CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N); expect(ref.digestValue).to.equal(digests[i]); - expect(ref.digestAlgorithm).to.equal("http://www.w3.org/2000/09/xmldsig#sha1"); + expect(ref.digestAlgorithm).to.equal(HASH_ALGORITHMS.SHA1); } } @@ -932,7 +935,7 @@ describe("Signature unit tests", function () { function loadSignature(xml: string, idMode?: "wssecurity") { const doc = new xmldom.DOMParser().parseFromString(xml); const node = xpath.select1( - "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(node); @@ -1056,16 +1059,16 @@ describe("Signature unit tests", function () { 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", + transforms: [XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE], + digestAlgorithm: HASH_ALGORITHMS.SHA1, uri: "", digestValue: "", inclusiveNamespacesPrefixList: [], isEmptyUri: true, }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getSignedXml(); const doc = new xmldom.DOMParser().parseFromString(signedXml); @@ -1081,8 +1084,8 @@ describe("Signature unit tests", function () { 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#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); try { @@ -1123,8 +1126,8 @@ describe("Signature unit tests", function () { const assertionId = "_81d5fba5c807be9e9cf60c58566349b1"; sig.getKeyInfoContent = getKeyInfoContentWithAssertionId.bind(this, { assertionId }); sig.privateKey = fs.readFileSync("./test/static/client.pem"); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml, { prefix: "ds", location: { @@ -1132,8 +1135,8 @@ describe("Signature unit tests", function () { action: "after", }, existingPrefixes: { - wsse: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", - wsu: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", + wsse: NAMESPACES.wsse, + wsu: NAMESPACES.wsu, }, }); const result = sig.getSignedXml(); @@ -1149,15 +1152,15 @@ describe("Signature unit tests", function () { 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", + transforms: [XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE], + digestAlgorithm: HASH_ALGORITHMS.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.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getSignedXml(); @@ -1186,15 +1189,15 @@ describe("Signature unit tests", function () { 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", + transforms: [XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE], + digestAlgorithm: HASH_ALGORITHMS.SHA1, uri: "", digestValue: "", inclusiveNamespacesPrefixList: [], }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getSignedXml(); @@ -1214,12 +1217,12 @@ describe("Signature unit tests", function () { 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", + transforms: [XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE], + digestAlgorithm: HASH_ALGORITHMS.SHA1, }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getSignedXml(); @@ -1253,12 +1256,12 @@ describe("Signature unit tests", function () { 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", + transforms: [XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE], + digestAlgorithm: HASH_ALGORITHMS.SHA1, }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getSignedXml(); @@ -1284,8 +1287,8 @@ describe("Signature unit tests", function () { }; sig.getKeyInfoContent = () => ""; - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getSignedXml(); @@ -1316,8 +1319,8 @@ describe("Signature unit tests", function () { const pemBuffer = fs.readFileSync("./test/static/client_bundle.pem"); sig.privateKey = pemBuffer; sig.publicCert = pemBuffer; - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getSignedXml(); @@ -1355,14 +1358,14 @@ describe("Signature unit tests", function () { sig.addReference({ xpath: "//*[local-name(.)='x']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], id: "ref-1", type: "http://www.w3.org/2000/09/xmldsig#Object", }); - sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#"; - sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getSignedXml(); @@ -1389,14 +1392,14 @@ describe("Signature unit tests", function () { it("should throw if xpath matches no nodes", () => { 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", + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA256, }); sig.addReference({ xpath: "//definitelyNotThere", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); expect(() => sig.computeSignature("")).to.throw( @@ -1408,14 +1411,14 @@ describe("Signature unit tests", function () { const xml = ''; 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/2000/09/xmldsig#rsa-sha1", + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, }); sig.addReference({ xpath: "//*[local-name(.)='x']", - digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"], + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], }); sig.computeSignature(xml); diff --git a/test/static/chain_client.crt.pem b/test/static/chain_client.crt.pem new file mode 100644 index 00000000..f20a5799 --- /dev/null +++ b/test/static/chain_client.crt.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIUb+8ZAYLznP4nyAYw6Wr+8fojmsowDQYJKoZIhvcNAQEL +BQAwVDELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5 +MRUwEwYDVQQKDAxPcmdhbml6YXRpb24xDzANBgNVBAMMBlJvb3RDQTAeFw0yNTEw +MjcyMTQ3MjVaFw0zNTEwMjUyMTQ3MjVaMFgxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI +DAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRMw +EQYDVQQDDApTaWduZWRDZXJ0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEArRiG7bSPk4pNlCzlGBXG8Wmcpt25oL5GXaSgOkLozCqNGT3Ozoujs6GnIaJQ +mvR6RUBlrjjPqw1R1UQZip1NKViEOYzgwy7TmD5P6Aj1Ds+WWke7j1xCtl1sw7l1 +Bwo0BQ5iRW9hhujOgoOEK3XH7OxcwTax4zWM1UNstJmVIP/OdJU+QoMWxm2eAvK4 +P464XJ3FokUmw26srDiCO2xxLaQH3ygPx4gc0BNMpSBravTXGpPudcSgS9dbPPEP +rkpCMMc5sXvqeMnCHznfG+9AEJTS6mQA4cfhcAImnpwdz9O9S8aw//8IHD6vejJW +/xeqQW5mSGlz42njNORrn8rtpQIDAQABo0IwQDAdBgNVHQ4EFgQUoeCxmSUsDnfI +28ViPssKhEoR9eowHwYDVR0jBBgwFoAUmasR+rPpj9JuLcRl3Fn1FLpEIIUwDQYJ +KoZIhvcNAQELBQADggEBADAWzRh6y98k41B6Hdt//yG7VqDQJVJyMqQ/UXcPt3aC +QbBZSopAR7n1kb4UlhBuCHhnj54V4JdotW2PS9kBNN3s/0OOZqk6069DDyZhKNH4 +KK2KVcieNtuwRSVU9mrVsTDvJHFnmwKkL2YS+DRqtQtzHLLJjOYf2yGz978ZgP38 +HFz1xCYQjitBFVVlzpnBm4HPfKj9279oO3cpByM+GIDNSEND+TkVZ3g+/1bTXFeH +h4NIFIqoyQktH7p6isJpASGwJlSrlxe/WrX85pmtHZFqVwfbMKZUx7LBgpkjrZwq +JA79GZ7IijhhU8WdQASHWrQnucKgXCxTWJUJa029HkE= +-----END CERTIFICATE----- diff --git a/test/static/chain_client.key.pem b/test/static/chain_client.key.pem new file mode 100644 index 00000000..2b655b07 --- /dev/null +++ b/test/static/chain_client.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCtGIbttI+Tik2U +LOUYFcbxaZym3bmgvkZdpKA6QujMKo0ZPc7Oi6OzoacholCa9HpFQGWuOM+rDVHV +RBmKnU0pWIQ5jODDLtOYPk/oCPUOz5ZaR7uPXEK2XWzDuXUHCjQFDmJFb2GG6M6C +g4Qrdcfs7FzBNrHjNYzVQ2y0mZUg/850lT5CgxbGbZ4C8rg/jrhcncWiRSbDbqys +OII7bHEtpAffKA/HiBzQE0ylIGtq9Ncak+51xKBL11s88Q+uSkIwxzmxe+p4ycIf +Od8b70AQlNLqZADhx+FwAiaenB3P071LxrD//wgcPq96Mlb/F6pBbmZIaXPjaeM0 +5Gufyu2lAgMBAAECggEAUxcwgf/IYh8kQWpRqL+fabh0ScequWZNOdtyTLVcsdEF +PWYllZGDihGhvGwBvHh6Dy8sADdmPKqeqzzO8/KxnRTQGB4vsJIUYYMb8XsHQ85T +UtAXUWiM36S2NrgaXMBBm2G9u64NR2kO5KjEM+aMi4ckuV0LhFFq4t7EWmdVJmrJ +dqnL1m1nCb2pfBoO3+DDO/auHbGGRVYHu9S/vB8qeCsSudbCERPzDFly4IUUqkGS +9+zrQzZOjyG6isrrV07qRxS1nEBh4ZB5Nq1xgl1ptjnQn4IJBd5571smCr1+2FWG +QGj6KlB6qJgYi42GDkfsaK85O0Hal8dFi4NjjN2wiwKBgQDv+KAnQSgGZONz31hu +/OlLtpwNRL/2XnoCk9tGkGCPaZT7pqo0KSmqBQjWHqKrdNaRsPQClTGSZJWjz58U +Zhs7FPuG/ibIsJgJOW5RDFJ9n4gwAHMbwr8AMvp164mNVn/8xZwG9B3ZEp64fRDE +YldCytWknEWqQ6a37o8kOI6OmwKBgQC4qF6UY6qUTuY1oY3BM+dyWM61vv9iotar +UzbfY/ufXqU/5OhxY8E5SZUO4g3Zrl5RtEdOoM8Py7mymvznSlzJulRcX+6SESbV +A+5O9fGCqtQ+v1P1xGRE3t73myXrJ6s/znZBphMuMNIkqsmqxA48vq9tmsgsYhPb +egO7qTAYvwKBgHE5dAdRfNsXeyJO/WDQwBrTPGoeSByskxDoRovSz1ybSoo6JxCZ +Y2kvGu48YjBX3m27ekZFsrAJ+XjjG4H6c1q7Gbql7BLBD9s6V8yx7bIMNavAao9s +ocYsR3Sf/7TKXXUcn/O/9t1XJcCScfjXFakUHx2eBljBtsYOL0e9z7WFAoGBAIo0 +KaVx+sdJTe8x3MCPMlhYs00/iDCwo25St6z2Ter3kUKC9p13BbT0p4UeFzOm15zb +CsuEe7Tcyz0r1sDc3Rl2RZFlk07rW17utDuQw5MCfBwCYrp8pHcPP12eVwDrDbaR +tdxoic52Z7FdydXvKqC4LuAfilX9idMoPQcFF6RNAoGAAKt5ehZpu6O/vIVLX2Oa +7/6ktelkghs4IjzYklEUY1Qjb4xbDOwHVDlsgczhgQf7a/pnLnk4OiLbSUt9fa8s +2uFDcQmNBxvPELc0CxnY3jeey8cmINFyejlsR94nxBDbLOvGisigkyfG/2sl8Tpq +IlK7fz+aDRZrmeJO1YW4aQY= +-----END PRIVATE KEY----- diff --git a/test/static/chain_root.crt.pem b/test/static/chain_root.crt.pem new file mode 100644 index 00000000..d6e2ee8b --- /dev/null +++ b/test/static/chain_root.crt.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDiTCCAnGgAwIBAgIUR9LnAGXI1oyKIYuthL2GhMVSYlQwDQYJKoZIhvcNAQEL +BQAwVDELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5 +MRUwEwYDVQQKDAxPcmdhbml6YXRpb24xDzANBgNVBAMMBlJvb3RDQTAeFw0yNTEw +MjcyMTQ1MjhaFw0zNTEwMjUyMTQ1MjhaMFQxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI +DAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMQ8w +DQYDVQQDDAZSb290Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCY +eFugeXX+MELgCdh4qAHsqodLDx3yCHsZQnmtA0qDxiVrnXT0zYVXUuqTjv1zBtce +scqqOmjsxnlTv5FIDkrIlgz8rbvP6VQDUaAz4j3t62l3C3N67z4QEfW7052rDauZ +JzonBVMiUAAlY769a+cOstPzjW3Pe3hvKbP5pTHlyn9L4wCCsfjXgP5n+BcVs8wC +kvjRG9fAOVjE0OCzaowVtCqdPkZ/iQ2A5Q+Fz/FtIjcrEowANnbaAyEkHogu2LuN +94qH9FFx7ZPMo3uVT+0bulFDw5ms/yaYAz9wOmZe+EGrIVS069TXnExIEGjB7VzF +zAJDXXWCuMD9icpJFs/bAgMBAAGjUzBRMB0GA1UdDgQWBBSZqxH6s+mP0m4txGXc +WfUUukQghTAfBgNVHSMEGDAWgBSZqxH6s+mP0m4txGXcWfUUukQghTAPBgNVHRMB +Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBjOKuGCGvKYi7izcRFBWqHFZJA +Qd9WesrWSpNBAd1sflGBJt7pMrI08Z3Zk04JL0oyTuYJndFvoc1putnq7YVtFxhQ +hrqzea3+Wb6MxeAsmGgiwNrGtl3BGaTdvz/t2eVE84oNtscypJKJQz9ApZmnEQ3t +6ZbTTDDCzx3UiiTvKodOSPuMDyaJUeTreXEr2dD8hseH39NCgqxKhyUJXzidwhbq +7wj3Ga9HpQnLepJVVADNYpkRG7q1nuByiZj91b99o7eMpkvidPCrGXk30EJ7t28n +MwCEBUj3dG+d/7iogXuptsL79Hk3dTmCgTQ1d/HVDbeh2500nO2/8X3kBUY0 +-----END CERTIFICATE----- diff --git a/test/static/chain_root.crt.srl b/test/static/chain_root.crt.srl new file mode 100644 index 00000000..8d11a28e --- /dev/null +++ b/test/static/chain_root.crt.srl @@ -0,0 +1 @@ +6FEF190182F39CFE27C80630E96AFEF1FA239ACA diff --git a/test/static/chain_root.key.pem b/test/static/chain_root.key.pem new file mode 100644 index 00000000..71c7686b --- /dev/null +++ b/test/static/chain_root.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCYeFugeXX+MELg +Cdh4qAHsqodLDx3yCHsZQnmtA0qDxiVrnXT0zYVXUuqTjv1zBtcescqqOmjsxnlT +v5FIDkrIlgz8rbvP6VQDUaAz4j3t62l3C3N67z4QEfW7052rDauZJzonBVMiUAAl +Y769a+cOstPzjW3Pe3hvKbP5pTHlyn9L4wCCsfjXgP5n+BcVs8wCkvjRG9fAOVjE +0OCzaowVtCqdPkZ/iQ2A5Q+Fz/FtIjcrEowANnbaAyEkHogu2LuN94qH9FFx7ZPM +o3uVT+0bulFDw5ms/yaYAz9wOmZe+EGrIVS069TXnExIEGjB7VzFzAJDXXWCuMD9 +icpJFs/bAgMBAAECggEABK3nnKcy+ILL//WNZTaSyIv7QachyM8l0q1Nmq0NLmTN +uohx5x7KRtcj8ooNkT2BK09S5E038dGti4/HilSV9aNVRrP2SE0654HWeYEqBrvr +grHL8NaYKwTCFpT0s86O7lEGjIgHTc2daN2veZTEL0P3E/R1OExBHU2ZrmxkJE85 +3xranr0ZnxOXfT1b5tzO95sO8MmCr46O12imNMPFLLwHwqTZZr0nFNnlJ6OVOnH7 +CssCsycfYmk/IGhr0b2xPf/r+V3cFHfUi/lY6ZC/gWyo5BRIKjDJ+VJzbS1QLzhe +UorFuzxZ8KrdVuHl8dfUETZG4L82M2wAiX/1iYNqEQKBgQDGG/Sr5h1GdQnoRi0G +5m24V/Z8/wrkYVzxjfRlmLcxNKTA9H0bQaFaS6TGHHPMBnsw1LRc21x7sSIqExWo +66uJ8Nr83PxrJeetnroUaIMRfB8f2jTqTQdAF8fQ4NGEPHpLlmnAg4mX0wQQb0ZO +CS2fotYbh2iVspKOVOUhBRiRDwKBgQDFBkLLM9LZDg1fgimdAvmleaXq6YNQZxph +MapMFfOuT0zIcEAEhEMb6l8sHKTsi9VrYhIi0c2xpuOnhNS+Z2LHwcoUgRKHY+DG +NwEiPlK0JSSdKnKRXJ9g2BXU8I54K8FkcWgJivo6rp8bID9ZoVvLQmWj/qOieAvm +GbA9JVM8dQKBgAcNZbdc2LvyXKjtHps5RryiPP8UITIiGSnsMMARIKxawGayDWYT +/wd02+fFiYXA0U/aspT/photIxc2WLYLta6SaWlJAJ9b2RSAKwWg9tF/hqgen3Wb +yl9IuW9BIZRAhuX788XLqPFDrMhc/ba3cu1U4aRXPKzfj4ILmaCESuyXAoGARKDF +q1pF221VoysHq7VZmBYjgQwNvXfsbGaMVyxeUR02NatD4U7gwVyGAiuIFw0uLdVf +U9mYuITVT4ipQhlpAwOxjCrZdWeI6AJI1tC2piE5+7TJa3DD40vhbubL+XfkSURn +ZMuQFdi1exFkf6gA/XAHT3RnMzR1kJTqGqJht/ECgYAOcJUXE8uQnwFMajPNu/Cj +F2VVcouOtopKhFNqqGYigYWeJT8JDvuJb46xsLf1Blxvgns5thttGkir9UaG1tbH +9DFVOKllX6x2yqyY2fPuNhT4/YadFdkrGtZPqLhmL+Ws4g83j8qi7rcguDMmbf+c +zt4tPipnP8L9OeCzTSaPMA== +-----END PRIVATE KEY----- diff --git a/test/static/expired_certificate.crt.pem b/test/static/expired_certificate.crt.pem new file mode 100644 index 00000000..a3baa560 --- /dev/null +++ b/test/static/expired_certificate.crt.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDizCCAnOgAwIBAgIUai2c+XPx6ig5Gp++qU4GckIHLIQwDQYJKoZIhvcNAQEL +BQAwVTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5 +MRUwEwYDVQQKDAxPcmdhbml6YXRpb24xEDAOBgNVBAMMB0V4cGlyZWQwHhcNMTkx +MjMxMjMwMDAwWhcNMjAwMTAxMjMwMDAwWjBVMQswCQYDVQQGEwJVUzEOMAwGA1UE +CAwFU3RhdGUxDTALBgNVBAcMBENpdHkxFTATBgNVBAoMDE9yZ2FuaXphdGlvbjEQ +MA4GA1UEAwwHRXhwaXJlZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +APpx3e8bSksaJfQqWSjpn09xA/F+acobLdidspYSdLOBGkV9ou1vgzrYrURfFcVj +s/U2s6CzR14kqzE2ZGAi5xdUMxLyx+jLtx851FTtTQEZm4AeZIeC77JNcu998yk0 ++gD7LrAcWWQ1j3f7gj0+ciWy5Yjeck9PyPSLpqpNY0EFy0oBwVHaZd9qnrYrwesJ +mPetQ+lexY11g1t72Oxyh6eimY5Uz4SavhACACYQ9jao5nwZttJ7d/tJa6xHbo1w +pmPze3hhVMMoVWWA+Wb9jxkjhzqZF7i0wu3wzTk3AQPmfU3QMPNpwJaH278WT9B1 +1hONCyEPA12VDn04yXldJMcCAwEAAaNTMFEwHQYDVR0OBBYEFDcsmaoj3UiR3R54 +kDBgTy6KnVREMB8GA1UdIwQYMBaAFDcsmaoj3UiR3R54kDBgTy6KnVREMA8GA1Ud +EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABwyWeEUI+AUjsB3MBXOPpwj +Xhq3j0On00nz4w+QujvyACXVahPuQ8kxPqk9emnS8b0/+YBCh9yIDta1XiJ/46kl +ajiXzhlLFLUHEU9n5BJKEV6DYr47stYO/lZ+Q9dVtNFxtzVywI9WHY4JqWQ7W1D9 +T8VnKbLFu+IabNJLIQaKdOwWx9F3ybQtRufT+aHK04lC2zQco+yFCiq0Je0aa9Pl +dfLV/UNvYrNr56xwiRhfqFi+MHNz04jWpFal6q1NuI1bahyWbPhTzfXR+XrUZoS2 +3+xL61FvoWd57BWz8+Pid8bPoB/cYFU33mqWeH/LI1pSfhdOqP9X67bsH3MFZwQ= +-----END CERTIFICATE----- diff --git a/test/static/expired_certificate.key.pem b/test/static/expired_certificate.key.pem new file mode 100644 index 00000000..449468dc --- /dev/null +++ b/test/static/expired_certificate.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD6cd3vG0pLGiX0 +Klko6Z9PcQPxfmnKGy3YnbKWEnSzgRpFfaLtb4M62K1EXxXFY7P1NrOgs0deJKsx +NmRgIucXVDMS8sfoy7cfOdRU7U0BGZuAHmSHgu+yTXLvffMpNPoA+y6wHFlkNY93 ++4I9PnIlsuWI3nJPT8j0i6aqTWNBBctKAcFR2mXfap62K8HrCZj3rUPpXsWNdYNb +e9jscoenopmOVM+Emr4QAgAmEPY2qOZ8GbbSe3f7SWusR26NcKZj83t4YVTDKFVl +gPlm/Y8ZI4c6mRe4tMLt8M05NwED5n1N0DDzacCWh9u/Fk/QddYTjQshDwNdlQ59 +OMl5XSTHAgMBAAECggEAc9PR1tICTDWts/0Z+0gBPBaCwl+6wZRMYdCdVbb3bkWZ +RuZSQgm+4apwiByJzx7Lje9cqEgCC9Jdsob7aVL7GdkBPhQ2zL3a1YBDaXvOj2Gu +f1SPHfU6snYLYCQaH8a2kVmaQCz8UtJKpi0WEQkedb0FV4W5zGCUCjXEQSNFcj43 +iy8V8pIznTJoaTBtXjhI0uWGixTtlUgKXw3sTwkD+nJUXEe5fafJCBW0cFkswCG/ +O48oDsW9uG0cp9u6+zhTS9Y+cTjZtjyKPuAlmdEvA2vgP+pxkjt5E3ZdKD5wTUaM +w08O7vTM/TwgMjERZDt1OeItwKLVT/WIao+nXlKv8QKBgQD/xZrMjP4FKscK/bfl +Shk0Hg++3rxOcEL8OBymZt98ovKc1g0CxBVO8a+NPPIkUvZycq9hxrAT74Gzc2U3 +ilpTCw0Bnmzj6Hrwa+g0G92ptPWsMrRg1wyDY1XElHYJ6TmxV56J0XkI6iUx4BZS +lccLC3WGwwj/alw/1drtzP9x+wKBgQD6qwvHotsTU/LFRLwJgWvZfz1S5o66zIpr +pTxnrFSij+A5aHbp8mSSthMZWSKg0t5LLjykbDdlEzSl0YjQkfvETbAUvVzK3PSk +LF8DNA9rUuQXKyQzBBMUbMMQFn3mIUoc16M7v9D19E14AL1exSyPOOGeDaVXoH57 +6lfg/piqpQKBgHvbPPMA86Gc7XYtFvg5waqzQ/yx744sXsO0iGssNd0tKz83iGVm +fssTzmcetENSyXTyhGtcw7djq/MyVjlnDgZYu5ulFCXpVl9GYdOaCuU7dBxHEYIz +oSOe3tGq8t4pyn5OZ79laK8gc5KLaUPks9ZtXiQ8HgdRggqHjNTLCIgxAoGBAMOM +aDYnT+x2Eu/dvStVMYONBZQElNgY9OshDkx6XdQrlWpzmkDLfbYOIDwoEyGPHydb +PKewXE6XevzYx3ieSeBMEs87IoaHdLoWe1CObnD1S0bfuu+pgBDxAAMu6Kx8z8pM +VuUnsKYPHdg+C31BKI/aeffJAXGonMOif0fglcyZAoGBAKn0KUfzF85kAWXhbqAD +BaoNHB2R+FBTIbK62XT7ipVr7/ESO+BPFF3/no8fj7UcnLRfwJNqp7WgUzDjfhas +NS4Go8hlBdpRAZIeNZ2MuNRjyWOi57+UqJMiXNkfRLW0sxs1dpXap/C49Srb40Cp +FPFcndwuNUAbFlORtw3A97du +-----END PRIVATE KEY----- diff --git a/test/static/future_certificate.crt.pem b/test/static/future_certificate.crt.pem new file mode 100644 index 00000000..1a5822f0 --- /dev/null +++ b/test/static/future_certificate.crt.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC7TCCAdWgAwIBAgIUUYG5GJ7mlsFhulnT9/RkKyA8jNUwDQYJKoZIhvcNAQEL +BQAwHTEbMBkGA1UEAwwSRnV0dXJlIENlcnRpZmljYXRlMCIYDzk5OTkwMTAxMjMw +MDAwWhgPOTk5OTAxMDIyMzAwMDBaMB0xGzAZBgNVBAMMEkZ1dHVyZSBDZXJ0aWZp +Y2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKfCW4az2KHliibZ ++CxT5bgBOFAuGYFJBqN89lpGiZR1wIUSY4970S1yT1Mqspac16WpSuMPn5z6P+YL +9IMSWGhbM+AFKPoWgwpn8tdOT9zhSNO6cK27wP4p4CSt877TgKQwDsjZS/qpk+gZ +FNL2z5ZyppOoY9Fl1BwoqDF6LBykU+EjqWtSE5BZ47fcC+6YWOlu64ujwcy7c7y3 +qMAV1695grEYZeG0kIwzWtHGi5hGJsYLSREpy49c3M6OpzCRxTKdes1VJ70WavT3 ++gZE6bxDq/S5P1Jtcf9QAS1xM9kzIj8uQQQtfQqZbpF9x8iKNSbg3aMLz/KFj69d +xtThaekCAwEAAaMhMB8wHQYDVR0OBBYEFJWppJVYPR6ti8+6hYSJwSncPipSMA0G +CSqGSIb3DQEBCwUAA4IBAQBVC6INvRLzPVSK5F0YH07izKj7Ky8oumQ/an+G0/Yt +GJ52oRDTloHa012ad2weTLObIVlKGz10zRWZPB0IWcBOsMq7A2HE84pHjZpDiDXg +xgwTMOSlwBWqjK9r3fzK3jzz/zu8dRV0egvmu1bqK5hrquMuIWJIeUkvuNU8nS5g +lzk9M3dPo+fqFYujzxUtVrWc/MRg7gnsEUFWkZKdRmkJsdSWGxl2yc+jPo5WAXJA +kRrNigclD620Vnxji0JGd1EVrGHxoiwmG4Vq1yUrXv+7fcYB3mhK4nhkOLhjNqx3 +i8l/o/cqTCe8tXXuPeH+ymnKEAJsOGW0cH3kEComNQO9 +-----END CERTIFICATE----- diff --git a/test/static/future_certificate.csr.pem b/test/static/future_certificate.csr.pem new file mode 100644 index 00000000..9979127b --- /dev/null +++ b/test/static/future_certificate.csr.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICYjCCAUoCAQAwHTEbMBkGA1UEAwwSRnV0dXJlIENlcnRpZmljYXRlMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp8JbhrPYoeWKJtn4LFPluAE4UC4Z +gUkGo3z2WkaJlHXAhRJjj3vRLXJPUyqylpzXpalK4w+fnPo/5gv0gxJYaFsz4AUo ++haDCmfy105P3OFI07pwrbvA/ingJK3zvtOApDAOyNlL+qmT6BkU0vbPlnKmk6hj +0WXUHCioMXosHKRT4SOpa1ITkFnjt9wL7phY6W7ri6PBzLtzvLeowBXXr3mCsRhl +4bSQjDNa0caLmEYmxgtJESnLj1zczo6nMJHFMp16zVUnvRZq9Pf6BkTpvEOr9Lk/ +Um1x/1ABLXEz2TMiPy5BBC19CplukX3HyIo1JuDdowvP8oWPr13G1OFp6QIDAQAB +oAAwDQYJKoZIhvcNAQELBQADggEBABHJ+oTa5shXN7qNxfJVXznUMiujXlEqw6te +GMz14U/bZSllI42X3ipwdsn7GHHUPWN5STtJz534o4HdcSyKSzhO4Q9hcoKFE5xg +z1NeQSrBcX1UNgEL4+0xzSuVYJsNlz5qDEMFB/dOGtf+dSV7W+ljr3RPr9AeuDT4 +nkakHi6zbK5plEX295HY6GJxCCP7YZOJe3Iw3807L3rS+iQ+uwP/xiCSg25gzz/u +xNPWYRm99onM/4w+DlS8KHcjVyPJlurtsPxSdvFUxmOOusQYzG65xxZqazuBqVtC +qMDFVlYjazKNMU426JDmV41SR9yMa8XCIRYQmJuRN/4IH4g/Dfg= +-----END CERTIFICATE REQUEST----- diff --git a/test/static/future_certificate.key.pem b/test/static/future_certificate.key.pem new file mode 100644 index 00000000..386c5076 --- /dev/null +++ b/test/static/future_certificate.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCnwluGs9ih5Yom +2fgsU+W4AThQLhmBSQajfPZaRomUdcCFEmOPe9Etck9TKrKWnNelqUrjD5+c+j/m +C/SDElhoWzPgBSj6FoMKZ/LXTk/c4UjTunCtu8D+KeAkrfO+04CkMA7I2Uv6qZPo +GRTS9s+WcqaTqGPRZdQcKKgxeiwcpFPhI6lrUhOQWeO33AvumFjpbuuLo8HMu3O8 +t6jAFdeveYKxGGXhtJCMM1rRxouYRibGC0kRKcuPXNzOjqcwkcUynXrNVSe9Fmr0 +9/oGROm8Q6v0uT9SbXH/UAEtcTPZMyI/LkEELX0KmW6RfcfIijUm4N2jC8/yhY+v +XcbU4WnpAgMBAAECggEAFc/OaKgvjXUzzjNe8hyCbLcz5DDqPgYJp+4SddBgCP56 +ZpLqgPhfTSJkr/KIP87qtu5Y/0bDwPxEnJuHUhdriT36c7EYD9Qne43iZB4ZgiWE +e4rtJZmY0TMOopY/b9s+CZr6ASFHoLK1uWKxc3CFsxD7GY22VL6BopuiqrQw0hRR +rDOEGXwlJjlIdwz4istCaEsNAmaOPCQ6r+KdIw7cMdUKuG+lsxfTpC2QJtbiknI0 +WcNkWZ9zBxbTX+GpTWTLIM7rz2g96wuD+0ThCzteyWVffBBtIlCS3GOg+rdhWTYV +v2IbJZGG7qT5lOKlkvIUSMAiMd0fwCoIaPBybaQS0QKBgQDWSOKnMhQ10QuxqkL5 +Pr7dD+x6zkz8kaZCuYWXGevL3UYuBsBh50FeznKjqIAQRpz1v1Mgat9q4pI+DtOg +hqxreTsgAJfFSysaL4BuHUM606dqDEzeGz7Vavpnok1PgEzSAOBEDCo/xOHye2zE +evBy7nhMLhYwCcRNX/QhRnb8pQKBgQDIatKXDwX3ZK52yE4BuC1y+GFiL13m1BR5 +voHdFTwf7expYYmc95wud2KMqvEOT76tvMpNNkO/oESNbDVyjEUsXtykk5h7QJMq +1T50YgJQR2SAHVzPsWhR3NB/EJzBNW92gS+JysbtWgdkt3PunY58SMy0Xg6Kz1/u +XqYkoykg9QKBgQCgdZWbo6l0nyRFlvxtzal4ufrX/vGxU5OPdYLuog9q6jgqMQ4Q +ge32g1te58d16JqSfwFNThoc3Kqr48he9VnZZL98eFUt/Nq60gU275yvSVyc0bch +vn8vqtr1jZicxrM/sj49Vmqws8qKHBhXjMPPHHliekRNFpMzaX3TCQQCrQKBgGOT +v8JSMpKysYRPDYMJMXu4MRqJkkxH/0xl/TwNeuwaWKYbUjZtSGpF4u8lV9PWh1Tn +QlSOq6agSK9DnmKlkxDyqQoUU2SZtwVHIlrM/31Hm4WUETMYYE6cOfOIG3pbxF/K +3AXIfIIdgyLli3J5UfwqZ5sOSIdrdayH1mDJuHupAoGAcZJzimLVBHW9szrKFji1 +BkSUBaHvJExkXewJWaRjmkkV1GvWzbLU1vVM6/QxFy9kkrZk3dlYd60JW40Ap68n +ZvlfnArNw324nigu3Twc4b7ENYKUsimq3zprETeLw4BPojHPDTrQk+A/nhJZXXOo +QUJcxSlNR3iOhb7+BBwrI74= +-----END PRIVATE KEY----- diff --git a/test/utils-tests.spec.ts b/test/utils-tests.spec.ts index b5fe41aa..aa66598d 100644 --- a/test/utils-tests.spec.ts +++ b/test/utils-tests.spec.ts @@ -80,4 +80,60 @@ describe("Utils tests", function () { expect(() => utils.pemToDer("not a pem")).to.throw(); }); }); + + describe("findAttr", function () { + it("should find attribute with no namespace when null is passed as namespace", function () { + const xml = + ''; + const doc = new xmldom.DOMParser().parseFromString(xml); + const rootElement = doc.documentElement; + + const attr = utils.findAttr(rootElement, "testAttr", null); + + expect(attr).to.not.be.null; + expect(attr?.value).to.equal("value"); + expect(attr?.namespaceURI).to.be.undefined; + }); + + it("should not find namespaced attribute when null is passed as namespace", function () { + const xml = ''; + const doc = new xmldom.DOMParser().parseFromString(xml); + const rootElement = doc.documentElement; + + const attr = utils.findAttr(rootElement, "testAttr", null); + + expect(attr).to.be.null; + }); + + it("should find namespaced attribute when matching namespace is provided", function () { + const xml = ''; + const doc = new xmldom.DOMParser().parseFromString(xml); + const rootElement = doc.documentElement; + + const attr = utils.findAttr(rootElement, "testAttr", "http://example.com"); + + expect(attr).to.not.be.null; + expect(attr?.value).to.equal("nsValue"); + expect(attr?.namespaceURI).to.equal("http://example.com"); + }); + + it("should distinguish between namespaced and non-namespaced attributes with same localName", function () { + const xml = + ''; + const doc = new xmldom.DOMParser().parseFromString(xml); + const rootElement = doc.documentElement; + + // Find the non-namespaced attribute + const noNsAttr = utils.findAttr(rootElement, "testAttr", null); + expect(noNsAttr).to.not.be.null; + expect(noNsAttr?.value).to.equal("noNsValue"); + expect(noNsAttr?.namespaceURI).to.be.undefined; + + // Find the namespaced attribute + const nsAttr = utils.findAttr(rootElement, "testAttr", "http://example.com"); + expect(nsAttr).to.not.be.null; + expect(nsAttr?.value).to.equal("nsValue"); + expect(nsAttr?.namespaceURI).to.equal("http://example.com"); + }); + }); }); 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 index f3eca86e..2a832156 100644 --- a/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/program-repro-misc-validation-and-canon.cs +++ b/test/validators/XmlCryptoUtilities/XmlCryptoUtilities/program-repro-misc-validation-and-canon.cs @@ -1,6 +1,6 @@ // // This example signs an XML file using an -// envelope signature. It then verifies the +// envelope signature. It then verifies the // signed XML. // using System; @@ -66,7 +66,7 @@ static bool ValidateXml(XmlDocument receipt, X509Certificate2 certificate) } sxml.LoadXml((XmlElement)dsig); - + // Check the signature bool isValid = sxml.CheckSignature(certificate, true); @@ -91,13 +91,13 @@ static bool ValidateXml(XmlDocument receipt, X509Certificate2 certificate) 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); - + @@ -124,21 +124,21 @@ 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", + + FieldInfo field = t.GetType().GetField("_signaturePosition", BindingFlags.NonPublic | BindingFlags.Instance); - field.SetValue(t, 1); - + field.SetValue(t, 1); + var res = (XmlDocument)t.GetOutput(); var s = res.OuterXml; @@ -147,31 +147,31 @@ public static void Main(String[] args) var mem = (MemoryStream)c14.GetOutput(); var sha = new SHA256Managed(); - + var byte1 = c14.GetDigestedOutput(new SHA256Managed()); - var digest1 = Convert.ToBase64String(byte1); + 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 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 - + + + //validate signature + CryptoConfig.AddAlgorithm(typeof(RSAPKCS1SHA256SignatureDescription), "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"); - XmlDocument xmlDoc = new XmlDocument(); + 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); + bool isValid = ValidateXml(xmlDoc, cert); //return; - + //calc hash var sha1 = new SHA256Managed(); @@ -179,8 +179,8 @@ public static void Main(String[] args) 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 + // 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) { @@ -193,7 +193,7 @@ public static void SignXmlFile(string FileName, string SignedFileName, RSA Key) // Create a SignedXml object. SignedXml signedXml = new SignedXml(doc); - // Add the key to the SignedXml document. + // Add the key to the SignedXml document. signedXml.SigningKey = Key; // Create a reference to be signed. @@ -229,14 +229,14 @@ public static void SignXmlFile(string FileName, string SignedFileName, RSA Key) xmltw.Close(); } - // Verify the signature of an XML file against an asymetric + // 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. + // Load the passed XML file into the document. xmlDocument.Load(Name); // Create a new SignedXml object and pass it diff --git a/test/wsfed-metadata-tests.spec.ts b/test/wsfed-metadata-tests.spec.ts index 7ddea8e8..574bfc5d 100644 --- a/test/wsfed-metadata-tests.spec.ts +++ b/test/wsfed-metadata-tests.spec.ts @@ -1,4 +1,4 @@ -import { SignedXml } from "../src/index"; +import { SignedXml, XMLDSIG_URIS } from "../src"; import * as xpath from "xpath"; import * as xmldom from "@xmldom/xmldom"; import * as fs from "fs"; @@ -10,7 +10,7 @@ describe("WS-Fed Metadata tests", function () { const xml = fs.readFileSync("./test/static/wsfederation_metadata.xml", "utf-8"); const doc = new xmldom.DOMParser().parseFromString(xml); const signature = xpath.select1( - "/*/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", + `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, ); isDomNode.assertIsNodeLike(signature); diff --git a/test/xmldsig-verifier.spec.ts b/test/xmldsig-verifier.spec.ts new file mode 100644 index 00000000..4e025a0d --- /dev/null +++ b/test/xmldsig-verifier.spec.ts @@ -0,0 +1,999 @@ +import * as fs from "fs"; +import { expect } from "chai"; +import { XmlDSigVerifier, SignedXml, ExclusiveCanonicalization } from "../src"; +import { RsaSha1 } from "../src/signature-algorithms"; +import { Sha1 } from "../src/hash-algorithms"; +import { EnvelopedSignature } from "../src/enveloped-signature"; +import { XMLDSIG_URIS, XmlDsigVerificationResult } from "../src/"; + +import { X509Certificate } from "node:crypto"; + +// Parse the XML and get both signature nodes +import { DOMParser } from "@xmldom/xmldom"; + +const { CANONICALIZATION_ALGORITHMS, HASH_ALGORITHMS, SIGNATURE_ALGORITHMS, TRANSFORM_ALGORITHMS } = + XMLDSIG_URIS; + +// Default test certificate files +const privateKey = fs.readFileSync("./test/static/client.pem", "utf-8"); +const publicCert = fs.readFileSync("./test/static/client_public.pem", "utf-8"); + +// Chain certificate files for truststore testing +const chainPrivateKey = fs.readFileSync("./test/static/chain_client.key.pem", "utf-8"); +const chainPublicCert = fs.readFileSync("./test/static/chain_client.crt.pem", "utf-8"); +const rootCert = fs.readFileSync("./test/static/chain_root.crt.pem", "utf-8"); + +// Expired certificate for testing certificate expiration validation +const expiredKey = fs.readFileSync("./test/static/expired_certificate.key.pem", "utf-8"); +const expiredCert = fs.readFileSync("./test/static/expired_certificate.crt.pem", "utf-8"); + +// Future certificate for testing certificate validity period validation +const futureKey = fs.readFileSync("./test/static/future_certificate.key.pem", "utf-8"); +const futureCert = fs.readFileSync("./test/static/future_certificate.crt.pem", "utf-8"); + +// Helper function to create a signed XML document +function createSignedXml( + xml: string, + options: { prefix?: string; attrs?: Record } = {}, +): string { + const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + }); + + sig.addReference({ + xpath: "//*[local-name(.)='test']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + + sig.computeSignature(xml, options); + return sig.getSignedXml(); +} + +// Helper function to create a signed XML document for truststore testing +function createChainSignedXml(xml: string): string { + const sig = new SignedXml({ + privateKey: chainPrivateKey, + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + getKeyInfoContent: () => SignedXml.getKeyInfoContent({ publicCert: chainPublicCert }), + }); + + sig.addReference({ + xpath: "//*[local-name(.)='test']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + + sig.computeSignature(xml); + return sig.getSignedXml(); +} + +function createExpiredSignedXml(xml: string): string { + const sig = new SignedXml({ + privateKey: expiredKey, + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + getKeyInfoContent: () => SignedXml.getKeyInfoContent({ publicCert: expiredCert }), + }); + + sig.addReference({ + xpath: "//*[local-name(.)='test']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + + sig.computeSignature(xml); + return sig.getSignedXml(); +} + +function createFutureSignedXml(xml: string): string { + const sig = new SignedXml({ + privateKey: futureKey, + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + getKeyInfoContent: () => SignedXml.getKeyInfoContent({ publicCert: futureCert }), + }); + + sig.addReference({ + xpath: "//*[local-name(.)='test']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + + sig.computeSignature(xml); + return sig.getSignedXml(); +} + +function expectValidResult(result: XmlDsigVerificationResult, references: number = 1) { + expect(result.success).to.be.true; + expect(result.error).to.be.undefined; + expect(result.signedReferences).to.be.an("array"); + expect(result.signedReferences).to.have.length(references); +} + +function expectInvalidResult(result: XmlDsigVerificationResult, errorMessage?: string) { + expect(result.success).to.be.false; + expect(result.signedReferences).to.be.undefined; + expect(result.error).to.be.a("string"); + if (errorMessage && result.error) { + expect(result.error.toLowerCase()).to.contain(errorMessage.toLowerCase()); + } +} + +describe("XmlDSigVerifier", function () { + const xml = "content"; + + describe("constructor", function () { + it("should create verifier with public certificate", function () { + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + }); + expect(verifier).to.be.instanceOf(XmlDSigVerifier); + }); + + it("should create verifier with getCertFromKeyInfo function", function () { + const verifier = new XmlDSigVerifier({ + keySelector: { + getCertFromKeyInfo: () => publicCert, + }, + }); + expect(verifier).to.be.instanceOf(XmlDSigVerifier); + }); + + it("should throw when trying to create a verifier without publicCert or getCertFromKeyInfo", function () { + expect(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new XmlDSigVerifier({ keySelector: {} as any }); + }).to.throw("XmlDSigVerifier requires a valid keySelector option"); + }); + + it("should create verifier with all options set", function () { + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + idAttributes: ["customId"], + implicitTransforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + throwOnError: true, + security: { + maxTransforms: 5, + checkCertExpiration: true, + truststore: [rootCert], + signatureAlgorithms: SignedXml.getDefaultAsymmetricSignatureAlgorithms(), + hashAlgorithms: SignedXml.getDefaultHashAlgorithms(), + transformAlgorithms: SignedXml.getDefaultTransformAlgorithms(), + }, + }); + expect(verifier).to.be.instanceOf(XmlDSigVerifier); + }); + + it("should throw when getCertFromKeyInfo is undefined", function () { + expect(() => { + new XmlDSigVerifier({ + keySelector: { + getCertFromKeyInfo: undefined as never, + }, + }); + }).to.throw("XmlDSigVerifier requires a valid getCertFromKeyInfo function."); + }); + + it("should throw when getCertFromKeyInfo is set to publicCert string directly", function () { + expect(() => { + new XmlDSigVerifier({ + keySelector: { + getCertFromKeyInfo: publicCert as never, + }, + }); + }).to.throw("XmlDSigVerifier requires a valid getCertFromKeyInfo function."); + }); + }); + + describe("publicCert selector", function () { + it("should validate a valid signed XML document", function () { + const signedXml = createSignedXml(xml); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should validate when publicCert is a buffer", function () { + const signedXml = createSignedXml(xml); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert: Buffer.from(publicCert) }, + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should fail validation when document is signed with different key", function () { + const signedXml = createChainSignedXml(xml); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + throwOnError: false, + }); + + expectInvalidResult(verifier.verifySignature(signedXml), "invalid signature"); + }); + }); + + describe("getCertFromKeyInfo selector", function () { + it("should validate a valid signed XML document", function () { + const signedXml = createSignedXml(xml); + + const verifier = new XmlDSigVerifier({ + keySelector: { + getCertFromKeyInfo: () => publicCert, + }, + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should fail validation when document is signed with different key", function () { + const signedXml = createChainSignedXml(xml); + + const verifier = new XmlDSigVerifier({ + keySelector: { + getCertFromKeyInfo: () => publicCert, + }, + throwOnError: false, + }); + + expectInvalidResult(verifier.verifySignature(signedXml), "invalid signature"); + }); + + it("should fail validation when getCertFromKeyInfo returns null", function () { + const signedXml = createSignedXml(xml); + + const verifier = new XmlDSigVerifier({ + keySelector: { + getCertFromKeyInfo: () => null, + }, + throwOnError: false, + }); + + expectInvalidResult(verifier.verifySignature(signedXml), "keyinfo"); + }); + + it("should fail validation when getCertFromKeyInfo returns empty string", function () { + const signedXml = createSignedXml(xml); + + const verifier = new XmlDSigVerifier({ + keySelector: { + getCertFromKeyInfo: () => "", + }, + throwOnError: false, + }); + + expectInvalidResult(verifier.verifySignature(signedXml), "keyinfo"); + }); + }); + + describe("idAttributes option", function () { + const xmlWithCustomId = 'content'; + const xmlWithPrefixedId = `content`; + + it("should validate a valid signed XML document with custom Id", function () { + const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + idAttributes: ["customId"], + }); + sig.addReference({ + xpath: "//*[@customId='test1']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig.computeSignature(xmlWithCustomId); + const signedXml = sig.getSignedXml(); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + idAttributes: ["customId"], + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should validate a valid signed XML document with prefixed Id", function () { + const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + idAttributes: ["customId"], + }); + sig.addReference({ + xpath: "//*[@*[namespace-uri() = 'uri:foo' and local-name() = 'customId']]", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig.computeSignature(xmlWithPrefixedId); + const signedXml = sig.getSignedXml(); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + idAttributes: ["customId"], + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should work with explicitly namespaced Id attributes", function () { + const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + idAttributes: ["customId"], + }); + sig.addReference({ + xpath: "//*[@*[namespace-uri() = 'uri:foo' and local-name() = 'customId']]", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig.computeSignature(xmlWithPrefixedId); + const signedXml = sig.getSignedXml(); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + idAttributes: [{ localName: "customId", namespaceUri: "uri:foo" }], + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should fail validation when Id attribute is not in the correct namespace", function () { + const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + idAttributes: ["customId"], + }); + sig.addReference({ + xpath: "//*[@*[namespace-uri() = 'uri:foo' and local-name() = 'customId']]", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig.computeSignature(xmlWithPrefixedId); + const signedXml = sig.getSignedXml(); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + idAttributes: [{ localName: "customId", namespaceUri: "uri:bar" }], + throwOnError: false, + }); + expectInvalidResult(verifier.verifySignature(signedXml), "fail"); + }); + + it("should fail validation when Id attribute is not namespaced but namespaceUri is provided", function () { + const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + idAttributes: ["customId"], + }); + sig.addReference({ + xpath: "//*[@customId='test1']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig.computeSignature(xmlWithCustomId); + const signedXml = sig.getSignedXml(); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + idAttributes: [{ localName: "customId", namespaceUri: "uri:foo" }], + throwOnError: false, + }); + expectInvalidResult(verifier.verifySignature(signedXml), "fail"); + }); + + describe("idAttributes property handling", function () { + const xmlIdAttrNoNs = 'content'; + const xmlIdAttrWithNs = + 'content'; + const xmlIdAttrOtherNs = + 'content'; + + it("should validate when idAttributes is a string (matches non-namespaced ID attribute)", function () { + // Create signature for no-namespace XML + const sig = new SignedXml({ + privateKey, + idAttributes: ["customId"], + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + }); + sig.addReference({ + xpath: "//*[local-name(.)='test']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig.computeSignature(xmlIdAttrNoNs); + const signedXml = sig.getSignedXml(); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + idAttributes: ["customId"], + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should validate namespaced attribute when idAttributes is a string (matches namespaced ID attribute)", function () { + const sig = new SignedXml({ + privateKey, + idAttributes: ["customId"], + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + }); + sig.addReference({ + xpath: "//*[local-name(.)='test']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig.computeSignature(xmlIdAttrWithNs); + const signedXml = sig.getSignedXml(); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + idAttributes: ["customId"], + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should validate when `idAttributes` `namespaceUri` is `undefined` (matches namespaced ID attribute)", function () { + const sig = new SignedXml({ + privateKey, + idAttributes: [{ localName: "customId", namespaceUri: undefined }], + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + }); + sig.addReference({ + xpath: "//*[local-name(.)='test']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig.computeSignature(xmlIdAttrWithNs); + const signedXml = sig.getSignedXml(); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + idAttributes: [{ localName: "customId", namespaceUri: undefined }], + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should validate when `idAttributes` `namespaceUri` is `null` (matches non-namespaced ID attribute)", function () { + const sig = new SignedXml({ + privateKey, + idAttributes: [{ localName: "customId", namespaceUri: null }], + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + }); + sig.addReference({ + xpath: "//*[local-name(.)='test']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig.computeSignature(xmlIdAttrNoNs); + const signedXml = sig.getSignedXml(); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + idAttributes: [{ localName: "customId", namespaceUri: null }], + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should fail validation when `idAttributes` `namespaceUri` is `null` but ID attribute is namespaced (should be excluded)", function () { + const sig = new SignedXml({ + privateKey, + idAttributes: ["customId"], // Sign it loosely so it signs + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + }); + sig.addReference({ + xpath: "//*[local-name(.)='test']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig.computeSignature(xmlIdAttrWithNs); + const signedXml = sig.getSignedXml(); + + // Verifier expects NO namespace, but XML has one + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + idAttributes: [{ localName: "customId", namespaceUri: null }], + throwOnError: false, + }); + + // Should fail because it can't find the reference target with the strict criteria + expectInvalidResult(verifier.verifySignature(signedXml), "verification failed"); + }); + + it("should validate when `idAttributes` `namespaceUri` is a string and matches the ID attribute's namespace", function () { + const sig = new SignedXml({ + privateKey, + idAttributes: [{ localName: "customId", namespaceUri: "urn:test" }], + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + }); + sig.addReference({ + xpath: "//*[local-name(.)='test']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig.computeSignature(xmlIdAttrWithNs); + const signedXml = sig.getSignedXml(); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + idAttributes: [{ localName: "customId", namespaceUri: "urn:test" }], + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should fail validation when `idAttributes` `namespaceUri` is a string but ID attribute has a different namespace (should be excluded)", function () { + const sig = new SignedXml({ + privateKey, + idAttributes: ["customId"], // Sign loosely + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + }); + sig.addReference({ + xpath: "//*[local-name(.)='test']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig.computeSignature(xmlIdAttrOtherNs); + const signedXml = sig.getSignedXml(); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + idAttributes: [{ localName: "customId", namespaceUri: "urn:test" }], + throwOnError: false, + }); + + expectInvalidResult(verifier.verifySignature(signedXml), "verification failed"); + }); + }); + }); + + describe("throwOnError option", function () { + it("should throw validation errors when throwOnError is true", function () { + const signedXml = createSignedXml(xml); + const tamperedXml = signedXml.replace("content", "tampered"); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + throwOnError: true, + }); + + expect(() => verifier.verifySignature(tamperedXml)).to.throw("verification failed"); + }); + + it("should return error details when throwOnError is false", function () { + const signedXml = createSignedXml(xml); + const tamperedXml = signedXml.replace("content", "tampered"); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + throwOnError: false, + }); + + const result = verifier.verifySignature(tamperedXml); + expectInvalidResult(result, "verification failed"); + }); + }); + + describe("security options", function () { + describe("maxTransforms", function () { + it("should validate when number of transforms is within maxTransforms", function () { + const signedXml = createSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + security: { maxTransforms: 1 }, + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should fail validation when number of transforms exceeds maxTransforms", function () { + const sig = new SignedXml({ + privateKey, + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + }); + sig.addReference({ + xpath: "//*[local-name(.)='test']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [ + CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE, + ], + }); + sig.computeSignature(xml); + const signedXml = sig.getSignedXml(); + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + security: { maxTransforms: 1 }, + }); + expectInvalidResult(verifier.verifySignature(signedXml), "exceeds the maximum allowed"); + }); + }); + + describe("checkCertExpiration", function () { + it("should validate when certificate is not expired and checkCertExpiration is true", function () { + const signedXml = createSignedXml(xml); + // @ts-expect-error -- ignore for test purposes + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + security: { checkCertExpiration: true }, + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should validate when certificate is expired and checkCertExpiration is false", function () { + const signedXml = createExpiredSignedXml(xml); + // @ts-expect-error -- ignore for test purposes + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert: expiredCert }, + security: { checkCertExpiration: false }, + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should fail validation when certificate is expired and checkCertExpiration is true", function () { + const signedXml = createExpiredSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { getCertFromKeyInfo: () => expiredCert }, + security: { checkCertExpiration: true }, + }); + expectInvalidResult(verifier.verifySignature(signedXml), "expired"); + }); + + it("should fail validation when certificate is not yet valid and checkCertExpiration is true", function () { + const signedXml = createFutureSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { getCertFromKeyInfo: () => futureCert }, + security: { checkCertExpiration: true }, + }); + expectInvalidResult(verifier.verifySignature(signedXml), "not yet valid"); + }); + }); + + describe("truststore", function () { + it("should validate when certificate is exactly in truststore", function () { + const signedXml = createSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { getCertFromKeyInfo: () => publicCert }, + security: { truststore: [publicCert] }, + }); + + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should validate when certificate is trusted", function () { + const signedXml = createChainSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { getCertFromKeyInfo: () => chainPublicCert }, + security: { truststore: [rootCert] }, + }); + + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should validate when X509Certificate is directly passed into truststore", function () { + const signedXml = createChainSignedXml(xml); + const rootX509 = new X509Certificate(rootCert); + + const verifier = new XmlDSigVerifier({ + keySelector: { getCertFromKeyInfo: () => chainPublicCert }, + security: { truststore: [rootX509] }, + }); + + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should fail validation when certificate is not trusted", function () { + const signedXml = createSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { getCertFromKeyInfo: () => publicCert }, + security: { truststore: [rootCert] }, + }); + expectInvalidResult(verifier.verifySignature(signedXml), "not trusted"); + }); + + it("should validate truststore even when checkCertExpiration is false", function () { + const signedXml = createChainSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { getCertFromKeyInfo: () => chainPublicCert }, + security: { + checkCertExpiration: false, + truststore: [rootCert], + }, + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should validate when checkCertExpiration is false and no truststore is provided", function () { + const signedXml = createSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { getCertFromKeyInfo: () => publicCert }, + security: { + checkCertExpiration: false, + truststore: [], + }, + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + }); + + describe("signatureAlgorithms", function () { + it("should validate when signature algorithm is allowed", function () { + const signedXml = createSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + security: { signatureAlgorithms: SignedXml.getDefaultAsymmetricSignatureAlgorithms() }, + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should fail validation when signature algorithm is not allowed", function () { + const signedXml = createSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + security: { signatureAlgorithms: { foo: RsaSha1 } }, + }); + expectInvalidResult(verifier.verifySignature(signedXml), "signature algorithm"); + }); + }); + + describe("hashAlgorithms", function () { + it("should validate when hash algorithm is allowed", function () { + const signedXml = createSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + security: { hashAlgorithms: SignedXml.getDefaultHashAlgorithms() }, + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should fail validation when hash algorithm is not allowed", function () { + const signedXml = createSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + security: { hashAlgorithms: { foo: Sha1 } }, + }); + expectInvalidResult(verifier.verifySignature(signedXml), "hash algorithm"); + }); + }); + + describe("transformAlgorithms", function () { + it("should validate when transform algorithms are allowed", function () { + const signedXml = createSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + security: { transformAlgorithms: SignedXml.getDefaultTransformAlgorithms() }, + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should fail validation when a transform algorithm is not allowed", function () { + const signedXml = createSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + security: { transformAlgorithms: { foo: EnvelopedSignature } }, + }); + expectInvalidResult(verifier.verifySignature(signedXml), "transform algorithm"); + }); + }); + + describe("canonicalizationAlgorithms", function () { + it("should validate when canonicalization algorithms are allowed", function () { + const signedXml = createSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + security: { + canonicalizationAlgorithms: SignedXml.getDefaultCanonicalizationAlgorithms(), + }, + }); + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("should fail validation when a canonicalization algorithm is not allowed", function () { + const signedXml = createSignedXml(xml); + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + security: { canonicalizationAlgorithms: { foo: ExclusiveCanonicalization } }, + }); + expectInvalidResult(verifier.verifySignature(signedXml), "canonicalization algorithm"); + }); + }); + }); + + describe("signatureNode parameter", function () { + it("should fail when XML has no signatures", function () { + const unsignedXml = "content"; + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + throwOnError: false, + }); + + const result = verifier.verifySignature(unsignedXml); + expectInvalidResult(result, "No Signature element found"); + }); + + it("should validate when signatureNode is provided directly", function () { + const signedXml = createSignedXml(xml); + const doc = new DOMParser().parseFromString(signedXml, "application/xml"); + const signatureNode = doc.getElementsByTagNameNS(XMLDSIG_URIS.NAMESPACES.ds, "Signature")[0]; + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + }); + + const result = verifier.verifySignature(signedXml, signatureNode); + expectValidResult(result); + }); + + it("should fail when XML has multiple signatures but no signatureNode is specified", function () { + // Create XML with two different test elements + const xmlWithTwoElements = + "content1content2"; + + // Create first signature for first test element + const sig1 = new SignedXml({ + privateKey, + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + }); + sig1.addReference({ + xpath: "//*[@id='1']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig1.computeSignature(xmlWithTwoElements, { + location: { reference: "/root", action: "append" }, + }); + const xmlWithFirstSig = sig1.getSignedXml(); + + // Create second signature for second test element + const sig2 = new SignedXml({ + privateKey: chainPrivateKey, + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + }); + sig2.addReference({ + xpath: "//*[@id='2']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig2.computeSignature(xmlWithFirstSig, { + location: { reference: "/root", action: "append" }, + }); + const xmlWithTwoSigs = sig2.getSignedXml(); + + const verifier = new XmlDSigVerifier({ + keySelector: { publicCert }, + throwOnError: false, + }); + + const result = verifier.verifySignature(xmlWithTwoSigs); + expectInvalidResult(result, "Multiple Signature elements found"); + }); + + it("should validate specific signature when XML has multiple signatures", function () { + // Create XML with two different test elements + const xmlWithTwoElements = + "content1content2"; + + // Create first signature for first test element + const sig1 = new SignedXml({ + privateKey, + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + }); + sig1.addReference({ + xpath: "//*[@id='1']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig1.computeSignature(xmlWithTwoElements, { + location: { reference: "/root", action: "append" }, + }); + const xmlWithFirstSig = sig1.getSignedXml(); + + // Create second signature for second test element + const sig2 = new SignedXml({ + privateKey: chainPrivateKey, // Use different key for second signature + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + }); + sig2.addReference({ + xpath: "//*[@id='2']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + sig2.computeSignature(xmlWithFirstSig, { + location: { reference: "/root", action: "append" }, + }); + const xmlWithTwoSigs = sig2.getSignedXml(); + const doc = new DOMParser().parseFromString(xmlWithTwoSigs, "application/xml"); + const signatureNodes = doc.getElementsByTagNameNS(XMLDSIG_URIS.NAMESPACES.ds, "Signature"); + + expect(signatureNodes.length).to.equal(2); + + // Verify first signature with first key + const verifier1 = new XmlDSigVerifier({ + keySelector: { publicCert }, + }); + const result1 = verifier1.verifySignature(xmlWithTwoSigs, signatureNodes[0]); + expectValidResult(result1); + + // Verify second signature with second key + const verifier2 = new XmlDSigVerifier({ + keySelector: { publicCert: chainPublicCert }, + }); + const result2 = verifier2.verifySignature(xmlWithTwoSigs, signatureNodes[1]); + expectValidResult(result2); + }); + }); + + describe("static verifySignature method", function () { + it("should return success result when throwOnError is false and no error occurs", function () { + const signedXml = createSignedXml(xml); + + const result = XmlDSigVerifier.verifySignature(signedXml, { + keySelector: { publicCert }, + throwOnError: false, + }); + + expectValidResult(result); + }); + + it("should return error result when throwOnError is false and error occurs", function () { + const signedXml = createSignedXml(xml); + const tamperedXml = signedXml.replace("content", "tampered"); + + const result = XmlDSigVerifier.verifySignature(tamperedXml, { + keySelector: { publicCert }, + throwOnError: false, + }); + + expectInvalidResult(result, "verification failed"); + }); + + it("should return success result when throwOnError is true and no error occurs", function () { + const signedXml = createSignedXml(xml); + + const result = XmlDSigVerifier.verifySignature(signedXml, { + keySelector: { publicCert }, + throwOnError: true, + }); + + expectValidResult(result); + }); + + it("should throw error when throwOnError is true and error occurs", function () { + const signedXml = createSignedXml(xml); + const tamperedXml = signedXml.replace("content", "tampered"); + + expect(() => { + XmlDSigVerifier.verifySignature(tamperedXml, { + keySelector: { publicCert }, + throwOnError: true, + }); + }).to.throw("verification failed"); + }); + + it("should use default throwOnError (false) when not explicitly provided", function () { + const result = XmlDSigVerifier.verifySignature("content", { + keySelector: { + getCertFromKeyInfo: null as never, + }, + }); + + expectInvalidResult(result, "XmlDSigVerifier requires a valid getCertFromKeyInfo function."); + }); + }); +}); From 92eb4178fe35273d661b3f9cf6e358f0cd9fc0a2 Mon Sep 17 00:00:00 2001 From: shunkica Date: Sun, 28 Dec 2025 13:06:53 +0100 Subject: [PATCH 02/12] fix: Support nested enveloped signature location (#525) Update XPath query to find Signature elements at any depth within the document, not just direct children. This fixes an issue where signatures nested within other elements were not properly detected and removed. --- src/enveloped-signature.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/enveloped-signature.ts b/src/enveloped-signature.ts index 389a0627..8dd1226a 100644 --- a/src/enveloped-signature.ts +++ b/src/enveloped-signature.ts @@ -17,7 +17,7 @@ export class EnvelopedSignature implements TransformAlgorithm { process(node: Node, options: TransformAlgorithmOptions): Node { if (null == options.signatureNode) { const signature = xpath.select1( - `./*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, + `.//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, node, ); if (isDomNode.isNodeLike(signature) && signature.parentNode) { From 23d8b147234f95e24df25112236b0c85493af7c9 Mon Sep 17 00:00:00 2001 From: shunkica Date: Thu, 5 Mar 2026 17:43:43 +0100 Subject: [PATCH 03/12] refactor: clean up types and use array-based algorithm registration Remove backward-compatibility type aliases: - CanonicalizationOrTransformationAlgorithm - CanonicalizationOrTransformAlgorithmType - CanonicalizationAlgorithmType - SignatureAlgorithmType - HashAlgorithmType - CanonicalizationOrTransformationAlgorithmProcessOptions Replace all internal usages with canonical names and remove associated eslint-disable comments and TODO markers. Replace map-based algorithm options (Record) with simple constructor arrays in XmlDSigVerifierSecurityOptions. Each class provides its own URI via getAlgorithmName(), eliminating redundant keys and risk of URI mismatches. - Add static default algorithm arrays on XmlDSigVerifier - Add private toAlgorithmMap helper for internal conversion - Export algorithm classes (Sha*, Rsa*, HmacSha1, EnvelopedSignature) - Update tests and XMLDSIG_VERIFIER.md documentation --- XMLDSIG_VERIFIER.md | 148 ++++++++++++++++++++++++++++++---- src/enveloped-signature.ts | 10 +-- src/index.ts | 3 + src/signed-xml.ts | 9 +-- src/types.ts | 79 +++++------------- src/xmldsig-verifier.ts | 93 +++++++++++++++++---- test/xmldsig-verifier.spec.ts | 31 ++++--- 7 files changed, 255 insertions(+), 118 deletions(-) diff --git a/XMLDSIG_VERIFIER.md b/XMLDSIG_VERIFIER.md index 14fb938d..32a1e045 100644 --- a/XMLDSIG_VERIFIER.md +++ b/XMLDSIG_VERIFIER.md @@ -6,7 +6,9 @@ - **Type-Safe Configuration:** Explicit options for different key retrieval strategies (Public Certificate, KeyInfo, Shared Secret). - **Enhanced Security:** Built-in checks for certificate expiration, truststore validation, and limits on transform complexity. +- **Algorithm Allow-Lists:** Restrict which signature, hash, transform, and canonicalization algorithms are accepted. - **Flexible Error Handling:** Choose between throwing errors or returning a result object. +- **Reusable Instances:** Create a verifier once and use it to verify multiple documents. ## Installation @@ -16,6 +18,42 @@ Ensure you have `xml-crypto` installed: npm install xml-crypto ``` +## Imports + +```typescript +import { + XmlDSigVerifier, + XMLDSIG_URIS, + // Algorithm classes (for customizing allowed algorithms) + Sha1, + Sha256, + Sha512, + RsaSha1, + RsaSha256, + RsaSha256Mgf1, + RsaSha512, + HmacSha1, + EnvelopedSignature, + C14nCanonicalization, + C14nCanonicalizationWithComments, + ExclusiveCanonicalization, + ExclusiveCanonicalizationWithComments, + // Types (optional, for TypeScript users) + type XmlDSigVerifierOptions, + type XmlDsigVerificationResult, +} from "xml-crypto"; +``` + +`XMLDSIG_URIS` provides constants for all supported algorithm URIs: + +```typescript +XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA256; // "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" +XMLDSIG_URIS.HASH_ALGORITHMS.SHA256; // "http://www.w3.org/2001/04/xmlenc#sha256" +XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; // "http://www.w3.org/2001/10/xml-exc-c14n#" +XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE; // "http://www.w3.org/2000/09/xmldsig#enveloped-signature" +XMLDSIG_URIS.NAMESPACES.ds; // "http://www.w3.org/2000/09/xmldsig#" +``` + ## Quick Start ### 1. Verifying with a Public Certificate @@ -73,16 +111,33 @@ if (result.success) { } ``` +### 3. Verifying with a Shared Secret (HMAC) + +For documents signed with HMAC (symmetric key): + +```typescript +import { XmlDSigVerifier } from "xml-crypto"; + +const result = XmlDSigVerifier.verifySignature(xml, { + keySelector: { sharedSecretKey: "my-shared-secret" }, +}); + +if (result.success) { + console.log("HMAC signature valid!"); +} +``` + +Note: When using `sharedSecretKey`, HMAC signature algorithms are enabled and asymmetric algorithms are disabled by default to prevent key confusion attacks. + ## Advanced Usage ### Reusing the Verifier Instance -For better performance when verifying multiple documents with the same configuration, create an instance of `XmlDSigVerifier`. +For better performance when verifying multiple documents with the same configuration, create an instance of `XmlDSigVerifier`: ```typescript const verifier = new XmlDSigVerifier({ keySelector: { publicCert: myPublicCert }, - // Global security options security: { maxTransforms: 2 }, }); @@ -98,28 +153,73 @@ The `verifySignature` method accepts an options object with the following struct interface XmlDSigVerifierOptions { // STRATEGY: Choose one of the following key selectors keySelector: - | { publicCert: string | Buffer } // Direct public key/cert - | { getCertFromKeyInfo: (node) => string | null } // Extract from XML - | { sharedSecretKey: string | Buffer }; // HMAC + | { publicCert: KeyLike } // Direct public key/cert + | { getCertFromKeyInfo: (node?: Node | null) => string | null } // Extract from XML + | { sharedSecretKey: KeyLike }; // HMAC shared secret // CONFIGURATION - idAttributes?: string[]; // e.g., ['Id', 'ID'] + idAttributes?: VerificationIdAttributeType[]; // Default: ["Id", "ID", "id"] + implicitTransforms?: ReadonlyArray; // Hidden transforms to apply throwOnError?: boolean; // Default: false (returns result object) // SECURITY security?: { - maxTransforms?: number; // Limit transforms (DoS protection) - checkCertExpiration?: boolean; // Check NotBefore/NotAfter (KeyInfo only) - truststore?: (string | Buffer)[]; // List of trusted CAs (KeyInfo only) - - // Algorithm allow-lists - signatureAlgorithms?: Record; - hashAlgorithms?: Record; - // ... + maxTransforms?: number; // Limit transforms per reference (DoS protection). Default: 4 + signatureAlgorithms?: Array SignatureAlgorithm>; // Allowed signature algorithms + hashAlgorithms?: Array HashAlgorithm>; // Allowed hash algorithms + transformAlgorithms?: Array TransformAlgorithm>; // Allowed transform algorithms + canonicalizationAlgorithms?: Array CanonicalizationAlgorithm>; // Allowed canonicalization algorithms + + // KeyInfo-only options (only available with getCertFromKeyInfo selector): + checkCertExpiration?: boolean; // Check NotBefore/NotAfter. Default: true + truststore?: Array; // Trusted CA certificates }; } ``` +#### Extending Defaults with Custom Algorithms + +Algorithm lists are simple arrays of constructor classes. Each class provides its own URI via `getAlgorithmName()`, eliminating the need for manual URI keys. To add a custom algorithm alongside the defaults, spread the default array: + +```typescript +import { XmlDSigVerifier } from "xml-crypto"; +import { MyCustomHashAlgorithm } from "./my-algorithms"; + +const result = XmlDSigVerifier.verifySignature(xml, { + keySelector: { publicCert }, + security: { + hashAlgorithms: [...XmlDSigVerifier.defaultHashAlgorithms, MyCustomHashAlgorithm], + }, +}); +``` + +### ID Attributes + +The `idAttributes` option controls which XML attributes are treated as element identifiers when resolving signature references. + +```typescript +// Simple string format (matches attribute name in any namespace) +idAttributes: ["Id", "ID", "customId"]; + +// Namespaced format for stricter matching +idAttributes: [ + { localName: "Id", namespaceUri: "http://example.com/ns" }, // Match only in specific namespace + { localName: "Id", namespaceUri: null }, // Match only non-namespaced attributes + { localName: "Id" }, // Match regardless of namespace (same as string) +]; +``` + +### Implicit Transforms + +If you fail to verify signed XML, one possible cause is hidden implicit transforms that were applied during signing but not listed in the signature. Use `implicitTransforms` to specify them: + +```typescript +const result = XmlDSigVerifier.verifySignature(xml, { + keySelector: { publicCert }, + implicitTransforms: [XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.C14N], +}); +``` + ### Error Handling By default, `verifySignature` returns a result object. If you prefer to handle exceptions: @@ -136,6 +236,24 @@ try { } ``` +### Result Types + +The verification result is a discriminated union: + +```typescript +// On success +{ + success: true, + signedReferences: string[] // Canonicalized XML content that was signed +} + +// On failure (when throwOnError is false) +{ + success: false, + error: string // Description of what went wrong +} +``` + ### Handling Multiple Signatures If a document contains multiple signatures, you must specify which one to verify by passing the signature node. @@ -144,7 +262,7 @@ If a document contains multiple signatures, you must specify which one to verify import { DOMParser } from "@xmldom/xmldom"; const doc = new DOMParser().parseFromString(xml, "application/xml"); -const signatures = doc.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature"); +const signatures = doc.getElementsByTagNameNS(XMLDSIG_URIS.NAMESPACES.ds, "Signature"); // Verify the second signature const result = XmlDSigVerifier.verifySignature( diff --git a/src/enveloped-signature.ts b/src/enveloped-signature.ts index 8dd1226a..95b2f003 100644 --- a/src/enveloped-signature.ts +++ b/src/enveloped-signature.ts @@ -1,11 +1,7 @@ import * as xpath from "xpath"; import * as isDomNode from "@xmldom/is-dom-node"; import { XMLDSIG_URIS } from "./xmldsig-uris"; -import type { - TransformAlgorithmOptions, - CanonicalizationOrTransformAlgorithmType, - TransformAlgorithm, -} from "./types"; +import type { TransformAlgorithmOptions, TransformAlgorithmURI, TransformAlgorithm } from "./types"; export class EnvelopedSignature implements TransformAlgorithm { protected includeComments = false; @@ -55,9 +51,7 @@ export class EnvelopedSignature implements TransformAlgorithm { return node; } - // eslint-disable-next-line deprecation/deprecation - getAlgorithmName(): CanonicalizationOrTransformAlgorithmType { - // TODO: replace with TransformAlgorithmURI in next breaking change + getAlgorithmName(): TransformAlgorithmURI { return XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE; } } diff --git a/src/index.ts b/src/index.ts index a370bfef..691f5cce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,5 +6,8 @@ export { export { SignedXml } from "./signed-xml"; export { XmlDSigVerifier } from "./xmldsig-verifier"; export { XMLDSIG_URIS } from "./xmldsig-uris"; +export { Sha1, Sha256, Sha512 } from "./hash-algorithms"; +export { RsaSha1, RsaSha256, RsaSha256Mgf1, RsaSha512, HmacSha1 } from "./signature-algorithms"; +export { EnvelopedSignature } from "./enveloped-signature"; export * from "./types"; export * from "./utils"; diff --git a/src/signed-xml.ts b/src/signed-xml.ts index 4bcb2c56..e6111fad 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -14,8 +14,8 @@ import type { SignatureAlgorithmMap, CanonicalizationAlgorithmMap, TransformAlgorithmMap, + TransformAlgorithmURI, VerificationIdAttributeType, - CanonicalizationOrTransformAlgorithmType, } from "./types"; import * as isDomNode from "@xmldom/is-dom-node"; @@ -79,8 +79,7 @@ export class SignedXml { }; maxTransforms: number | null; - // eslint-disable-next-line deprecation/deprecation - implicitTransforms: ReadonlyArray = []; // TODO: replace with TransformAlgorithmURI in next breaking change + implicitTransforms: ReadonlyArray = []; keyInfoAttributes: { [attrName: string]: string } = {}; getKeyInfoContent = SignedXml.getKeyInfoContent; getCertFromKeyInfo = SignedXml.getCertFromKeyInfo; @@ -540,9 +539,7 @@ export class SignedXml { } } - // eslint-disable-next-line deprecation/deprecation - private findTransformAlgorithm(name: CanonicalizationOrTransformAlgorithmType) { - // TODO: replace with TransformAlgorithmURI in next breaking change + private findTransformAlgorithm(name: TransformAlgorithmURI) { // TODO: remove this fallback (breaking change) if (this.TransformAlgorithms == null) { return this.findCanonicalizationAlgorithm(name); diff --git a/src/types.ts b/src/types.ts index 432de774..f59b8d74 100644 --- a/src/types.ts +++ b/src/types.ts @@ -128,29 +128,7 @@ export type CanonicalizationAlgorithmMap = Record< CanonicalizationAlgorithmURI, new () => CanonicalizationAlgorithm >; -/** - * @deprecated Use CanonicalizationAlgorithm or TransformAlgorithm instead. - */ -// eslint-disable-next-line deprecation/deprecation -export type CanonicalizationOrTransformationAlgorithm = - | CanonicalizationAlgorithm - | TransformAlgorithm; -export type TransformAlgorithmMap = Record< - TransformAlgorithmURI, - // eslint-disable-next-line deprecation/deprecation - new () => CanonicalizationOrTransformationAlgorithm ->; // TODO: replace with TransformAlgorithm in next breaking change -/** - * @deprecated Use CanonicalizationAlgorithmURI or TransformAlgorithmURI instead. - */ -export type CanonicalizationOrTransformAlgorithmType = - | CanonicalizationAlgorithmURI - | TransformAlgorithmURI; - -/** - * @deprecated Use CanonicalizationAlgorithmURI instead. - */ -export type CanonicalizationAlgorithmType = CanonicalizationAlgorithmURI; +export type TransformAlgorithmMap = Record TransformAlgorithm>; /** * Options for the SignedXml constructor. */ @@ -164,8 +142,7 @@ export interface SignedXmlOptions { canonicalizationAlgorithm?: CanonicalizationAlgorithmURI; inclusiveNamespacesPrefixList?: string | string[]; maxTransforms?: number | null; - // eslint-disable-next-line deprecation/deprecation - implicitTransforms?: ReadonlyArray; // TODO: replace with TransformAlgorithmURI in next breaking change + implicitTransforms?: ReadonlyArray; keyInfoAttributes?: Record; getKeyInfoContent?(args?: GetKeyInfoContentArgs): string | null; getCertFromKeyInfo?: KeySelectorFunction; @@ -213,8 +190,7 @@ export interface Reference { xpath?: string; // An array of transforms to be applied to the data before signing. - // eslint-disable-next-line deprecation/deprecation - transforms: ReadonlyArray; // TODO: replace with TransformAlgorithmURI in next breaking change + transforms: ReadonlyArray; // The algorithm used to calculate the digest value of the data. digestAlgorithm: HashAlgorithmURI; @@ -337,8 +313,7 @@ export interface XmlDSigVerifierOptionsBase { /** * Transforms to apply implicitly during canonicalization. */ - // eslint-disable-next-line deprecation/deprecation - implicitTransforms?: ReadonlyArray; // TODO: replace with TransformAlgorithmURI in next breaking change + implicitTransforms?: ReadonlyArray; /** * Whether to throw an exception on verification failure. @@ -351,37 +326,42 @@ export interface XmlDSigVerifierSecurityOptions { /** * Maximum number of transforms allowed per Reference element. * Limits complexity to prevent denial-of-service attacks. - * @default {@link SignedXml.DEFAULT_MAX_TRANSFORMS} + * @default {@link XmlDSigVerifier.DEFAULT_MAX_TRANSFORMS} */ maxTransforms?: number; /** - * Signature algorithms allowed during verification. + * Signature algorithm constructors allowed during verification. + * Each constructor's `getAlgorithmName()` is used as the lookup key. * - * @default {@link SignedXml.getDefaultAsymmetricSignatureAlgorithms()} {@link SignedXml.getDefaultSymmetricSignatureAlgorithms()} + * @default {@link XmlDSigVerifier.defaultAsymmetricSignatureAlgorithms} or {@link XmlDSigVerifier.defaultSymmetricSignatureAlgorithms} */ - signatureAlgorithms?: SignatureAlgorithmMap; + signatureAlgorithms?: Array SignatureAlgorithm>; /** - * Hash algorithms allowed during verification. + * Hash algorithm constructors allowed during verification. + * Each constructor's `getAlgorithmName()` is used as the lookup key. * - * @default {@link SignedXml.getDefaultHashAlgorithms()} + * @default {@link XmlDSigVerifier.defaultHashAlgorithms} */ - hashAlgorithms?: HashAlgorithmMap; + hashAlgorithms?: Array HashAlgorithm>; /** - * Transform algorithms allowed during verification. (This must include canonicalization algorithms) + * Transform algorithm constructors allowed during verification. + * Must include any canonicalization algorithms used as transforms. + * Each constructor's `getAlgorithmName()` is used as the lookup key. * - * @default all algorithms in {@link SignedXml.getDefaultTransformAlgorithms()} + * @default {@link XmlDSigVerifier.defaultTransformAlgorithms} */ - transformAlgorithms?: TransformAlgorithmMap; + transformAlgorithms?: Array TransformAlgorithm>; /** - * Canonicalization algorithms allowed during verification. + * Canonicalization algorithm constructors allowed during verification. + * Each constructor's `getAlgorithmName()` is used as the lookup key. * - * @default all algorithms in {@link SignedXml.getDefaultCanonicalizationAlgorithms()} + * @default {@link XmlDSigVerifier.defaultCanonicalizationAlgorithms} */ - canonicalizationAlgorithms?: CanonicalizationAlgorithmMap; + canonicalizationAlgorithms?: Array CanonicalizationAlgorithm>; } export interface KeyInfoXmlDSigSecurityOptions extends XmlDSigVerifierSecurityOptions { @@ -475,18 +455,3 @@ export type FailedXmlDsigVerificationResult = { export type XmlDsigVerificationResult = | SuccessfulXmlDsigVerificationResult | FailedXmlDsigVerificationResult; - -/** - * @deprecated Use TransformAlgorithmOptions instead. - */ -export type CanonicalizationOrTransformationAlgorithmProcessOptions = TransformAlgorithmOptions; - -/** - * @deprecated Use SignatureAlgorithmURI instead. - */ -export type SignatureAlgorithmType = SignatureAlgorithmURI; - -/** - * @deprecated Use HashAlgorithmURI instead. - */ -export type HashAlgorithmType = HashAlgorithmURI; diff --git a/src/xmldsig-verifier.ts b/src/xmldsig-verifier.ts index 67db21cb..cff414df 100644 --- a/src/xmldsig-verifier.ts +++ b/src/xmldsig-verifier.ts @@ -8,16 +8,26 @@ import { XmlDSigVerifierOptions, XmlDsigVerificationResult, TransformAlgorithmURI, - KeyInfoXmlDSigSecurityOptions, KeyInfoKeySelector, SharedSecretKeySelector, CertificateKeySelector, - XmlDSigVerifierSecurityOptions, + SignatureAlgorithmMap, + HashAlgorithmMap, + TransformAlgorithmMap, + CanonicalizationAlgorithmMap, KeyInfoXmlDSigVerifierOptions, SharedSecretXmlDSigVerifierOptions, PublicCertXmlDSigVerifierOptions, } from "./types"; import { isArrayHasLength } from "./utils"; +import { Sha1, Sha256, Sha512 } from "./hash-algorithms"; +import { RsaSha1, RsaSha256, RsaSha256Mgf1, RsaSha512, HmacSha1 } from "./signature-algorithms"; +import { C14nCanonicalization, C14nCanonicalizationWithComments } from "./c14n-canonicalization"; +import { + ExclusiveCanonicalization, + ExclusiveCanonicalizationWithComments, +} from "./exclusive-canonicalization"; +import { EnvelopedSignature } from "./enveloped-signature"; type ResolvedXmlDSigVerifierOptionsBase = { idAttributes: VerificationIdAttributeType[]; @@ -25,22 +35,36 @@ type ResolvedXmlDSigVerifierOptionsBase = { throwOnError: boolean; }; +/** Resolved security options use maps (what SignedXml expects), not arrays. */ +type ResolvedSecurityOptions = { + maxTransforms: number; + signatureAlgorithms: SignatureAlgorithmMap; + hashAlgorithms: HashAlgorithmMap; + transformAlgorithms: TransformAlgorithmMap; + canonicalizationAlgorithms: CanonicalizationAlgorithmMap; +}; + +type ResolvedKeyInfoSecurityOptions = ResolvedSecurityOptions & { + checkCertExpiration: boolean; + truststore: Array; +}; + type ResolvedKeyInfoOptions = ResolvedXmlDSigVerifierOptionsBase & { optionsType: "keyinfo"; keySelector: KeyInfoKeySelector; - security: Required; + security: ResolvedKeyInfoSecurityOptions; }; type ResolvedCertificateOptions = ResolvedXmlDSigVerifierOptionsBase & { optionsType: "certificate"; keySelector: CertificateKeySelector; - security: Required; + security: ResolvedSecurityOptions; }; type ResolvedSharedSecretOptions = ResolvedXmlDSigVerifierOptionsBase & { optionsType: "sharedsecret"; keySelector: SharedSecretKeySelector; - security: Required; + security: ResolvedSecurityOptions; }; type ResolvedXmlDsigVerifierOptions = @@ -83,6 +107,36 @@ export class XmlDSigVerifier { public static readonly DEFAULT_CHECK_CERT_EXPIRATION = true; public static readonly DEFAULT_THROW_ON_ERROR = false; + static readonly defaultHashAlgorithms = [Sha1, Sha256, Sha512]; + static readonly defaultAsymmetricSignatureAlgorithms = [ + RsaSha1, + RsaSha256, + RsaSha256Mgf1, + RsaSha512, + ]; + static readonly defaultSymmetricSignatureAlgorithms = [HmacSha1]; + static readonly defaultCanonicalizationAlgorithms = [ + C14nCanonicalization, + C14nCanonicalizationWithComments, + ExclusiveCanonicalization, + ExclusiveCanonicalizationWithComments, + ]; + static readonly defaultTransformAlgorithms = [ + ...XmlDSigVerifier.defaultCanonicalizationAlgorithms, + EnvelopedSignature, + ]; + + private static toAlgorithmMap( + constructors: Array T>, + ): Record T> { + const map: Record T> = {}; + for (const Ctor of constructors) { + const instance = new Ctor(); + map[instance.getAlgorithmName()] = Ctor; + } + return map; + } + /** * Creates a new XmlDSigVerifier instance. The instance can be reused for multiple verifications. * @@ -171,13 +225,13 @@ export class XmlDSigVerifier { idAttributes: SignedXml.getDefaultIdAttributes(), maxTransforms: XmlDSigVerifier.DEFAULT_MAX_TRANSFORMS, checkCertExpiration: XmlDSigVerifier.DEFAULT_CHECK_CERT_EXPIRATION, - truststore: [], + truststore: [] as Array, signatureAlgorithms: isSharedSecretSelector(options) - ? SignedXml.getDefaultSymmetricSignatureAlgorithms() - : SignedXml.getDefaultAsymmetricSignatureAlgorithms(), - hashAlgorithms: SignedXml.getDefaultHashAlgorithms(), - transformAlgorithms: SignedXml.getDefaultTransformAlgorithms(), - canonicalizationAlgorithms: SignedXml.getDefaultCanonicalizationAlgorithms(), + ? XmlDSigVerifier.defaultSymmetricSignatureAlgorithms + : XmlDSigVerifier.defaultAsymmetricSignatureAlgorithms, + hashAlgorithms: XmlDSigVerifier.defaultHashAlgorithms, + transformAlgorithms: XmlDSigVerifier.defaultTransformAlgorithms, + canonicalizationAlgorithms: XmlDSigVerifier.defaultCanonicalizationAlgorithms, }; const baseOptions = { @@ -186,13 +240,20 @@ export class XmlDSigVerifier { throwOnError: options.throwOnError ?? XmlDSigVerifier.DEFAULT_THROW_ON_ERROR, }; - const baseSecurity = { + const baseSecurity: ResolvedSecurityOptions = { maxTransforms: options.security?.maxTransforms ?? defaults.maxTransforms, - signatureAlgorithms: options.security?.signatureAlgorithms ?? defaults.signatureAlgorithms, - hashAlgorithms: options.security?.hashAlgorithms ?? defaults.hashAlgorithms, - transformAlgorithms: options.security?.transformAlgorithms ?? defaults.transformAlgorithms, - canonicalizationAlgorithms: + signatureAlgorithms: XmlDSigVerifier.toAlgorithmMap( + options.security?.signatureAlgorithms ?? defaults.signatureAlgorithms, + ), + hashAlgorithms: XmlDSigVerifier.toAlgorithmMap( + options.security?.hashAlgorithms ?? defaults.hashAlgorithms, + ), + transformAlgorithms: XmlDSigVerifier.toAlgorithmMap( + options.security?.transformAlgorithms ?? defaults.transformAlgorithms, + ), + canonicalizationAlgorithms: XmlDSigVerifier.toAlgorithmMap( options.security?.canonicalizationAlgorithms ?? defaults.canonicalizationAlgorithms, + ), }; if (isKeyInfoSelector(options)) { diff --git a/test/xmldsig-verifier.spec.ts b/test/xmldsig-verifier.spec.ts index 4e025a0d..aa278d63 100644 --- a/test/xmldsig-verifier.spec.ts +++ b/test/xmldsig-verifier.spec.ts @@ -1,10 +1,7 @@ import * as fs from "fs"; import { expect } from "chai"; -import { XmlDSigVerifier, SignedXml, ExclusiveCanonicalization } from "../src"; -import { RsaSha1 } from "../src/signature-algorithms"; -import { Sha1 } from "../src/hash-algorithms"; -import { EnvelopedSignature } from "../src/enveloped-signature"; -import { XMLDSIG_URIS, XmlDsigVerificationResult } from "../src/"; +import { XmlDSigVerifier, SignedXml, EnvelopedSignature, XMLDSIG_URIS } from "../src"; +import type { XmlDsigVerificationResult } from "../src/"; import { X509Certificate } from "node:crypto"; @@ -160,9 +157,9 @@ describe("XmlDSigVerifier", function () { maxTransforms: 5, checkCertExpiration: true, truststore: [rootCert], - signatureAlgorithms: SignedXml.getDefaultAsymmetricSignatureAlgorithms(), - hashAlgorithms: SignedXml.getDefaultHashAlgorithms(), - transformAlgorithms: SignedXml.getDefaultTransformAlgorithms(), + signatureAlgorithms: XmlDSigVerifier.defaultAsymmetricSignatureAlgorithms, + hashAlgorithms: XmlDSigVerifier.defaultHashAlgorithms, + transformAlgorithms: XmlDSigVerifier.defaultTransformAlgorithms, }, }); expect(verifier).to.be.instanceOf(XmlDSigVerifier); @@ -732,7 +729,9 @@ describe("XmlDSigVerifier", function () { const signedXml = createSignedXml(xml); const verifier = new XmlDSigVerifier({ keySelector: { publicCert }, - security: { signatureAlgorithms: SignedXml.getDefaultAsymmetricSignatureAlgorithms() }, + security: { + signatureAlgorithms: XmlDSigVerifier.defaultAsymmetricSignatureAlgorithms, + }, }); expectValidResult(verifier.verifySignature(signedXml)); }); @@ -741,7 +740,7 @@ describe("XmlDSigVerifier", function () { const signedXml = createSignedXml(xml); const verifier = new XmlDSigVerifier({ keySelector: { publicCert }, - security: { signatureAlgorithms: { foo: RsaSha1 } }, + security: { signatureAlgorithms: [] }, }); expectInvalidResult(verifier.verifySignature(signedXml), "signature algorithm"); }); @@ -752,7 +751,7 @@ describe("XmlDSigVerifier", function () { const signedXml = createSignedXml(xml); const verifier = new XmlDSigVerifier({ keySelector: { publicCert }, - security: { hashAlgorithms: SignedXml.getDefaultHashAlgorithms() }, + security: { hashAlgorithms: XmlDSigVerifier.defaultHashAlgorithms }, }); expectValidResult(verifier.verifySignature(signedXml)); }); @@ -761,7 +760,7 @@ describe("XmlDSigVerifier", function () { const signedXml = createSignedXml(xml); const verifier = new XmlDSigVerifier({ keySelector: { publicCert }, - security: { hashAlgorithms: { foo: Sha1 } }, + security: { hashAlgorithms: [] }, }); expectInvalidResult(verifier.verifySignature(signedXml), "hash algorithm"); }); @@ -772,7 +771,7 @@ describe("XmlDSigVerifier", function () { const signedXml = createSignedXml(xml); const verifier = new XmlDSigVerifier({ keySelector: { publicCert }, - security: { transformAlgorithms: SignedXml.getDefaultTransformAlgorithms() }, + security: { transformAlgorithms: XmlDSigVerifier.defaultTransformAlgorithms }, }); expectValidResult(verifier.verifySignature(signedXml)); }); @@ -781,7 +780,7 @@ describe("XmlDSigVerifier", function () { const signedXml = createSignedXml(xml); const verifier = new XmlDSigVerifier({ keySelector: { publicCert }, - security: { transformAlgorithms: { foo: EnvelopedSignature } }, + security: { transformAlgorithms: [EnvelopedSignature] }, }); expectInvalidResult(verifier.verifySignature(signedXml), "transform algorithm"); }); @@ -793,7 +792,7 @@ describe("XmlDSigVerifier", function () { const verifier = new XmlDSigVerifier({ keySelector: { publicCert }, security: { - canonicalizationAlgorithms: SignedXml.getDefaultCanonicalizationAlgorithms(), + canonicalizationAlgorithms: XmlDSigVerifier.defaultCanonicalizationAlgorithms, }, }); expectValidResult(verifier.verifySignature(signedXml)); @@ -803,7 +802,7 @@ describe("XmlDSigVerifier", function () { const signedXml = createSignedXml(xml); const verifier = new XmlDSigVerifier({ keySelector: { publicCert }, - security: { canonicalizationAlgorithms: { foo: ExclusiveCanonicalization } }, + security: { canonicalizationAlgorithms: [] }, }); expectInvalidResult(verifier.verifySignature(signedXml), "canonicalization algorithm"); }); From 41bab4a17ad4ba682da8865a876ccd5ef3317273 Mon Sep 17 00:00:00 2001 From: shunkica Date: Thu, 5 Mar 2026 18:13:56 +0100 Subject: [PATCH 04/12] fix: improve getCertFromKeyInfo docs and normalize thrown errors - Clarify KeyInfoKeySelector JSDoc: returned string is used as KeyLike, must be certificate material when expiration/truststore checks are on - Normalize thrown values to Error instances in handleError - Update XMLDSIG_VERIFIER.md comment for getCertFromKeyInfo --- XMLDSIG_VERIFIER.md | 2 +- src/types.ts | 6 +++++- src/xmldsig-verifier.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/XMLDSIG_VERIFIER.md b/XMLDSIG_VERIFIER.md index 32a1e045..dfbec3e7 100644 --- a/XMLDSIG_VERIFIER.md +++ b/XMLDSIG_VERIFIER.md @@ -154,7 +154,7 @@ interface XmlDSigVerifierOptions { // STRATEGY: Choose one of the following key selectors keySelector: | { publicCert: KeyLike } // Direct public key/cert - | { getCertFromKeyInfo: (node?: Node | null) => string | null } // Extract from XML + | { getCertFromKeyInfo: (node?: Node | null) => string | null } // Extract certificate from XML | { sharedSecretKey: KeyLike }; // HMAC shared secret // CONFIGURATION diff --git a/src/types.ts b/src/types.ts index f59b8d74..9da733c0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -289,7 +289,11 @@ export type CertificateKeySelector = { }; export type KeyInfoKeySelector = { - /** Function to extract the public key from KeyInfo element */ + /** Function to extract the public certificate or key from the KeyInfo element. + * The returned string is passed to the signature algorithm's `verifySignature` as a `KeyLike`. + * When `checkCertExpiration` or `truststore` security options are enabled, it is also + * parsed as a certificate, so it must be valid certificate material in that case. + * @see {@link SignedXml.getCertFromKeyInfo} for a default implementation. */ getCertFromKeyInfo: (keyInfo?: Node | null) => string | null; }; diff --git a/src/xmldsig-verifier.ts b/src/xmldsig-verifier.ts index cff414df..db2dc109 100644 --- a/src/xmldsig-verifier.ts +++ b/src/xmldsig-verifier.ts @@ -359,7 +359,7 @@ export class XmlDSigVerifier { private static handleError(error: unknown, throwOnError: boolean): XmlDsigVerificationResult { if (throwOnError) { - throw error; + throw error instanceof Error ? error : new Error(String(error)); } const errorMessage = From 920d0d46db87b546f7601d75bd63970281802c14 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Thu, 5 Mar 2026 12:47:46 -0600 Subject: [PATCH 05/12] Make tests pass --- src/signed-xml.ts | 48 +++++++++------------ src/utils.ts | 6 +++ src/xmldsig-verifier.ts | 5 +-- test/c14n-non-exclusive-unit-tests.spec.ts | 4 +- test/c14nWithComments-unit-tests.spec.ts | 7 ++-- test/canonicalization-unit-tests.spec.ts | 11 ++--- test/document-tests.spec.ts | 15 +++---- test/hmac-tests.spec.ts | 7 ++-- test/key-info-tests.spec.ts | 5 ++- test/saml-response-tests.spec.ts | 23 +++++----- test/signature-integration-tests.spec.ts | 11 ++--- test/signature-object-tests.spec.ts | 23 +++++----- test/signature-unit-tests.spec.ts | 49 ++++++++++++---------- test/utils-tests.spec.ts | 21 ++++++---- test/wsfed-metadata-tests.spec.ts | 3 +- test/xmldsig-verifier.spec.ts | 5 ++- 16 files changed, 130 insertions(+), 113 deletions(-) diff --git a/src/signed-xml.ts b/src/signed-xml.ts index e6111fad..b6de2dd9 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -19,7 +19,6 @@ import type { } from "./types"; import * as isDomNode from "@xmldom/is-dom-node"; -import * as xmldom from "@xmldom/xmldom"; import * as crypto from "crypto"; import { deprecate } from "util"; import * as xpath from "xpath"; @@ -321,7 +320,7 @@ export class SignedXml { this.signedXml = xml; - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); // Reset the references as only references from our re-parsed signedInfo node can be trusted this.references = []; @@ -337,10 +336,7 @@ export class SignedXml { } // unsigned, verify later to keep with consistent callback behavior - const parsedUnverifiedSignedInfo = new xmldom.DOMParser().parseFromString( - unverifiedSignedInfoCanon, - "text/xml", - ); + const parsedUnverifiedSignedInfo = utils.parseXml(unverifiedSignedInfoCanon); const unverifiedSignedInfoDoc = parsedUnverifiedSignedInfo.documentElement; if (!unverifiedSignedInfoDoc) { @@ -701,17 +697,15 @@ export class SignedXml { * @param signatureNode The XML node or string representing the signature. */ loadSignature(signatureNode: Node | string): void { - if (typeof signatureNode === "string") { - this.signatureNode = signatureNode = new xmldom.DOMParser().parseFromString(signatureNode); - } else { - this.signatureNode = signatureNode; - } + const signatureNodeParsed = + typeof signatureNode === "string" ? utils.parseXml(signatureNode) : signatureNode; + this.signatureNode = signatureNodeParsed; - this.signatureXml = signatureNode.toString(); + this.signatureXml = signatureNodeParsed.toString(); const node = xpath.select1( ".//*[local-name(.)='CanonicalizationMethod']/@Algorithm", - signatureNode, + signatureNodeParsed, ); if (!isDomNode.isNodeLike(node)) { throw new Error("could not find CanonicalizationMethod/@Algorithm element"); @@ -729,7 +723,7 @@ export class SignedXml { const signatureAlgorithm = xpath.select1( ".//*[local-name(.)='SignatureMethod']/@Algorithm", - signatureNode, + signatureNodeParsed, ); if (isDomNode.isAttributeNode(signatureAlgorithm)) { @@ -762,10 +756,7 @@ export class SignedXml { [canonicalizationAlgorithmForSignedInfo], signedInfoNodes[0], ); - const temporaryCanonSignedInfoXml = new xmldom.DOMParser().parseFromString( - temporaryCanonSignedInfo, - "text/xml", - ); + const temporaryCanonSignedInfoXml = utils.parseXml(temporaryCanonSignedInfo); const signedInfoDoc = temporaryCanonSignedInfoXml.documentElement; this.references = []; @@ -781,14 +772,14 @@ export class SignedXml { const signatureValue = xpath.select1( ".//*[local-name(.)='SignatureValue']/text()", - signatureNode, + signatureNodeParsed, ); if (isDomNode.isTextNode(signatureValue)) { this.signatureValue = signatureValue.data.replace(/\r?\n/g, ""); } - const keyInfo = xpath.select1(".//*[local-name(.)='KeyInfo']", signatureNode); + const keyInfo = xpath.select1(".//*[local-name(.)='KeyInfo']", signatureNodeParsed); if (isDomNode.isNodeLike(keyInfo)) { this.keyInfo = keyInfo; @@ -1024,7 +1015,7 @@ export class SignedXml { options = (options ?? {}) as ComputeSignatureOptions; } - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); let xmlNsAttr = "xmlns"; const signatureAttrs: string[] = []; let currentPrefix: string; @@ -1067,11 +1058,12 @@ export class SignedXml { continue; } // No specific nodes to ID for empty URI - const nodes = xpath.selectWithResolver( - ref.xpath ?? "", - doc, - this.namespaceResolver, - ) as Element[]; + const selectedNodes = xpath.selectWithResolver(ref.xpath ?? "", doc, this.namespaceResolver); + const nodes = isDomNode.isArrayOfNodes(selectedNodes) + ? selectedNodes + : isDomNode.isNodeLike(selectedNodes) + ? [selectedNodes] + : []; for (const node of nodes) { isDomNode.assertIsElementNode(node); this.ensureHasId(node); @@ -1113,7 +1105,7 @@ export class SignedXml { // A trick to remove the namespaces that already exist in the xml // This only works if the prefix and namespace match with those in the xml const dummySignatureWrapper = `${signatureXml}`; - const nodeXml = new xmldom.DOMParser().parseFromString(dummySignatureWrapper); + const nodeXml = utils.parseXml(dummySignatureWrapper); // Because we are using a dummy wrapper hack described above, we know there will be a `firstChild` // and that it will be an `Element` node. @@ -1503,7 +1495,7 @@ export class SignedXml { //we need to wrap the info in a dummy signature since it contains the default namespace. const dummySignatureWrapper = `<${prefix}Signature ${xmlNsAttr}="${NAMESPACES.ds}">${signatureValueXml}`; - const doc = new xmldom.DOMParser().parseFromString(dummySignatureWrapper); + const doc = utils.parseXml(dummySignatureWrapper); // Because we are using a dummy wrapper hack described above, we know there will be a `firstChild` // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/src/utils.ts b/src/utils.ts index 308f2508..1406ddb8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,13 @@ +import * as xmldom from "@xmldom/xmldom"; import * as xpath from "xpath"; import type { NamespacePrefix } from "./types"; import * as isDomNode from "@xmldom/is-dom-node"; +export function parseXml(xml: string, mimeType = "text/xml"): Document { + const normalizedXml = xml.replace(/^\uFEFF/, ""); + return new xmldom.DOMParser().parseFromString(normalizedXml, mimeType) as unknown as Document; +} + export function isArrayHasLength(array: unknown): array is unknown[] { return Array.isArray(array) && array.length > 0; } diff --git a/src/xmldsig-verifier.ts b/src/xmldsig-verifier.ts index db2dc109..c6989b7d 100644 --- a/src/xmldsig-verifier.ts +++ b/src/xmldsig-verifier.ts @@ -1,5 +1,4 @@ import { KeyLike, X509Certificate } from "node:crypto"; -import { DOMParser } from "@xmldom/xmldom"; import { SignedXml } from "./signed-xml"; import { KeySelectorFunction, @@ -19,7 +18,7 @@ import { SharedSecretXmlDSigVerifierOptions, PublicCertXmlDSigVerifierOptions, } from "./types"; -import { isArrayHasLength } from "./utils"; +import { isArrayHasLength, parseXml } from "./utils"; import { Sha1, Sha256, Sha512 } from "./hash-algorithms"; import { RsaSha1, RsaSha256, RsaSha256Mgf1, RsaSha512, HmacSha1 } from "./signature-algorithms"; import { C14nCanonicalization, C14nCanonicalizationWithComments } from "./c14n-canonicalization"; @@ -185,7 +184,7 @@ export class XmlDSigVerifier { this.signedXml.loadSignature(signatureNode); } else { // Auto-detect signature if exactly one signature is found in the document - const doc = new DOMParser().parseFromString(xml, "application/xml"); + const doc = parseXml(xml, "application/xml"); const signatureNodes = this.signedXml.findSignatures(doc); if (signatureNodes.length === 0) { diff --git a/test/c14n-non-exclusive-unit-tests.spec.ts b/test/c14n-non-exclusive-unit-tests.spec.ts index 73505052..a45be233 100644 --- a/test/c14n-non-exclusive-unit-tests.spec.ts +++ b/test/c14n-non-exclusive-unit-tests.spec.ts @@ -7,7 +7,7 @@ import * as utils from "../src/utils"; import * as isDomNode from "@xmldom/is-dom-node"; const test_C14nCanonicalization = function (xml, xpathArg, expected) { - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const node = xpath.select1(xpathArg, doc); const can = new C14nCanonicalization(); @@ -22,7 +22,7 @@ const test_C14nCanonicalization = function (xml, xpathArg, expected) { }; const test_findAncestorNs = function (xml, xpath, expected) { - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const result = utils.findAncestorNs(doc, xpath); expect(result).to.deep.equal(expected); diff --git a/test/c14nWithComments-unit-tests.spec.ts b/test/c14nWithComments-unit-tests.spec.ts index 2ef98f34..d7394c2e 100644 --- a/test/c14nWithComments-unit-tests.spec.ts +++ b/test/c14nWithComments-unit-tests.spec.ts @@ -5,9 +5,10 @@ import * as xmldom from "@xmldom/xmldom"; import * as xpath from "xpath"; import { SignedXml, XMLDSIG_URIS } from "../src"; import * as isDomNode from "@xmldom/is-dom-node"; +import * as utils from "../src/utils"; const compare = function (xml, xpathArg, expected, inclusiveNamespacesPrefixList?: string[]) { - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const elem = xpath.select1(xpathArg, doc); const can = new c14nWithComments(); isDomNode.assertIsElementNode(elem); @@ -348,7 +349,7 @@ describe("Exclusive canonicalization with comments", function () { }); it("Multiple Canonicalization with namespace definition outside of signed element", function () { - const doc = new xmldom.DOMParser().parseFromString( + const doc = utils.parseXml( '', ); const node = xpath.select1("//*[local-name(.)='y']", doc); @@ -370,7 +371,7 @@ describe("Exclusive canonicalization with comments", function () { // in a document. const xml = ''; - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const node = xpath.select1("//*[local-name(.)='y']", doc); const sig = new SignedXml(); const transforms = [XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE]; diff --git a/test/canonicalization-unit-tests.spec.ts b/test/canonicalization-unit-tests.spec.ts index 97d4d71e..fdf71160 100644 --- a/test/canonicalization-unit-tests.spec.ts +++ b/test/canonicalization-unit-tests.spec.ts @@ -4,6 +4,7 @@ import * as xmldom from "@xmldom/xmldom"; import * as xpath from "xpath"; import { SignedXml, ExclusiveCanonicalization, XMLDSIG_URIS } from "../src"; import * as isDomNode from "@xmldom/is-dom-node"; +import * as utils from "../src/utils"; const compare = function ( xml: string, @@ -12,7 +13,7 @@ const compare = function ( inclusiveNamespacesPrefixList?: string[], defaultNsForPrefix?: Record, ) { - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const elem = xpath.select1(xpathArg, doc); const can = new ExclusiveCanonicalization(); isDomNode.assertIsElementNode(elem); @@ -53,7 +54,7 @@ describe("Canonicalization unit tests", function () { it("Exclusive canonicalization works with default namespace for prefix", function () { compare( - '', + '', "//*[local-name(.)='SignedInfo']", '', undefined, @@ -397,7 +398,7 @@ describe("Canonicalization unit tests", function () { }); it("Multiple Canonicalization with namespace definition outside of signed element", function () { - const doc = new xmldom.DOMParser().parseFromString( + const doc = utils.parseXml( '', ); const node = xpath.select1("//*[local-name(.)='y']", doc); @@ -415,7 +416,7 @@ describe("Canonicalization unit tests", function () { }); it("Shouldn't continue processing transforms if we end up with a string as a result of a transform", function () { - const doc = new xmldom.DOMParser().parseFromString( + const doc = utils.parseXml( '', ); const node1 = xpath.select1("//*[local-name(.)='y']", doc); @@ -444,7 +445,7 @@ describe("Canonicalization unit tests", function () { // in a document. const xml = ''; - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const node = xpath.select1("//*[local-name(.)='y']", doc); isDomNode.assertIsNodeLike(node); diff --git a/test/document-tests.spec.ts b/test/document-tests.spec.ts index 84392ec5..e3585032 100644 --- a/test/document-tests.spec.ts +++ b/test/document-tests.spec.ts @@ -4,11 +4,12 @@ import * as xmldom from "@xmldom/xmldom"; import * as fs from "fs"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; +import * as utils from "../src/utils"; describe("Document tests", function () { it("test with a document (using FileKeyInfo)", function () { const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const node = xpath.select1( `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, @@ -26,7 +27,7 @@ describe("Document tests", function () { it("test with a document (using StringKeyInfo)", function () { const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const node = xpath.select1( `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, @@ -47,7 +48,7 @@ describe("Document tests", function () { describe("Validated node references tests", function () { it("should return references if the document is validly signed", function () { const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const sig = new SignedXml(); sig.getCertFromKeyInfo = SignedXml.getCertFromKeyInfo; sig.loadSignature(sig.findSignatures(doc)[0]); @@ -64,7 +65,7 @@ describe("Validated node references tests", function () { it("should not return references if the document is not validly signed", function () { const xml = fs.readFileSync("./test/static/invalid_signature - changed content.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const sig = new SignedXml(); sig.loadSignature(sig.findSignatures(doc)[0]); const validSignature = sig.checkSignature(xml); @@ -80,7 +81,7 @@ describe("Validated node references tests", function () { it("should return `null` if the selected node isn't found", function () { const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const sig = new SignedXml(); sig.getCertFromKeyInfo = SignedXml.getCertFromKeyInfo; sig.loadSignature(sig.findSignatures(doc)[0]); @@ -96,7 +97,7 @@ describe("Validated node references tests", function () { it("should return the selected node if it is validly signed", function () { const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const sig = new SignedXml(); sig.getCertFromKeyInfo = SignedXml.getCertFromKeyInfo; sig.loadSignature(sig.findSignatures(doc)[0]); @@ -115,7 +116,7 @@ describe("Validated node references tests", function () { it("should return `null` if the selected node isn't validly signed", function () { const xml = fs.readFileSync("./test/static/invalid_signature - changed content.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const sig = new SignedXml(); sig.loadSignature(sig.findSignatures(doc)[0]); const validSignature = sig.checkSignature(xml); diff --git a/test/hmac-tests.spec.ts b/test/hmac-tests.spec.ts index 9ebaa2dd..a857a395 100644 --- a/test/hmac-tests.spec.ts +++ b/test/hmac-tests.spec.ts @@ -4,11 +4,12 @@ import * as xmldom from "@xmldom/xmldom"; import * as fs from "fs"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; +import * as utils from "../src/utils"; describe("HMAC tests", function () { it("test validating HMAC signature", function () { const xml = fs.readFileSync("./test/static/hmac_signature.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const signature = xpath.select1( `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, @@ -27,7 +28,7 @@ describe("HMAC tests", function () { it("test HMAC signature with incorrect key", function () { const xml = fs.readFileSync("./test/static/hmac_signature.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const signature = xpath.select1( `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, @@ -57,7 +58,7 @@ describe("HMAC tests", function () { sig.canonicalizationAlgorithm = XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; sig.computeSignature(xml); - const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml()); + const doc = utils.parseXml(sig.getSignedXml()); const signature = xpath.select1( `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, diff --git a/test/key-info-tests.spec.ts b/test/key-info-tests.spec.ts index 4ba648a1..0bfbf8b5 100644 --- a/test/key-info-tests.spec.ts +++ b/test/key-info-tests.spec.ts @@ -4,6 +4,7 @@ import * as xpath from "xpath"; import { SignedXml, XMLDSIG_URIS } from "../src"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; +import * as utils from "../src/utils"; describe("KeyInfo tests", function () { it("adds X509Certificate element during signature", function () { @@ -15,7 +16,7 @@ describe("KeyInfo tests", function () { sig.signatureAlgorithm = XMLDSIG_URIS.SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); const x509 = xpath.select("//*[local-name(.)='X509Certificate']", doc.documentElement); isDomNode.assertIsArrayOfNodes(x509); @@ -37,7 +38,7 @@ describe("KeyInfo tests", function () { sig.canonicalizationAlgorithm = XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; sig.computeSignature(xml); - const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml()); + const doc = utils.parseXml(sig.getSignedXml()); const keyInfo = xpath.select1("//*[local-name(.)='KeyInfo']", doc); expect(keyInfo).to.be.undefined; diff --git a/test/saml-response-tests.spec.ts b/test/saml-response-tests.spec.ts index 1677a703..2c417b5b 100644 --- a/test/saml-response-tests.spec.ts +++ b/test/saml-response-tests.spec.ts @@ -4,11 +4,12 @@ import * as xmldom from "@xmldom/xmldom"; import * as fs from "fs"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; +import * as utils from "../src/utils"; describe("SAML response tests", function () { it("test validating SAML response", function () { const xml = fs.readFileSync("./test/static/valid_saml.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const signature = xpath.select1( `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, @@ -25,7 +26,7 @@ describe("SAML response tests", function () { it("test validating SAML response with sha256-rsa-MGF1", function () { const xml = fs.readFileSync("./test/static/valid_saml_sha256_rsa_mgf1.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const signature = xpath.select1( `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, @@ -41,7 +42,7 @@ describe("SAML response tests", function () { it("test validating SAML response with sha256-rsa-MGF1 fails for modified file", function () { const xml = fs.readFileSync("./test/static/invalid_saml_sha256_rsa_mgf1.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const signature = xpath.select1( `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, @@ -57,7 +58,7 @@ describe("SAML response tests", function () { it("test validating wrapped assertion signature", function () { const xml = fs.readFileSync("./test/static/valid_saml_signature_wrapping.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const assertion = xpath.select1("//*[local-name(.)='Assertion']", doc); isDomNode.assertIsNodeLike(assertion); const signature = xpath.select1( @@ -81,7 +82,7 @@ describe("SAML response tests", function () { it("test validating SAML response where a namespace is defined outside the signed element", function () { const xml = fs.readFileSync("./test/static/saml_external_ns.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const signature = xpath.select1( `//*//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, @@ -97,7 +98,7 @@ describe("SAML response tests", function () { it("test reference id does not contain quotes", function () { const xml = fs.readFileSync("./test/static/id_with_quotes.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const assertion = xpath.select1("//*[local-name(.)='Assertion']", doc); isDomNode.assertIsNodeLike(assertion); const signature = xpath.select1( @@ -115,7 +116,7 @@ describe("SAML response tests", function () { it("test validating SAML response WithComments", function () { const xml = fs.readFileSync("./test/static/valid_saml_withcomments.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const signature = xpath.select1( `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, @@ -131,7 +132,7 @@ describe("SAML response tests", function () { it("throws an error for a document with no `SignedInfo` node", function () { const xml = fs.readFileSync("./test/static/invalid_saml_no_signed_info.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const node = xpath.select1( `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, @@ -147,7 +148,7 @@ describe("SAML response tests", function () { it("test validation ignores an additional wrapped `SignedInfo` node", function () { const xml = fs.readFileSync("./test/static/saml_wrapped_signed_info_node.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const assertion = xpath.select1("//*[local-name(.)='Assertion']", doc); isDomNode.assertIsNodeLike(assertion); const signature = xpath.select1( @@ -168,7 +169,7 @@ describe("SAML response tests", function () { it("test signature throws if multiple `SignedInfo` nodes are found", function () { const xml = fs.readFileSync("./test/static/saml_multiple_signed_info_nodes.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const assertion = xpath.select1("//*[local-name(.)='Assertion'][1]", doc); isDomNode.assertIsNodeLike(assertion); const signature = xpath.select1( @@ -187,7 +188,7 @@ describe("SAML response tests", function () { describe("for a SAML response with a digest value comment", () => { it("loads digest value from text content instead of comment", function () { const xml = fs.readFileSync("./test/static/valid_saml_with_digest_comment.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const assertion = xpath.select1("//*[local-name(.)='Assertion']", doc); isDomNode.assertIsNodeLike(assertion); const signature = xpath.select1( diff --git a/test/signature-integration-tests.spec.ts b/test/signature-integration-tests.spec.ts index 12962337..52d73fbb 100644 --- a/test/signature-integration-tests.spec.ts +++ b/test/signature-integration-tests.spec.ts @@ -4,6 +4,7 @@ import { SignedXml, XMLDSIG_URIS } from "../src"; import * as fs from "fs"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; +import * as utils from "../src/utils"; describe("Signature integration tests", function () { function verifySignature(xml, expected, xpath, canonicalizationAlgorithm) { @@ -97,7 +98,7 @@ describe("Signature integration tests", function () { */ xml = xml.replace(/>\s*<"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const childXml = doc.firstChild?.toString(); const signature = xpath.select1( @@ -116,7 +117,7 @@ describe("Signature integration tests", function () { it("signature with inclusive namespaces", function () { const xml = fs.readFileSync("./test/static/signature_with_inclusivenamespaces.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const childXml = doc.firstChild?.toString(); const signature = xpath.select1( @@ -138,7 +139,7 @@ describe("Signature integration tests", function () { "./test/static/signature_with_inclusivenamespaces_lines.xml", "utf-8", ); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const childXml = doc.firstChild?.toString(); const signature = xpath.select1( @@ -160,7 +161,7 @@ describe("Signature integration tests", function () { "./test/static/signature_with_inclusivenamespaces_lines_windows.xml", "utf-8", ); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const childXml = doc.firstChild?.toString(); const signature = xpath.select1( @@ -193,7 +194,7 @@ describe("Signature integration tests", function () { const signed = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signed); + const doc = utils.parseXml(signed); /* Expecting this structure: diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index fdd45e7f..311bfc90 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -5,6 +5,7 @@ import * as xmldom from "@xmldom/xmldom"; import * as isDomNode from "@xmldom/is-dom-node"; import { SignedXml, XMLDSIG_URIS } from "../src"; import { Sha256 } from "../src/hash-algorithms"; +import * as utils from "../src/utils"; const privateKey = fs.readFileSync("./test/static/client.pem", "utf-8"); const publicCert = fs.readFileSync("./test/static/client_public.pem", "utf-8"); @@ -81,7 +82,7 @@ describe("ds:Object support in XML signatures", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); // Should have three Object elements const objectNodes = selectNs("/root/ds:Signature/ds:Object", doc); @@ -146,7 +147,7 @@ describe("ds:Object support in XML signatures", function () { // When we add a prefix to the signature, there is no default namespace sig.computeSignature(xml, { prefix: "ds" }); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); // Verify the namespace of the ds:Object element const objectNode = select1Ns("/root/ds:Signature/ds:Object[@Id='object1']", doc); @@ -172,7 +173,7 @@ describe("ds:Object support in XML signatures", function () { sigWithNull.computeSignature(xml); const signedXmlWithNull = sigWithNull.getSignedXml(); - const docWithNull = new xmldom.DOMParser().parseFromString(signedXmlWithNull); + const docWithNull = utils.parseXml(signedXmlWithNull); // Verify that no Object elements exist const objectNodesWithNull = selectNs("//ds:Object", docWithNull); @@ -195,7 +196,7 @@ describe("ds:Object support in XML signatures", function () { sigWithEmpty.computeSignature(xml); const signedXmlWithEmpty = sigWithEmpty.getSignedXml(); - const docWithEmpty = new xmldom.DOMParser().parseFromString(signedXmlWithEmpty); + const docWithEmpty = utils.parseXml(signedXmlWithEmpty); // Verify that no Object elements exist const objectNodesWithEmpty = selectNs("//ds:Object", docWithEmpty); @@ -230,7 +231,7 @@ describe("ds:Object support in XML signatures", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const signedDoc = new xmldom.DOMParser().parseFromString(signedXml); + const signedDoc = utils.parseXml(signedXml); // Verify that there is exactly one ds:Reference const referenceNodes = selectNs("/root/ds:Signature/ds:SignedInfo/ds:Reference", signedDoc); @@ -298,7 +299,7 @@ describe("Valid signatures with ds:Object elements", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); // Verify that the signature is valid const { valid, errorMessage } = checkSignature(signedXml, doc); @@ -346,7 +347,7 @@ describe("Valid signatures with ds:Object elements", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); // Verify that there are two Reference elements const referenceNodes = selectNs("/ns1:root/ds:Signature/ds:SignedInfo/ds:Reference", doc, { @@ -389,7 +390,7 @@ describe("Valid signatures with ds:Object elements", function () { sig.computeSignature(xml, { prefix: "ds" }); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); // Find the ds:Object/Data element and get the value of its Id attribute (ensuring it was generated) const dataEl = select1Ns("/root/ds:Signature/ds:Object/Data[@Id]", doc); @@ -433,7 +434,7 @@ describe("Should successfuly sign references to ds:KeyInfo elements", function ( sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); // Verify that there is a Reference to KeyInfo const referenceEl = select1Ns( @@ -468,7 +469,7 @@ describe("Should successfuly sign references to ds:KeyInfo elements", function ( sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); // Find the KeyInfo element and get the value of its Id attribute (ensuring it was generated) const keyInfoEl = select1Ns("/root/ds:Signature/ds:KeyInfo[@Id]", doc); @@ -550,7 +551,7 @@ describe("XAdES Object support in XML signatures", function () { }); const signedXml = sig.getSignedXml(); - const signedDoc = new xmldom.DOMParser().parseFromString(signedXml); + const signedDoc = utils.parseXml(signedXml); // ds:Signature exists and has the expected Id const elSig = select1Ns(`/root/ds:Signature[@Id='${signatureId}']`, signedDoc); diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index 5f21b2fb..216acee6 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -5,6 +5,7 @@ import * as fs from "fs"; import * as crypto from "crypto"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; +import * as utils from "../src/utils"; const { SIGNATURE_ALGORITHMS, HASH_ALGORITHMS, CANONICALIZATION_ALGORITHMS, NAMESPACES } = XMLDSIG_URIS; @@ -37,7 +38,7 @@ describe("Signature unit tests", function () { } function loadSignature(xml: string): SignedXml { - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const node = xpath.select1( `//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, @@ -61,12 +62,14 @@ describe("Signature unit tests", function () { it(`should fail verification of signed xml with ${signatureAlgorithm} after manipulation`, function () { const xml = signWith(signatureAlgorithm); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const node = xpath.select1("//*[local-name(.)='x']", doc); isDomNode.assertIsElementNode(node); const targetElement = node as Element; targetElement.setAttribute("attr", "manipulatedValue"); - const manipulatedXml = new xmldom.XMLSerializer().serializeToString(doc); + const manipulatedXml = new xmldom.XMLSerializer().serializeToString( + doc as unknown as xmldom.Node, + ); const sig = loadSignature(manipulatedXml); const res = sig.checkSignature(manipulatedXml); @@ -113,7 +116,7 @@ describe("Signature unit tests", function () { sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getOriginalXmlWithIds(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); const op = nsMode === "equal" ? "=" : "!="; @@ -156,7 +159,7 @@ describe("Signature unit tests", function () { }); const signedXml = sig.getSignatureXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); const references = xpath.select("//*[local-name(.)='Reference']", doc); isDomNode.assertIsArrayOfNodes(references); expect(references.length).to.equal(2); @@ -176,7 +179,7 @@ describe("Signature unit tests", function () { sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getOriginalXmlWithIds(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); const attrs = xpath.select("//@*", doc); isDomNode.assertIsArrayOfNodes(attrs); expect(attrs.length, "wrong number of attributes").to.equal(2); @@ -215,7 +218,7 @@ describe("Signature unit tests", function () { }); const signedXml = sig.getSignatureXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); const signatureNode = doc.documentElement; expect(attrs.Id, `Id attribute is not equal to the expected value: "${attrs.Id}"`).to.equal( @@ -248,7 +251,7 @@ describe("Signature unit tests", function () { sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); - const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml()); + const doc = utils.parseXml(sig.getSignedXml()); const lastChild = doc.documentElement.lastChild; isDomNode.assertIsElementNode(lastChild); @@ -278,7 +281,7 @@ describe("Signature unit tests", function () { }, }); - const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml()); + const doc = utils.parseXml(sig.getSignedXml()); const referenceNode = xpath.select1("/root/name", doc); isDomNode.assertIsNodeLike(referenceNode); @@ -310,7 +313,7 @@ describe("Signature unit tests", function () { }, }); - const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml()); + const doc = utils.parseXml(sig.getSignedXml()); const referenceNode = xpath.select1("/root/name", doc); isDomNode.assertIsNodeLike(referenceNode); const firstChild = referenceNode.firstChild; @@ -341,7 +344,7 @@ describe("Signature unit tests", function () { }, }); - const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml()); + const doc = utils.parseXml(sig.getSignedXml()); const referenceNode = xpath.select1("/root/name", doc); isDomNode.assertIsNodeLike(referenceNode); const previousSibling = referenceNode.previousSibling; @@ -373,7 +376,7 @@ describe("Signature unit tests", function () { }, }); - const doc = new xmldom.DOMParser().parseFromString(sig.getSignedXml()); + const doc = utils.parseXml(sig.getSignedXml()); const referenceNode = xpath.select1("/root/name", doc); isDomNode.assertIsNodeLike(referenceNode); @@ -853,7 +856,7 @@ describe("Signature unit tests", function () { describe("pass loading signatures", function () { function passLoadSignature(file: string, toString?: boolean) { const xml = fs.readFileSync(file, "utf8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const signature = xpath.select1( `/*//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, @@ -933,7 +936,7 @@ describe("Signature unit tests", function () { describe("pass verify signature", function () { function loadSignature(xml: string, idMode?: "wssecurity") { - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const node = xpath.select1( `//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, @@ -1071,7 +1074,7 @@ describe("Signature unit tests", function () { sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.RSA_SHA1; sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); const URI = xpath.select1("//*[local-name(.)='Reference']/@URI", doc); isDomNode.assertIsAttributeNode(URI); expect(URI.value, `uri should be empty but instead was ${URI.value}`).to.equal(""); @@ -1164,7 +1167,7 @@ describe("Signature unit tests", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); const inclusiveNamespaces = xpath.select( "//*[local-name(.)='Reference']/*[local-name(.)='Transforms']/*[local-name(.)='Transform']/*[local-name(.)='InclusiveNamespaces']", doc.documentElement, @@ -1201,7 +1204,7 @@ describe("Signature unit tests", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); const inclusiveNamespaces = xpath.select1( "//*[local-name(.)='Reference']/*[local-name(.)='Transforms']/*[local-name(.)='Transform']/*[local-name(.)='InclusiveNamespaces']", doc.documentElement, @@ -1226,7 +1229,7 @@ describe("Signature unit tests", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); const inclusiveNamespaces = xpath.select( "//*[local-name(.)='CanonicalizationMethod']/*[local-name(.)='InclusiveNamespaces']", doc.documentElement, @@ -1265,7 +1268,7 @@ describe("Signature unit tests", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); const inclusiveNamespaces = xpath.select1( "//*[local-name(.)='CanonicalizationMethod']/*[local-name(.)='InclusiveNamespaces']", doc.documentElement, @@ -1292,7 +1295,7 @@ describe("Signature unit tests", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); const keyInfoElements = xpath.select("//*[local-name(.)='KeyInfo']", doc.documentElement); isDomNode.assertIsArrayOfNodes(keyInfoElements); @@ -1324,7 +1327,7 @@ describe("Signature unit tests", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); const x509certificates = xpath.select( "//*[local-name(.)='X509Certificate']", @@ -1369,7 +1372,7 @@ describe("Signature unit tests", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); const referenceElements = xpath.select("//*[local-name(.)='Reference']", doc); isDomNode.assertIsArrayOfNodes(referenceElements); expect(referenceElements.length, "Reference element should exist").to.equal(1); @@ -1424,7 +1427,7 @@ describe("Signature unit tests", function () { sig.computeSignature(xml); const signedXml = sig.getSignedXml(); - const doc = new xmldom.DOMParser().parseFromString(signedXml); + const doc = utils.parseXml(signedXml); const referenceElements = xpath.select("//*[local-name(.)='Reference']", doc); isDomNode.assertIsArrayOfNodes(referenceElements); expect(referenceElements.length, "Reference element should exist").to.equal(1); diff --git a/test/utils-tests.spec.ts b/test/utils-tests.spec.ts index aa66598d..bb97d2e9 100644 --- a/test/utils-tests.spec.ts +++ b/test/utils-tests.spec.ts @@ -6,6 +6,13 @@ import * as xpath from "xpath"; import * as isDomNode from "@xmldom/is-dom-node"; describe("Utils tests", function () { + describe("parseXml", function () { + it("accepts XML documents with a UTF-8 BOM", function () { + const xml = utils.parseXml("\uFEFF"); + expect(xml.documentElement.localName).to.equal("root"); + }); + }); + describe("derToPem", function () { it("will return a normalized PEM format when given an non-normalized PEM format", function () { const normalizedPem = fs.readFileSync("./test/static/client_public.pem", "latin1"); @@ -45,7 +52,7 @@ describe("Utils tests", function () { }); it("will return a normalized PEM format when given a base64 string with line breaks and spaces at the line breaks", function () { - const xml = new xmldom.DOMParser().parseFromString( + const xml = utils.parseXml( fs.readFileSync("./test/static/keyinfo - pretty-printed.xml", "latin1"), ); const cert = xpath.select1(".//*[local-name(.)='X509Certificate']", xml); @@ -85,19 +92,19 @@ describe("Utils tests", function () { it("should find attribute with no namespace when null is passed as namespace", function () { const xml = ''; - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const rootElement = doc.documentElement; const attr = utils.findAttr(rootElement, "testAttr", null); expect(attr).to.not.be.null; expect(attr?.value).to.equal("value"); - expect(attr?.namespaceURI).to.be.undefined; + expect(attr?.namespaceURI).to.equal(null); }); it("should not find namespaced attribute when null is passed as namespace", function () { const xml = ''; - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const rootElement = doc.documentElement; const attr = utils.findAttr(rootElement, "testAttr", null); @@ -107,7 +114,7 @@ describe("Utils tests", function () { it("should find namespaced attribute when matching namespace is provided", function () { const xml = ''; - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const rootElement = doc.documentElement; const attr = utils.findAttr(rootElement, "testAttr", "http://example.com"); @@ -120,14 +127,14 @@ describe("Utils tests", function () { it("should distinguish between namespaced and non-namespaced attributes with same localName", function () { const xml = ''; - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const rootElement = doc.documentElement; // Find the non-namespaced attribute const noNsAttr = utils.findAttr(rootElement, "testAttr", null); expect(noNsAttr).to.not.be.null; expect(noNsAttr?.value).to.equal("noNsValue"); - expect(noNsAttr?.namespaceURI).to.be.undefined; + expect(noNsAttr?.namespaceURI).to.equal(null); // Find the namespaced attribute const nsAttr = utils.findAttr(rootElement, "testAttr", "http://example.com"); diff --git a/test/wsfed-metadata-tests.spec.ts b/test/wsfed-metadata-tests.spec.ts index 574bfc5d..468edbc6 100644 --- a/test/wsfed-metadata-tests.spec.ts +++ b/test/wsfed-metadata-tests.spec.ts @@ -4,11 +4,12 @@ import * as xmldom from "@xmldom/xmldom"; import * as fs from "fs"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; +import * as utils from "../src/utils"; describe("WS-Fed Metadata tests", function () { it("test validating WS-Fed Metadata", function () { const xml = fs.readFileSync("./test/static/wsfederation_metadata.xml", "utf-8"); - const doc = new xmldom.DOMParser().parseFromString(xml); + const doc = utils.parseXml(xml); const signature = xpath.select1( `/*/*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`, doc, diff --git a/test/xmldsig-verifier.spec.ts b/test/xmldsig-verifier.spec.ts index aa278d63..8dfbd46e 100644 --- a/test/xmldsig-verifier.spec.ts +++ b/test/xmldsig-verifier.spec.ts @@ -2,6 +2,7 @@ import * as fs from "fs"; import { expect } from "chai"; import { XmlDSigVerifier, SignedXml, EnvelopedSignature, XMLDSIG_URIS } from "../src"; import type { XmlDsigVerificationResult } from "../src/"; +import * as utils from "../src/utils"; import { X509Certificate } from "node:crypto"; @@ -824,7 +825,7 @@ describe("XmlDSigVerifier", function () { it("should validate when signatureNode is provided directly", function () { const signedXml = createSignedXml(xml); - const doc = new DOMParser().parseFromString(signedXml, "application/xml"); + const doc = utils.parseXml(signedXml, "application/xml"); const signatureNode = doc.getElementsByTagNameNS(XMLDSIG_URIS.NAMESPACES.ds, "Signature")[0]; const verifier = new XmlDSigVerifier({ @@ -917,7 +918,7 @@ describe("XmlDSigVerifier", function () { location: { reference: "/root", action: "append" }, }); const xmlWithTwoSigs = sig2.getSignedXml(); - const doc = new DOMParser().parseFromString(xmlWithTwoSigs, "application/xml"); + const doc = utils.parseXml(xmlWithTwoSigs, "application/xml"); const signatureNodes = doc.getElementsByTagNameNS(XMLDSIG_URIS.NAMESPACES.ds, "Signature"); expect(signatureNodes.length).to.equal(2); From 1079bb954b1ec46da4b1867580d9f42b0bde048b Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Thu, 5 Mar 2026 13:03:50 -0600 Subject: [PATCH 06/12] Remove unused imports --- test/c14n-non-exclusive-unit-tests.spec.ts | 1 - test/c14nWithComments-unit-tests.spec.ts | 1 - test/canonicalization-unit-tests.spec.ts | 1 - test/document-tests.spec.ts | 1 - test/hmac-tests.spec.ts | 1 - test/key-info-tests.spec.ts | 1 - test/saml-response-tests.spec.ts | 1 - test/signature-integration-tests.spec.ts | 1 - test/signature-object-tests.spec.ts | 1 - test/signature-unit-tests.spec.ts | 5 +---- test/utils-tests.spec.ts | 5 ++--- test/wsfed-metadata-tests.spec.ts | 1 - test/xmldsig-verifier.spec.ts | 1 - 13 files changed, 3 insertions(+), 18 deletions(-) diff --git a/test/c14n-non-exclusive-unit-tests.spec.ts b/test/c14n-non-exclusive-unit-tests.spec.ts index a45be233..9da34fbc 100644 --- a/test/c14n-non-exclusive-unit-tests.spec.ts +++ b/test/c14n-non-exclusive-unit-tests.spec.ts @@ -1,7 +1,6 @@ import { expect } from "chai"; import { C14nCanonicalization } from "../src"; -import * as xmldom from "@xmldom/xmldom"; import * as xpath from "xpath"; import * as utils from "../src/utils"; import * as isDomNode from "@xmldom/is-dom-node"; diff --git a/test/c14nWithComments-unit-tests.spec.ts b/test/c14nWithComments-unit-tests.spec.ts index d7394c2e..db42ff51 100644 --- a/test/c14nWithComments-unit-tests.spec.ts +++ b/test/c14nWithComments-unit-tests.spec.ts @@ -1,7 +1,6 @@ import { expect } from "chai"; import { ExclusiveCanonicalizationWithComments as c14nWithComments } from "../src/exclusive-canonicalization"; -import * as xmldom from "@xmldom/xmldom"; import * as xpath from "xpath"; import { SignedXml, XMLDSIG_URIS } from "../src"; import * as isDomNode from "@xmldom/is-dom-node"; diff --git a/test/canonicalization-unit-tests.spec.ts b/test/canonicalization-unit-tests.spec.ts index fdf71160..b9bad1c5 100644 --- a/test/canonicalization-unit-tests.spec.ts +++ b/test/canonicalization-unit-tests.spec.ts @@ -1,6 +1,5 @@ import { expect } from "chai"; -import * as xmldom from "@xmldom/xmldom"; import * as xpath from "xpath"; import { SignedXml, ExclusiveCanonicalization, XMLDSIG_URIS } from "../src"; import * as isDomNode from "@xmldom/is-dom-node"; diff --git a/test/document-tests.spec.ts b/test/document-tests.spec.ts index e3585032..f7580d0f 100644 --- a/test/document-tests.spec.ts +++ b/test/document-tests.spec.ts @@ -1,6 +1,5 @@ import { SignedXml, XMLDSIG_URIS } from "../src"; import * as xpath from "xpath"; -import * as xmldom from "@xmldom/xmldom"; import * as fs from "fs"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; diff --git a/test/hmac-tests.spec.ts b/test/hmac-tests.spec.ts index a857a395..d9ca6285 100644 --- a/test/hmac-tests.spec.ts +++ b/test/hmac-tests.spec.ts @@ -1,6 +1,5 @@ import { SignedXml, XMLDSIG_URIS } from "../src"; import * as xpath from "xpath"; -import * as xmldom from "@xmldom/xmldom"; import * as fs from "fs"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; diff --git a/test/key-info-tests.spec.ts b/test/key-info-tests.spec.ts index 0bfbf8b5..92402abe 100644 --- a/test/key-info-tests.spec.ts +++ b/test/key-info-tests.spec.ts @@ -1,4 +1,3 @@ -import * as xmldom from "@xmldom/xmldom"; import * as fs from "fs"; import * as xpath from "xpath"; import { SignedXml, XMLDSIG_URIS } from "../src"; diff --git a/test/saml-response-tests.spec.ts b/test/saml-response-tests.spec.ts index 2c417b5b..5151c757 100644 --- a/test/saml-response-tests.spec.ts +++ b/test/saml-response-tests.spec.ts @@ -1,6 +1,5 @@ import { SignedXml, XMLDSIG_URIS } from "../src"; import * as xpath from "xpath"; -import * as xmldom from "@xmldom/xmldom"; import * as fs from "fs"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; diff --git a/test/signature-integration-tests.spec.ts b/test/signature-integration-tests.spec.ts index 52d73fbb..000b97e7 100644 --- a/test/signature-integration-tests.spec.ts +++ b/test/signature-integration-tests.spec.ts @@ -1,5 +1,4 @@ import * as xpath from "xpath"; -import * as xmldom from "@xmldom/xmldom"; import { SignedXml, XMLDSIG_URIS } from "../src"; import * as fs from "fs"; import { expect } from "chai"; diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index 311bfc90..51dd2b08 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -1,7 +1,6 @@ import * as fs from "fs"; import { expect, assert } from "chai"; import * as xpath from "xpath"; -import * as xmldom from "@xmldom/xmldom"; import * as isDomNode from "@xmldom/is-dom-node"; import { SignedXml, XMLDSIG_URIS } from "../src"; import { Sha256 } from "../src/hash-algorithms"; diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index 216acee6..199899f8 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -1,5 +1,4 @@ import * as xpath from "xpath"; -import * as xmldom from "@xmldom/xmldom"; import { SignedXml, createOptionalCallbackFunction, XMLDSIG_URIS } from "../src"; import * as fs from "fs"; import * as crypto from "crypto"; @@ -67,9 +66,7 @@ describe("Signature unit tests", function () { isDomNode.assertIsElementNode(node); const targetElement = node as Element; targetElement.setAttribute("attr", "manipulatedValue"); - const manipulatedXml = new xmldom.XMLSerializer().serializeToString( - doc as unknown as xmldom.Node, - ); + const manipulatedXml = doc.toString(); const sig = loadSignature(manipulatedXml); const res = sig.checkSignature(manipulatedXml); diff --git a/test/utils-tests.spec.ts b/test/utils-tests.spec.ts index bb97d2e9..d21999c7 100644 --- a/test/utils-tests.spec.ts +++ b/test/utils-tests.spec.ts @@ -1,7 +1,6 @@ import * as fs from "fs"; import * as utils from "../src/utils"; import { expect } from "chai"; -import * as xmldom from "@xmldom/xmldom"; import * as xpath from "xpath"; import * as isDomNode from "@xmldom/is-dom-node"; @@ -99,7 +98,7 @@ describe("Utils tests", function () { expect(attr).to.not.be.null; expect(attr?.value).to.equal("value"); - expect(attr?.namespaceURI).to.equal(null); + expect(attr?.namespaceURI).to.be.undefined; }); it("should not find namespaced attribute when null is passed as namespace", function () { @@ -134,7 +133,7 @@ describe("Utils tests", function () { const noNsAttr = utils.findAttr(rootElement, "testAttr", null); expect(noNsAttr).to.not.be.null; expect(noNsAttr?.value).to.equal("noNsValue"); - expect(noNsAttr?.namespaceURI).to.equal(null); + expect(noNsAttr?.namespaceURI).to.be.undefined; // Find the namespaced attribute const nsAttr = utils.findAttr(rootElement, "testAttr", "http://example.com"); diff --git a/test/wsfed-metadata-tests.spec.ts b/test/wsfed-metadata-tests.spec.ts index 468edbc6..f29e2502 100644 --- a/test/wsfed-metadata-tests.spec.ts +++ b/test/wsfed-metadata-tests.spec.ts @@ -1,6 +1,5 @@ import { SignedXml, XMLDSIG_URIS } from "../src"; import * as xpath from "xpath"; -import * as xmldom from "@xmldom/xmldom"; import * as fs from "fs"; import { expect } from "chai"; import * as isDomNode from "@xmldom/is-dom-node"; diff --git a/test/xmldsig-verifier.spec.ts b/test/xmldsig-verifier.spec.ts index 8dfbd46e..907b1aa9 100644 --- a/test/xmldsig-verifier.spec.ts +++ b/test/xmldsig-verifier.spec.ts @@ -7,7 +7,6 @@ import * as utils from "../src/utils"; import { X509Certificate } from "node:crypto"; // Parse the XML and get both signature nodes -import { DOMParser } from "@xmldom/xmldom"; const { CANONICALIZATION_ALGORITHMS, HASH_ALGORITHMS, SIGNATURE_ALGORITHMS, TRANSFORM_ALGORITHMS } = XMLDSIG_URIS; From 260850f2ac57b0800e66133d54b9176db719df57 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Thu, 5 Mar 2026 13:10:57 -0600 Subject: [PATCH 07/12] Lint --- test/utils-tests.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils-tests.spec.ts b/test/utils-tests.spec.ts index d21999c7..342f89e4 100644 --- a/test/utils-tests.spec.ts +++ b/test/utils-tests.spec.ts @@ -7,7 +7,7 @@ import * as isDomNode from "@xmldom/is-dom-node"; describe("Utils tests", function () { describe("parseXml", function () { it("accepts XML documents with a UTF-8 BOM", function () { - const xml = utils.parseXml("\uFEFF"); + const xml = utils.parseXml('\uFEFF'); expect(xml.documentElement.localName).to.equal("root"); }); }); From b64e74e574a036378b0b41f2eb199bbce9650390 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Thu, 5 Mar 2026 14:28:21 -0600 Subject: [PATCH 08/12] Adjust some test names to better reflect what is being tested --- test/xmldsig-verifier.spec.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/xmldsig-verifier.spec.ts b/test/xmldsig-verifier.spec.ts index 907b1aa9..f52f30b7 100644 --- a/test/xmldsig-verifier.spec.ts +++ b/test/xmldsig-verifier.spec.ts @@ -124,14 +124,14 @@ describe("XmlDSigVerifier", function () { const xml = "content"; describe("constructor", function () { - it("should create verifier with public certificate", function () { + it("constructs when keySelector.publicCert is provided", function () { const verifier = new XmlDSigVerifier({ keySelector: { publicCert }, }); expect(verifier).to.be.instanceOf(XmlDSigVerifier); }); - it("should create verifier with getCertFromKeyInfo function", function () { + it("constructs when keySelector.getCertFromKeyInfo is a function", function () { const verifier = new XmlDSigVerifier({ keySelector: { getCertFromKeyInfo: () => publicCert, @@ -140,14 +140,14 @@ describe("XmlDSigVerifier", function () { expect(verifier).to.be.instanceOf(XmlDSigVerifier); }); - it("should throw when trying to create a verifier without publicCert or getCertFromKeyInfo", function () { + it("throws when keySelector has neither publicCert nor getCertFromKeyInfo", function () { expect(() => { // eslint-disable-next-line @typescript-eslint/no-explicit-any new XmlDSigVerifier({ keySelector: {} as any }); }).to.throw("XmlDSigVerifier requires a valid keySelector option"); }); - it("should create verifier with all options set", function () { + it("constructs when all supported constructor options are provided", function () { const verifier = new XmlDSigVerifier({ keySelector: { publicCert }, idAttributes: ["customId"], @@ -165,7 +165,7 @@ describe("XmlDSigVerifier", function () { expect(verifier).to.be.instanceOf(XmlDSigVerifier); }); - it("should throw when getCertFromKeyInfo is undefined", function () { + it("throws when keySelector.getCertFromKeyInfo is undefined", function () { expect(() => { new XmlDSigVerifier({ keySelector: { @@ -175,7 +175,7 @@ describe("XmlDSigVerifier", function () { }).to.throw("XmlDSigVerifier requires a valid getCertFromKeyInfo function."); }); - it("should throw when getCertFromKeyInfo is set to publicCert string directly", function () { + it("throws when keySelector.getCertFromKeyInfo is not a function (public cert string passed)", function () { expect(() => { new XmlDSigVerifier({ keySelector: { @@ -187,7 +187,7 @@ describe("XmlDSigVerifier", function () { }); describe("publicCert selector", function () { - it("should validate a valid signed XML document", function () { + it("verifies a valid signature using the publicCert selector", function () { const signedXml = createSignedXml(xml); const verifier = new XmlDSigVerifier({ @@ -196,7 +196,7 @@ describe("XmlDSigVerifier", function () { expectValidResult(verifier.verifySignature(signedXml)); }); - it("should validate when publicCert is a buffer", function () { + it("verifies a valid signature when keySelector.publicCert is provided as a Buffer", function () { const signedXml = createSignedXml(xml); const verifier = new XmlDSigVerifier({ @@ -205,7 +205,7 @@ describe("XmlDSigVerifier", function () { expectValidResult(verifier.verifySignature(signedXml)); }); - it("should fail validation when document is signed with different key", function () { + it("returns an invalid result when the signature key does not match keySelector.publicCert", function () { const signedXml = createChainSignedXml(xml); const verifier = new XmlDSigVerifier({ @@ -218,7 +218,7 @@ describe("XmlDSigVerifier", function () { }); describe("getCertFromKeyInfo selector", function () { - it("should validate a valid signed XML document", function () { + it("verifies a valid signature using the getCertFromKeyInfo selector", function () { const signedXml = createSignedXml(xml); const verifier = new XmlDSigVerifier({ @@ -229,7 +229,7 @@ describe("XmlDSigVerifier", function () { expectValidResult(verifier.verifySignature(signedXml)); }); - it("should fail validation when document is signed with different key", function () { + it("returns an invalid result when callback cert does not match the signing key", function () { const signedXml = createChainSignedXml(xml); const verifier = new XmlDSigVerifier({ @@ -242,7 +242,7 @@ describe("XmlDSigVerifier", function () { expectInvalidResult(verifier.verifySignature(signedXml), "invalid signature"); }); - it("should fail validation when getCertFromKeyInfo returns null", function () { + it("returns an invalid result when getCertFromKeyInfo returns null", function () { const signedXml = createSignedXml(xml); const verifier = new XmlDSigVerifier({ @@ -255,7 +255,7 @@ describe("XmlDSigVerifier", function () { expectInvalidResult(verifier.verifySignature(signedXml), "keyinfo"); }); - it("should fail validation when getCertFromKeyInfo returns empty string", function () { + it("returns an invalid result when getCertFromKeyInfo returns an empty string", function () { const signedXml = createSignedXml(xml); const verifier = new XmlDSigVerifier({ From 97b0b94be7198d0f7faecd8904f556a1f0e8a0dd Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Thu, 5 Mar 2026 15:16:01 -0600 Subject: [PATCH 09/12] Improve test coverage and make tests match descriptions This exposes a runtime gap that the type system was preventing. --- test/xmldsig-verifier.spec.ts | 116 ++++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 20 deletions(-) diff --git a/test/xmldsig-verifier.spec.ts b/test/xmldsig-verifier.spec.ts index f52f30b7..19043d5a 100644 --- a/test/xmldsig-verifier.spec.ts +++ b/test/xmldsig-verifier.spec.ts @@ -28,6 +28,10 @@ const expiredCert = fs.readFileSync("./test/static/expired_certificate.crt.pem", const futureKey = fs.readFileSync("./test/static/future_certificate.key.pem", "utf-8"); const futureCert = fs.readFileSync("./test/static/future_certificate.crt.pem", "utf-8"); +// Shared-secret keys for HMAC verification testing +const hmacKey = fs.readFileSync("./test/static/hmac.key"); +const wrongHmacKey = fs.readFileSync("./test/static/hmac-foobar.key"); + // Helper function to create a signed XML document function createSignedXml( xml: string, @@ -104,6 +108,23 @@ function createFutureSignedXml(xml: string): string { return sig.getSignedXml(); } +function createHmacSignedXml(xml: string): string { + const sig = new SignedXml(); + sig.enableHMAC(); + sig.privateKey = hmacKey; + sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; + sig.signatureAlgorithm = SIGNATURE_ALGORITHMS.HMAC_SHA1; + + sig.addReference({ + xpath: "//*[local-name(.)='test']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + + sig.computeSignature(xml); + return sig.getSignedXml(); +} + function expectValidResult(result: XmlDsigVerificationResult, references: number = 1) { expect(result.success).to.be.true; expect(result.error).to.be.undefined; @@ -269,6 +290,41 @@ describe("XmlDSigVerifier", function () { }); }); + describe("sharedSecretKey selector", function () { + it("verifies a valid HMAC signature using the sharedSecretKey selector", function () { + const signedXml = createHmacSignedXml(xml); + + const verifier = new XmlDSigVerifier({ + keySelector: { sharedSecretKey: hmacKey }, + }); + + expectValidResult(verifier.verifySignature(signedXml)); + }); + + it("returns an invalid result when sharedSecretKey does not match the signing key", function () { + const signedXml = createHmacSignedXml(xml); + + const verifier = new XmlDSigVerifier({ + keySelector: { sharedSecretKey: wrongHmacKey }, + throwOnError: false, + }); + + expectInvalidResult(verifier.verifySignature(signedXml), "invalid signature"); + }); + + it("returns an invalid result when HMAC signature algorithm is not allowed", function () { + const signedXml = createHmacSignedXml(xml); + + const verifier = new XmlDSigVerifier({ + keySelector: { sharedSecretKey: hmacKey }, + throwOnError: false, + security: { signatureAlgorithms: [] }, + }); + + expectInvalidResult(verifier.verifySignature(signedXml), "signature algorithm"); + }); + }); + describe("idAttributes option", function () { const xmlWithCustomId = 'content'; const xmlWithPrefixedId = `content`; @@ -359,7 +415,7 @@ describe("XmlDSigVerifier", function () { idAttributes: [{ localName: "customId", namespaceUri: "uri:bar" }], throwOnError: false, }); - expectInvalidResult(verifier.verifySignature(signedXml), "fail"); + expectInvalidResult(verifier.verifySignature(signedXml), "verification failed"); }); it("should fail validation when Id attribute is not namespaced but namespaceUri is provided", function () { @@ -382,7 +438,7 @@ describe("XmlDSigVerifier", function () { idAttributes: [{ localName: "customId", namespaceUri: "uri:foo" }], throwOnError: false, }); - expectInvalidResult(verifier.verifySignature(signedXml), "fail"); + expectInvalidResult(verifier.verifySignature(signedXml), "verification failed"); }); describe("idAttributes property handling", function () { @@ -618,24 +674,24 @@ describe("XmlDSigVerifier", function () { }); describe("checkCertExpiration", function () { - it("should validate when certificate is not expired and checkCertExpiration is true", function () { - const signedXml = createSignedXml(xml); - // @ts-expect-error -- ignore for test purposes - const verifier = new XmlDSigVerifier({ - keySelector: { publicCert }, - security: { checkCertExpiration: true }, - }); - expectValidResult(verifier.verifySignature(signedXml)); - }); - - it("should validate when certificate is expired and checkCertExpiration is false", function () { - const signedXml = createExpiredSignedXml(xml); - // @ts-expect-error -- ignore for test purposes - const verifier = new XmlDSigVerifier({ - keySelector: { publicCert: expiredCert }, - security: { checkCertExpiration: false }, - }); - expectValidResult(verifier.verifySignature(signedXml)); + it("should reject checkCertExpiration when used with publicCert selector (true)", function () { + expect(() => { + // @ts-expect-error -- checkCertExpiration is currently only typed for keyinfo selector + new XmlDSigVerifier({ + keySelector: { publicCert }, + security: { checkCertExpiration: true }, + }); + }).to.throw("checkCertExpiration is only supported with getCertFromKeyInfo"); + }); + + it("should reject checkCertExpiration when used with publicCert selector (false)", function () { + expect(() => { + // @ts-expect-error -- checkCertExpiration is currently only typed for keyinfo selector + new XmlDSigVerifier({ + keySelector: { publicCert: expiredCert }, + security: { checkCertExpiration: false }, + }); + }).to.throw("checkCertExpiration is only supported with getCertFromKeyInfo"); }); it("should fail validation when certificate is expired and checkCertExpiration is true", function () { @@ -658,6 +714,26 @@ describe("XmlDSigVerifier", function () { }); describe("truststore", function () { + it("should reject truststore when used with publicCert selector", function () { + expect(() => { + // @ts-expect-error -- truststore is currently only typed for keyinfo selector + new XmlDSigVerifier({ + keySelector: { publicCert }, + security: { truststore: [rootCert] }, + }); + }).to.throw("truststore is only supported with getCertFromKeyInfo"); + }); + + it("should reject truststore when used with sharedSecretKey selector", function () { + expect(() => { + // @ts-expect-error -- truststore is currently only typed for keyinfo selector + new XmlDSigVerifier({ + keySelector: { sharedSecretKey: hmacKey }, + security: { truststore: [rootCert] }, + }); + }).to.throw("truststore is only supported with getCertFromKeyInfo"); + }); + it("should validate when certificate is exactly in truststore", function () { const signedXml = createSignedXml(xml); const verifier = new XmlDSigVerifier({ From cd83d8e46e4ac403b8e2f8cdaeed24e1fb030a43 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Thu, 5 Mar 2026 16:14:19 -0600 Subject: [PATCH 10/12] Add some todos and some more test coverage --- .nvmrc | 1 - src/exclusive-canonicalization.ts | 3 +-- src/signed-xml.ts | 5 +++++ src/xmldsig-verifier.ts | 3 +++ test/signature-integration-tests.spec.ts | 4 ++-- test/signature-unit-tests.spec.ts | 20 ++++++++++++++++++++ test/xmldsig-verifier.spec.ts | 15 +++++++++++++++ 7 files changed, 46 insertions(+), 5 deletions(-) delete mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index aebd91c5..00000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v22.16.0 diff --git a/src/exclusive-canonicalization.ts b/src/exclusive-canonicalization.ts index 12ee4565..c1b26de7 100644 --- a/src/exclusive-canonicalization.ts +++ b/src/exclusive-canonicalization.ts @@ -267,8 +267,7 @@ export class ExclusiveCanonicalization implements CanonicalizationAlgorithm { * * @api public */ - process(elem: Element, options: TransformAlgorithmOptions): string { - options = options || {}; + process(elem: Element, options: TransformAlgorithmOptions = {}): string { let inclusiveNamespacesPrefixList = options.inclusiveNamespacesPrefixList || []; const defaultNs = options.defaultNs || ""; const defaultNsForPrefix = options.defaultNsForPrefix || {}; diff --git a/src/signed-xml.ts b/src/signed-xml.ts index b6de2dd9..c70516db 100644 --- a/src/signed-xml.ts +++ b/src/signed-xml.ts @@ -163,6 +163,7 @@ export class SignedXml { }); static readonly getDefaultSymmetricSignatureAlgorithms = (): SignatureAlgorithmMap => ({ + // TODO: add HMAC-SHA256 support and make it the default instead of HMAC-SHA1. [SIGNATURE_ALGORITHMS.HMAC_SHA1]: signatureAlgorithms.HmacSha1, }); @@ -1435,6 +1436,10 @@ export class SignedXml { `${firstIdAttr.prefix}:${firstIdAttr.localName}`, id, ); + } else if (typeof firstIdAttr.namespaceUri === "string") { + throw new Error( + `Invalid idAttributes[0]: prefix is required when namespaceUri is provided (${firstIdAttr.localName}).`, + ); } else { node.setAttribute(firstIdAttr.localName, id); } diff --git a/src/xmldsig-verifier.ts b/src/xmldsig-verifier.ts index c6989b7d..23f378fb 100644 --- a/src/xmldsig-verifier.ts +++ b/src/xmldsig-verifier.ts @@ -106,13 +106,16 @@ export class XmlDSigVerifier { public static readonly DEFAULT_CHECK_CERT_EXPIRATION = true; public static readonly DEFAULT_THROW_ON_ERROR = false; + // TODO(v7): remove SHA-1 from default hash algorithms. static readonly defaultHashAlgorithms = [Sha1, Sha256, Sha512]; static readonly defaultAsymmetricSignatureAlgorithms = [ + // TODO(v7): remove RSA-SHA1 from default signature algorithms. RsaSha1, RsaSha256, RsaSha256Mgf1, RsaSha512, ]; + // TODO: add HMAC-SHA256 support and make it the default instead of HMAC-SHA1. static readonly defaultSymmetricSignatureAlgorithms = [HmacSha1]; static readonly defaultCanonicalizationAlgorithms = [ C14nCanonicalization, diff --git a/test/signature-integration-tests.spec.ts b/test/signature-integration-tests.spec.ts index 000b97e7..e5e9ef9e 100644 --- a/test/signature-integration-tests.spec.ts +++ b/test/signature-integration-tests.spec.ts @@ -6,11 +6,11 @@ import * as isDomNode from "@xmldom/is-dom-node"; import * as utils from "../src/utils"; describe("Signature integration tests", function () { - function verifySignature(xml, expected, xpath, canonicalizationAlgorithm) { + function verifySignature(xml, expected, xpathQueries, canonicalizationAlgorithm) { const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); - xpath.forEach(function (n) { + xpathQueries.forEach(function (n) { sig.addReference({ xpath: n, digestAlgorithm: XMLDSIG_URIS.HASH_ALGORITHMS.SHA1, diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index 199899f8..5403f862 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -1437,4 +1437,24 @@ describe("Signature unit tests", function () { "#unique-id", ); }); + + it("should throw when idAttributes namespaceUri is provided without prefix during signing", () => { + const xml = ""; + const sig = new SignedXml({ + privateKey: fs.readFileSync("./test/static/client.pem"), + canonicalizationAlgorithm: CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N, + signatureAlgorithm: SIGNATURE_ALGORITHMS.RSA_SHA1, + idAttributes: [{ localName: "customId", namespaceUri: "urn:test" }], + }); + + sig.addReference({ + xpath: "//*[local-name(.)='x']", + digestAlgorithm: HASH_ALGORITHMS.SHA1, + transforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], + }); + + expect(() => sig.computeSignature(xml)).to.throw( + /prefix is required when namespaceUri is provided/, + ); + }); }); diff --git a/test/xmldsig-verifier.spec.ts b/test/xmldsig-verifier.spec.ts index 19043d5a..5f95fbc1 100644 --- a/test/xmldsig-verifier.spec.ts +++ b/test/xmldsig-verifier.spec.ts @@ -250,6 +250,21 @@ describe("XmlDSigVerifier", function () { expectValidResult(verifier.verifySignature(signedXml)); }); + it("does not carry verification state between sequential verifySignature calls", function () { + const signedXml = createChainSignedXml(xml); + const tamperedXml = signedXml.replace("content", "tampered"); + + const verifier = new XmlDSigVerifier({ + keySelector: { + getCertFromKeyInfo: () => chainPublicCert, + }, + throwOnError: false, + }); + + expectValidResult(verifier.verifySignature(signedXml)); + expectInvalidResult(verifier.verifySignature(tamperedXml), "verification failed"); + }); + it("returns an invalid result when callback cert does not match the signing key", function () { const signedXml = createChainSignedXml(xml); From ad260c2894b468a8e6fe92ea2c256e3361e94335 Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Thu, 5 Mar 2026 17:26:11 -0600 Subject: [PATCH 11/12] Clean up some more tests --- test/signature-object-tests.spec.ts | 2 +- test/signature-unit-tests.spec.ts | 82 ++++++++++++++++------------- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/test/signature-object-tests.spec.ts b/test/signature-object-tests.spec.ts index 51dd2b08..e55ade30 100644 --- a/test/signature-object-tests.spec.ts +++ b/test/signature-object-tests.spec.ts @@ -203,7 +203,7 @@ describe("ds:Object support in XML signatures", function () { expect(objectNodesWithEmpty.length).to.equal(0); }); - it("should handle Reference to Object", function () { + it("should serialize Reference to Object with expected URI/transform/digest fields", function () { const xml = ""; const sig = new SignedXml({ diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts index 5403f862..c1a77223 100644 --- a/test/signature-unit-tests.spec.ts +++ b/test/signature-unit-tests.spec.ts @@ -15,6 +15,7 @@ const signatureAlgorithms = [ SIGNATURE_ALGORITHMS.RSA_SHA256_MGF1, SIGNATURE_ALGORITHMS.RSA_SHA512, ]; +// TODO: Revisit/expand the signature algorithm matrix when SHA1 is no longer the default in signing flows. describe("Signature unit tests", function () { describe("sign and verify", function () { @@ -768,7 +769,7 @@ describe("Signature unit tests", function () { expect(expected, "wrong signature format").to.equal(signedXml); }); - it("signer creates correct signature values using async callback", function () { + it("signer creates correct signature values using async callback", function (done) { class DummySignatureAlgorithm { verifySignature = function () { return true; @@ -812,40 +813,49 @@ describe("Signature unit tests", function () { }); sig.canonicalizationAlgorithm = CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N; - sig.computeSignature(xml, function () { - const signedXml = sig.getSignedXml(); - const expected = - '' + - '' + - "" + - '' + - '' + - '' + - "" + - '' + - '' + - "b5GCZ2xpP5T7tbLWBTkOl4CYupQ=" + - "" + - '' + - "" + - '' + - "" + - '' + - "4Pq/sBri+AyOtxtSFsPSOyylyzk=" + - "" + - '' + - "" + - '' + - "" + - '' + - "6I7SDu1iV2YOajTlf+iMLIBfLnE=" + - "" + - "" + - "NejzGB9MDUddKCt3GL2vJhEd5q6NBuhLdQc3W4bJI5q34hk7Hk6zBRoW3OliX+/f7Hpi9y0INYoqMSUfrsAVm3IuPzUETKlI6xiNZo07ULRj1DwxRo6cU66ar1EKUQLRuCZas795FjB8jvUI2lyhcax/00uMJ+Cjf4bwAQ+9gOQ=" + - "" + - ""; - - expect(expected, "wrong signature format").to.equal(signedXml); + sig.computeSignature(xml, function (err) { + if (err) { + done(err); + return; + } + try { + const signedXml = sig.getSignedXml(); + const expected = + '' + + '' + + "" + + '' + + '' + + '' + + "" + + '' + + '' + + "b5GCZ2xpP5T7tbLWBTkOl4CYupQ=" + + "" + + '' + + "" + + '' + + "" + + '' + + "4Pq/sBri+AyOtxtSFsPSOyylyzk=" + + "" + + '' + + "" + + '' + + "" + + '' + + "6I7SDu1iV2YOajTlf+iMLIBfLnE=" + + "" + + "" + + "NejzGB9MDUddKCt3GL2vJhEd5q6NBuhLdQc3W4bJI5q34hk7Hk6zBRoW3OliX+/f7Hpi9y0INYoqMSUfrsAVm3IuPzUETKlI6xiNZo07ULRj1DwxRo6cU66ar1EKUQLRuCZas795FjB8jvUI2lyhcax/00uMJ+Cjf4bwAQ+9gOQ=" + + "" + + ""; + + expect(expected, "wrong signature format").to.equal(signedXml); + done(); + } catch (assertionErr) { + done(assertionErr); + } }); }); @@ -1182,7 +1192,7 @@ describe("Signature unit tests", function () { ).to.equal("prefix1 prefix2"); }); - it("does not create InclusiveNamespaces element when inclusiveNamespacesPrefixList is not set on Reference", function () { + it("does not create InclusiveNamespaces element when inclusiveNamespacesPrefixList is empty on Reference", function () { const xml = ""; const sig = new SignedXml(); sig.privateKey = fs.readFileSync("./test/static/client.pem"); From 04b411a89c73e4b89c99a5bd798c8e7f994acb2c Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Fri, 6 Mar 2026 10:00:25 -0600 Subject: [PATCH 12/12] Fix tests --- src/xmldsig-verifier.ts | 10 ++++++++++ test/xmldsig-verifier.spec.ts | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/xmldsig-verifier.ts b/src/xmldsig-verifier.ts index 23f378fb..84eef5a2 100644 --- a/src/xmldsig-verifier.ts +++ b/src/xmldsig-verifier.ts @@ -223,6 +223,16 @@ export class XmlDSigVerifier { } private static resolveOptions(options: XmlDSigVerifierOptions): ResolvedXmlDsigVerifierOptions { + if (!isKeyInfoSelector(options)) { + const security = options.security as Record | undefined; + if (security?.checkCertExpiration != null) { + throw new Error("checkCertExpiration is only supported with getCertFromKeyInfo"); + } + if (security?.truststore != null) { + throw new Error("truststore is only supported with getCertFromKeyInfo"); + } + } + const defaults = { idAttributes: SignedXml.getDefaultIdAttributes(), maxTransforms: XmlDSigVerifier.DEFAULT_MAX_TRANSFORMS, diff --git a/test/xmldsig-verifier.spec.ts b/test/xmldsig-verifier.spec.ts index 5f95fbc1..660db896 100644 --- a/test/xmldsig-verifier.spec.ts +++ b/test/xmldsig-verifier.spec.ts @@ -170,7 +170,9 @@ describe("XmlDSigVerifier", function () { it("constructs when all supported constructor options are provided", function () { const verifier = new XmlDSigVerifier({ - keySelector: { publicCert }, + keySelector: { + getCertFromKeyInfo: () => publicCert, + }, idAttributes: ["customId"], implicitTransforms: [CANONICALIZATION_ALGORITHMS.EXCLUSIVE_C14N], throwOnError: true,