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
275 changes: 275 additions & 0 deletions XMLDSIG_VERIFIER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
# 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.
- **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

Ensure you have `xml-crypto` installed:

```bash
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

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);
}
```

### 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`:

```typescript
const verifier = new XmlDSigVerifier({
keySelector: { publicCert: myPublicCert },
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: KeyLike } // Direct public key/cert
| { getCertFromKeyInfo: (node?: Node | null) => string | null } // Extract certificate from XML
| { sharedSecretKey: KeyLike }; // HMAC shared secret

// CONFIGURATION
idAttributes?: VerificationIdAttributeType[]; // Default: ["Id", "ID", "id"]
implicitTransforms?: ReadonlyArray<TransformAlgorithmURI>; // Hidden transforms to apply
throwOnError?: boolean; // Default: false (returns result object)

// SECURITY
security?: {
maxTransforms?: number; // Limit transforms per reference (DoS protection). Default: 4
signatureAlgorithms?: Array<new () => SignatureAlgorithm>; // Allowed signature algorithms
hashAlgorithms?: Array<new () => HashAlgorithm>; // Allowed hash algorithms
transformAlgorithms?: Array<new () => TransformAlgorithm>; // Allowed transform algorithms
canonicalizationAlgorithms?: Array<new () => CanonicalizationAlgorithm>; // Allowed canonicalization algorithms

// KeyInfo-only options (only available with getCertFromKeyInfo selector):
checkCertExpiration?: boolean; // Check NotBefore/NotAfter. Default: true
truststore?: Array<string | Buffer | X509Certificate>; // 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:

```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);
}
```

### 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.

```typescript
import { DOMParser } from "@xmldom/xmldom";

const doc = new DOMParser().parseFromString(xml, "application/xml");
const signatures = doc.getElementsByTagNameNS(XMLDSIG_URIS.NAMESPACES.ds, "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
Loading
Loading