Skip to content

Commit 5200d71

Browse files
committed
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<URI,Algorithm> 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)
1 parent b673581 commit 5200d71

40 files changed

+2705
-517
lines changed

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v22.16.0

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,31 @@ Note:
209209

210210
The xml-crypto api requires you to supply it separately the xml signature ("&lt;Signature&gt;...&lt;/Signature&gt;", 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.
211211

212+
### Secure Verification with XmlDSigVerifier (Recommended)
213+
214+
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.
215+
216+
```javascript
217+
const { XmlDSigVerifier } = require("xml-crypto");
218+
const fs = require("fs");
219+
220+
const xml = fs.readFileSync("signed.xml", "utf-8");
221+
const publicCert = fs.readFileSync("client_public.pem", "utf-8");
222+
223+
const result = XmlDSigVerifier.verifySignature(xml, {
224+
keySelector: { publicCert: publicCert },
225+
});
226+
227+
if (result.success) {
228+
console.log("Valid signature!");
229+
console.log("Signed content:", result.signedReferences);
230+
} else {
231+
console.error("Invalid signature:", result.error);
232+
}
233+
```
234+
235+
For detailed usage instructions, see [XMLDSIG_VERIFIER.md](./XMLDSIG_VERIFIER.md).
236+
212237
### Caring for Implicit transform
213238

214239
If you fail to verify signed XML, then one possible cause is that there are some hidden implicit transforms(#).

XMLDSIG_VERIFIER.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# XmlDSigVerifier Usage Guide
2+
3+
`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.
4+
5+
## Features
6+
7+
- **Type-Safe Configuration:** Explicit options for different key retrieval strategies (Public Certificate, KeyInfo, Shared Secret).
8+
- **Enhanced Security:** Built-in checks for certificate expiration, truststore validation, and limits on transform complexity.
9+
- **Flexible Error Handling:** Choose between throwing errors or returning a result object.
10+
11+
## Installation
12+
13+
Ensure you have `xml-crypto` installed:
14+
15+
```bash
16+
npm install xml-crypto
17+
```
18+
19+
## Quick Start
20+
21+
### 1. Verifying with a Public Certificate
22+
23+
If you already have the public certificate or key and want to verify a document signed with the corresponding private key:
24+
25+
```typescript
26+
import { XmlDSigVerifier } from "xml-crypto";
27+
import * as fs from "fs";
28+
29+
const xml = fs.readFileSync("signed_document.xml", "utf-8");
30+
const publicCert = fs.readFileSync("public_key.pem", "utf-8");
31+
32+
const result = XmlDSigVerifier.verifySignature(xml, {
33+
keySelector: { publicCert: publicCert },
34+
});
35+
36+
if (result.success) {
37+
console.log("Signature valid!");
38+
// Access the signed content securely
39+
console.log("Signed references:", result.signedReferences);
40+
} else {
41+
console.error("Verification failed:", result.error);
42+
}
43+
```
44+
45+
### 2. Verifying using KeyInfo (with Truststore)
46+
47+
When the XML document contains the certificate in a `<KeyInfo>` element, you can verify it while ensuring the certificate is trusted and valid.
48+
49+
```typescript
50+
import { XmlDSigVerifier, SignedXml } from "xml-crypto";
51+
import * as fs from "fs";
52+
53+
const xml = fs.readFileSync("signed_with_keyinfo.xml", "utf-8");
54+
const trustedRootCert = fs.readFileSync("root_ca.pem", "utf-8");
55+
56+
const result = XmlDSigVerifier.verifySignature(xml, {
57+
keySelector: {
58+
// Extract the certificate from KeyInfo
59+
getCertFromKeyInfo: (keyInfo) => SignedXml.getCertFromKeyInfo(keyInfo),
60+
},
61+
security: {
62+
// Ensure the certificate is trusted by your root CA
63+
truststore: [trustedRootCert],
64+
// Automatically check if the certificate is expired
65+
checkCertExpiration: true,
66+
},
67+
});
68+
69+
if (result.success) {
70+
console.log("Signature is valid and trusted.");
71+
} else {
72+
console.log("Verification failed:", result.error);
73+
}
74+
```
75+
76+
## Advanced Usage
77+
78+
### Reusing the Verifier Instance
79+
80+
For better performance when verifying multiple documents with the same configuration, create an instance of `XmlDSigVerifier`.
81+
82+
```typescript
83+
const verifier = new XmlDSigVerifier({
84+
keySelector: { publicCert: myPublicCert },
85+
// Global security options
86+
security: { maxTransforms: 2 },
87+
});
88+
89+
const result1 = verifier.verifySignature(xml1);
90+
const result2 = verifier.verifySignature(xml2);
91+
```
92+
93+
### Verification Options
94+
95+
The `verifySignature` method accepts an options object with the following structure:
96+
97+
```typescript
98+
interface XmlDSigVerifierOptions {
99+
// STRATEGY: Choose one of the following key selectors
100+
keySelector:
101+
| { publicCert: string | Buffer } // Direct public key/cert
102+
| { getCertFromKeyInfo: (node) => string | null } // Extract from XML
103+
| { sharedSecretKey: string | Buffer }; // HMAC
104+
105+
// CONFIGURATION
106+
idAttributes?: string[]; // e.g., ['Id', 'ID']
107+
throwOnError?: boolean; // Default: false (returns result object)
108+
109+
// SECURITY
110+
security?: {
111+
maxTransforms?: number; // Limit transforms (DoS protection)
112+
checkCertExpiration?: boolean; // Check NotBefore/NotAfter (KeyInfo only)
113+
truststore?: (string | Buffer)[]; // List of trusted CAs (KeyInfo only)
114+
115+
// Algorithm allow-lists
116+
signatureAlgorithms?: Record<string, any>;
117+
hashAlgorithms?: Record<string, any>;
118+
// ...
119+
};
120+
}
121+
```
122+
123+
### Error Handling
124+
125+
By default, `verifySignature` returns a result object. If you prefer to handle exceptions:
126+
127+
```typescript
128+
try {
129+
const result = XmlDSigVerifier.verifySignature(xml, {
130+
keySelector: { publicCert },
131+
throwOnError: true, // Will throw Error on failure
132+
});
133+
// If code reaches here, signature is valid
134+
} catch (e) {
135+
console.error("Signature invalid:", e.message);
136+
}
137+
```
138+
139+
### Handling Multiple Signatures
140+
141+
If a document contains multiple signatures, you must specify which one to verify by passing the signature node.
142+
143+
```typescript
144+
import { DOMParser } from "@xmldom/xmldom";
145+
146+
const doc = new DOMParser().parseFromString(xml, "application/xml");
147+
const signatures = doc.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature");
148+
149+
// Verify the second signature
150+
const result = XmlDSigVerifier.verifySignature(
151+
xml,
152+
{
153+
keySelector: { publicCert },
154+
},
155+
signatures[1],
156+
);
157+
```

package-lock.json

Lines changed: 8 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"dependencies": {
4141
"@xmldom/is-dom-node": "^1.0.1",
4242
"@xmldom/xmldom": "^0.8.10",
43-
"xpath": "^0.0.33"
43+
"xpath": "^0.0.34"
4444
},
4545
"devDependencies": {
4646
"@cjbarth/github-release-notes": "^4.2.0",

src/c14n-canonicalization.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import type {
2-
CanonicalizationOrTransformationAlgorithm,
3-
CanonicalizationOrTransformationAlgorithmProcessOptions,
2+
CanonicalizationAlgorithmURI,
3+
CanonicalizationAlgorithm,
4+
TransformAlgorithmOptions,
45
NamespacePrefix,
56
RenderedNamespace,
67
} from "./types";
78
import * as utils from "./utils";
89
import * as isDomNode from "@xmldom/is-dom-node";
10+
import { XMLDSIG_URIS } from "./xmldsig-uris";
911

10-
export class C14nCanonicalization implements CanonicalizationOrTransformationAlgorithm {
12+
export class C14nCanonicalization implements CanonicalizationAlgorithm {
1113
protected includeComments = false;
1214

1315
constructor() {
@@ -252,9 +254,10 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg
252254
* Perform canonicalization of the given node
253255
*
254256
* @param node
257+
* @param options
255258
* @api public
256259
*/
257-
process(node: Node, options: CanonicalizationOrTransformationAlgorithmProcessOptions): string {
260+
process(node: Node, options: TransformAlgorithmOptions): string {
258261
options = options || {};
259262
const defaultNs = options.defaultNs || "";
260263
const defaultNsForPrefix = options.defaultNsForPrefix || {};
@@ -275,8 +278,8 @@ export class C14nCanonicalization implements CanonicalizationOrTransformationAlg
275278
return res;
276279
}
277280

278-
getAlgorithmName() {
279-
return "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
281+
getAlgorithmName(): CanonicalizationAlgorithmURI {
282+
return XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.C14N;
280283
}
281284
}
282285

@@ -289,7 +292,7 @@ export class C14nCanonicalizationWithComments extends C14nCanonicalization {
289292
this.includeComments = true;
290293
}
291294

292-
getAlgorithmName() {
293-
return "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments";
295+
getAlgorithmName(): CanonicalizationAlgorithmURI {
296+
return XMLDSIG_URIS.CANONICALIZATION_ALGORITHMS.C14N_WITH_COMMENTS;
294297
}
295298
}

src/enveloped-signature.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import * as xpath from "xpath";
22
import * as isDomNode from "@xmldom/is-dom-node";
3-
3+
import { XMLDSIG_URIS } from "./xmldsig-uris";
44
import type {
5-
CanonicalizationOrTransformationAlgorithm,
6-
CanonicalizationOrTransformationAlgorithmProcessOptions,
5+
TransformAlgorithmOptions,
76
CanonicalizationOrTransformAlgorithmType,
7+
TransformAlgorithm,
88
} from "./types";
99

10-
export class EnvelopedSignature implements CanonicalizationOrTransformationAlgorithm {
10+
export class EnvelopedSignature implements TransformAlgorithm {
1111
protected includeComments = false;
1212

1313
constructor() {
1414
this.includeComments = false;
1515
}
1616

17-
process(node: Node, options: CanonicalizationOrTransformationAlgorithmProcessOptions): Node {
17+
process(node: Node, options: TransformAlgorithmOptions): Node {
1818
if (null == options.signatureNode) {
1919
const signature = xpath.select1(
20-
"./*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
20+
`./*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`,
2121
node,
2222
);
2323
if (isDomNode.isNodeLike(signature) && signature.parentNode) {
@@ -34,7 +34,7 @@ export class EnvelopedSignature implements CanonicalizationOrTransformationAlgor
3434
const expectedSignatureValueData = expectedSignatureValue.data;
3535

3636
const signatures = xpath.select(
37-
".//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
37+
`.//*[local-name(.)='Signature' and namespace-uri(.)='${XMLDSIG_URIS.NAMESPACES.ds}']`,
3838
node,
3939
);
4040
for (const nodeSignature of Array.isArray(signatures) ? signatures : []) {
@@ -55,7 +55,9 @@ export class EnvelopedSignature implements CanonicalizationOrTransformationAlgor
5555
return node;
5656
}
5757

58+
// eslint-disable-next-line deprecation/deprecation
5859
getAlgorithmName(): CanonicalizationOrTransformAlgorithmType {
59-
return "http://www.w3.org/2000/09/xmldsig#enveloped-signature";
60+
// TODO: replace with TransformAlgorithmURI in next breaking change
61+
return XMLDSIG_URIS.TRANSFORM_ALGORITHMS.ENVELOPED_SIGNATURE;
6062
}
6163
}

0 commit comments

Comments
 (0)