Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v22.16.0
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(#).
Expand Down
157 changes: 157 additions & 0 deletions XMLDSIG_VERIFIER.md
Original file line number Diff line number Diff line change
@@ -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 `<KeyInfo>` 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<string, any>;
hashAlgorithms?: Record<string, any>;
// ...
};
}
```

### 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],
);
```
15 changes: 8 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 11 additions & 8 deletions src/c14n-canonicalization.ts
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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 || {};
Expand All @@ -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;
}
}

Expand All @@ -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;
}
}
18 changes: 10 additions & 8 deletions src/enveloped-signature.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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 : []) {
Expand All @@ -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;
}
}
Loading