Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5200d71
feat: add XmlDSigVerifier wrapper for SignedXml
shunkica Oct 25, 2025
92eb417
fix: Support nested enveloped signature location (#525)
shunkica Dec 28, 2025
23d8b14
refactor: clean up types and use array-based algorithm registration
shunkica Mar 5, 2026
41bab4a
fix: improve getCertFromKeyInfo docs and normalize thrown errors
shunkica Mar 5, 2026
920d0d4
Make tests pass
cjbarth Mar 5, 2026
1079bb9
Remove unused imports
cjbarth Mar 5, 2026
260850f
Lint
cjbarth Mar 5, 2026
b64e74e
Adjust some test names to better reflect what is being tested
cjbarth Mar 5, 2026
97b0b94
Improve test coverage and make tests match descriptions
cjbarth Mar 5, 2026
cd83d8e
Add some todos and some more test coverage
cjbarth Mar 5, 2026
ad260c2
Clean up some more tests
cjbarth Mar 5, 2026
04b411a
Fix tests
cjbarth Mar 6, 2026
f90f841
Require non-empty truststore for getCertFromKeyInfo; surface accepted…
shunkica May 15, 2026
4ab6377
Add XmlDSigVerifier.extractAndVerify for deferred-trust verification
shunkica May 15, 2026
95568a2
Document trust model and deferred-trust verification
shunkica May 15, 2026
eeb6e8c
Refactor attr namespace helpers to three-way branching
shunkica May 15, 2026
c258b31
Throw a clear error when ensureHasId hits empty idAttributes
shunkica May 15, 2026
dc6929a
Tighten verifier paths and dedupe security-options plumbing
shunkica May 15, 2026
c9a39c7
Pass the validated cert downstream, not the raw KeyInfo string
shunkica May 18, 2026
16520bc
Fix verifier state reuse and enforce key-confusion guards on all paths
shunkica Jul 14, 2026
34e739f
Reset per-signature state in loadSignature
shunkica Jul 14, 2026
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
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