diff --git a/WEBCRYPTO.md b/WEBCRYPTO.md
new file mode 100644
index 00000000..b3c56996
--- /dev/null
+++ b/WEBCRYPTO.md
@@ -0,0 +1,281 @@
+# WebCrypto Support
+
+This library now supports the Web Crypto API, which allows it to run in browsers and modern Node.js environments **without any Node.js-specific dependencies** for cryptographic operations.
+
+## Overview
+
+The WebCrypto implementation provides:
+
+- **Browser compatibility**: Run XML signing and verification in the browser
+- **No Node.js crypto dependency**: Uses the standard Web Crypto API
+- **Callback-based async operations**: WebCrypto operations use callbacks for async handling
+- **Same API structure**: Uses the same methods as Node.js crypto, just with callbacks
+
+## Supported Algorithms
+
+### Hash Algorithms
+
+- `WebCryptoSha1` - SHA-1 hashing
+- `WebCryptoSha256` - SHA-256 hashing
+- `WebCryptoSha512` - SHA-512 hashing
+
+### Signature Algorithms
+
+- `WebCryptoRsaSha1` - RSA-SHA1 signing/verification
+- `WebCryptoRsaSha256` - RSA-SHA256 signing/verification
+- `WebCryptoRsaSha512` - RSA-SHA512 signing/verification
+- `WebCryptoHmacSha1` - HMAC-SHA1 signing/verification
+
+## Usage
+
+### Basic Example - Signing (Browser or Node.js with WebCrypto)
+
+```javascript
+import { SignedXml, WebCryptoRsaSha256, WebCryptoSha256 } from "xml-crypto";
+
+// Your XML to sign
+const xml = "Hello World";
+
+// Your private key (PEM format)
+const privateKey = `-----BEGIN PRIVATE KEY-----
+...
+-----END PRIVATE KEY-----`;
+
+// Create SignedXml instance
+const sig = new SignedXml();
+
+// Use WebCrypto algorithms
+sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
+sig.privateKey = privateKey;
+
+// Add reference with WebCrypto hash algorithm
+sig.addReference({
+ xpath: "//*[local-name(.)='data']",
+ digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
+ transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
+});
+
+// Register WebCrypto algorithms
+sig.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+sig.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] = WebCryptoRsaSha256;
+
+// Compute signature with callback
+sig.computeSignature(xml, (err, signedXmlObj) => {
+ if (err) {
+ console.error("Signing failed:", err);
+ return;
+ }
+
+ console.log(signedXmlObj.getSignedXml());
+});
+```
+
+### Verifying a Signature
+
+```javascript
+import { SignedXml, WebCryptoRsaSha256, WebCryptoSha256 } from "xml-crypto";
+import { DOMParser } from "@xmldom/xmldom";
+
+const signedXml = `...`; // Your signed XML
+
+const sig = new SignedXml();
+
+// Register WebCrypto algorithms
+sig.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+sig.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] = WebCryptoRsaSha256;
+
+// Provide public key or certificate (SPKI format)
+sig.publicCert = publicKey;
+
+// Load the signature - need to extract it from the signed XML first
+const doc = new DOMParser().parseFromString(signedXml);
+const signature = sig.findSignatures(doc)[0];
+sig.loadSignature(signature);
+
+// Verify with callback
+sig.checkSignature(signedXml, (err, isValid) => {
+ if (err) {
+ console.error("Verification failed:", err);
+ return;
+ }
+ console.log("Signature valid:", isValid);
+});
+```
+
+## Key Format Conversion
+
+The WebCrypto algorithms accept keys in PEM format (strings) and will automatically convert them to `CryptoKey` objects internally.
+
+**Note**: For verification, WebCrypto requires public keys in SPKI format, not X.509 certificates. See the X.509 Certificates section below for how to extract the public key.
+
+## Using Callbacks with WebCrypto
+
+Both `computeSignature` and `checkSignature` support an optional callback parameter. When using WebCrypto algorithms, you **must** provide a callback to handle the asynchronous operations:
+
+```javascript
+// Signing with callback
+sig.computeSignature(xml, (err, signedXmlObj) => {
+ if (err) {
+ console.error("Error:", err);
+ return;
+ }
+ // signedXmlObj is the SignedXml instance
+ const result = signedXmlObj.getSignedXml();
+});
+
+// Verification with callback
+sig.checkSignature(signedXml, (err, isValid) => {
+ if (err) {
+ console.error("Error:", err);
+ return;
+ }
+ console.log("Valid:", isValid);
+});
+```
+
+**Important**: If you try to use WebCrypto algorithms without providing a callback, the operation will fail because WebCrypto operations are inherently asynchronous.
+
+## Browser Compatibility
+
+The WebCrypto API is supported in all modern browsers:
+
+- Chrome/Edge 37+
+- Firefox 34+
+- Safari 11+
+
+## Node.js Compatibility
+
+WebCrypto is available in Node.js 15.0.0+ via the global `crypto.subtle` object. For older Node.js versions, continue using the standard crypto-based algorithms.
+
+## Migration from Node.js Crypto
+
+To migrate from Node.js crypto to WebCrypto:
+
+1. Change algorithm imports:
+
+ ```javascript
+ // Before
+ import { Sha256, RsaSha256 } from "xml-crypto";
+
+ // After
+ import { WebCryptoSha256, WebCryptoRsaSha256 } from "xml-crypto";
+ ```
+
+2. Update to use callbacks:
+
+ ```javascript
+ // Before (synchronous)
+ sig.computeSignature(xml);
+ const result = sig.getSignedXml();
+
+ // After (with callback)
+ sig.computeSignature(xml, (err, signedXmlObj) => {
+ if (err) {
+ console.error(err);
+ return;
+ }
+ const result = signedXmlObj.getSignedXml();
+ });
+ ```
+
+3. Register algorithms:
+ ```javascript
+ sig.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+ sig.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] =
+ WebCryptoRsaSha256;
+ ```
+
+## Limitations
+
+1. **X.509 Certificates**: The Web Crypto API doesn't directly support X.509 certificates. If you have a certificate, you need to extract the public key in SPKI format first:
+
+ ```javascript
+ // In Node.js, you can extract it like this:
+ import { createPublicKey } from "crypto";
+
+ const publicKey = createPublicKey(certificatePem);
+ const spkiPublicKey = publicKey.export({
+ type: "spki",
+ format: "pem",
+ });
+
+ // Use spkiPublicKey with WebCrypto algorithms
+ sig.publicCert = spkiPublicKey;
+ ```
+
+ In browsers, you'll need to prepare the keys in SPKI format beforehand or use a library to parse X.509 certificates.
+
+2. **PEM/DER parsing**: The utility functions provide basic PEM parsing.
+3. **Key formats**: Only PKCS8 private keys and SPKI public keys are currently supported for RSA.
+4. **Callback requirement**: All WebCrypto operations require callbacks - you cannot use them with the synchronous API (without a callback).
+
+## Benefits
+
+- **Zero dependencies on Node.js crypto**: Run in any environment that supports Web Crypto API
+- **Browser support**: Enable XML signing/verification in web applications
+- **Standard API**: Uses the widely-supported Web Crypto API standard
+- **Future-proof**: Web Crypto is the modern standard for cryptography on the web
+
+## Example: Complete Sign and Verify Flow
+
+```javascript
+import { SignedXml, WebCryptoRsaSha256, WebCryptoSha256 } from "xml-crypto";
+import { DOMParser } from "@xmldom/xmldom";
+
+function signAndVerify(callback) {
+ const xml = "Important data";
+
+ // Signing
+ const sigForSigning = new SignedXml();
+ sigForSigning.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
+ sigForSigning.privateKey = privateKeyPem;
+ sigForSigning.addReference({
+ xpath: "//*[local-name(.)='data']",
+ digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
+ transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
+ });
+
+ // Register algorithms
+ sigForSigning.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+ sigForSigning.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] =
+ WebCryptoRsaSha256;
+
+ sigForSigning.computeSignature(xml, (err, signedXmlObj) => {
+ if (err) {
+ return callback(err);
+ }
+
+ const signedXml = signedXmlObj.getSignedXml();
+
+ // Verification
+ const sigForVerifying = new SignedXml();
+ sigForVerifying.publicCert = publicKeyPem; // SPKI format
+
+ // Register algorithms for verification
+ sigForVerifying.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+ sigForVerifying.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] =
+ WebCryptoRsaSha256;
+
+ // Load the signature
+ const doc = new DOMParser().parseFromString(signedXml);
+ const signature = sigForVerifying.findSignatures(doc)[0];
+ sigForVerifying.loadSignature(signature);
+
+ sigForVerifying.checkSignature(signedXml, (err, isValid) => {
+ if (err) {
+ return callback(err);
+ }
+ console.log("Signature is valid:", isValid);
+ callback(null, isValid);
+ });
+ });
+}
+
+signAndVerify((err, isValid) => {
+ if (err) {
+ console.error("Error:", err);
+ } else {
+ console.log("Success! Valid:", isValid);
+ }
+});
+```
diff --git a/example/webcrypto-example.js b/example/webcrypto-example.js
new file mode 100644
index 00000000..58d6b2c1
--- /dev/null
+++ b/example/webcrypto-example.js
@@ -0,0 +1,136 @@
+/**
+ * Example of using xml-crypto with Web Crypto API (callback-based)
+ *
+ * This example demonstrates how to use the WebCrypto implementations
+ * to sign and verify XML signatures using callbacks.
+ *
+ * This works in:
+ * - Modern browsers
+ * - Node.js 15.0.0+
+ * - Deno
+ * - Any environment with Web Crypto API support
+ */
+
+import { SignedXml, WebCryptoRsaSha256, WebCryptoSha256 } from "../lib/index.js";
+import { readFileSync } from "fs";
+import { createPublicKey } from "crypto";
+import { DOMParser } from "@xmldom/xmldom";
+
+/**
+ * Helper function to convert X.509 certificate to SPKI format public key
+ * Note: This uses Node.js crypto for conversion. In a pure browser environment,
+ * you would need to extract the public key beforehand or use a library.
+ */
+function extractPublicKeyFromCertificate(certPem) {
+ try {
+ const publicKey = createPublicKey(certPem);
+ return publicKey.export({
+ type: "spki",
+ format: "pem",
+ });
+ } catch (error) {
+ throw new Error(`Failed to extract public key from certificate: ${error.message}`);
+ }
+}
+
+function signXml(callback) {
+ console.log("=== Signing XML with WebCrypto ===\n");
+
+ const xml = "Harry Potter";
+ console.log("Original XML:", xml);
+
+ // Load private key
+ const privateKey = readFileSync("./example/client.pem", "utf8");
+
+ // Create signature
+ const sig = new SignedXml();
+ sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
+ sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
+ sig.privateKey = privateKey;
+
+ // Add reference
+ sig.addReference({
+ xpath: "//*[local-name(.)='book']",
+ digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
+ transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
+ });
+
+ // Register WebCrypto algorithms
+ sig.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+ sig.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] = WebCryptoRsaSha256;
+
+ // Compute signature with callback
+ sig.computeSignature(xml, (err, signedXmlObj) => {
+ if (err) {
+ return callback(err);
+ }
+
+ const signedXml = signedXmlObj.getSignedXml();
+ console.log("\nSigned XML:", signedXml);
+ callback(null, signedXml);
+ });
+}
+
+function verifyXml(signedXml, callback) {
+ console.log("\n=== Verifying XML Signature with WebCrypto ===\n");
+
+ // Load public certificate and extract the public key in SPKI format
+ const certPem = readFileSync("./example/client_public.pem", "utf8");
+ const publicKeySpki = extractPublicKeyFromCertificate(certPem);
+
+ console.log("Note: Extracted public key from X.509 certificate");
+
+ // Create verification object
+ const sig = new SignedXml();
+ sig.publicCert = publicKeySpki; // Use SPKI format public key
+
+ // Register WebCrypto algorithms
+ sig.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+ sig.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] = WebCryptoRsaSha256;
+
+ // We need to load the signature before verification:
+ // 1. Parse the signed XML to get a DOM
+ // 2. Find the element within it
+ // 3. Load that specific signature node
+ const doc = new DOMParser().parseFromString(signedXml);
+ const signature = sig.findSignatures(doc)[0];
+ sig.loadSignature(signature);
+
+ // Verify with callback
+ sig.checkSignature(signedXml, (err, isValid) => {
+ if (err) {
+ console.error("Signature verification failed:", err.message);
+ return callback(err);
+ }
+ console.log("Signature is valid:", isValid);
+ callback(null, isValid);
+ });
+}
+
+function main() {
+ // Sign the XML
+ signXml((err, signedXml) => {
+ if (err) {
+ console.error("\n❌ Error during signing:", err);
+ process.exit(1);
+ }
+
+ // Verify the signature
+ verifyXml(signedXml, (err, isValid) => {
+ if (err) {
+ console.error("\n❌ Error during verification:", err);
+ process.exit(1);
+ }
+
+ if (isValid) {
+ console.log("\n✅ Success! XML was signed and verified using WebCrypto API");
+ } else {
+ console.log("\n❌ Verification failed");
+ process.exit(1);
+ }
+ });
+ });
+}
+
+// Run the example
+main();
diff --git a/src/hash-algorithms-webcrypto.ts b/src/hash-algorithms-webcrypto.ts
new file mode 100644
index 00000000..761de70d
--- /dev/null
+++ b/src/hash-algorithms-webcrypto.ts
@@ -0,0 +1,124 @@
+import type { ErrorFirstCallback, HashAlgorithm } from "./types";
+
+/**
+ * WebCrypto-based SHA-1 hash algorithm
+ * Uses the Web Crypto API which is available in browsers and modern Node.js
+ */
+export class WebCryptoSha1 implements HashAlgorithm {
+ getHash(xml: string): string;
+ getHash(xml: string, callback: ErrorFirstCallback): void;
+ getHash(xml: string, callback?: ErrorFirstCallback): string | void {
+ if (!callback) {
+ throw new Error(
+ "WebCrypto hash algorithms are async and require a callback. Use getHash(xml, callback).",
+ );
+ }
+
+ const encoder = new TextEncoder();
+ const data = encoder.encode(xml);
+ crypto.subtle
+ .digest("SHA-1", data)
+ .then((hashBuffer) => {
+ const hash = this.arrayBufferToBase64(hashBuffer);
+ callback(null, hash);
+ })
+ .catch((err) => {
+ callback(err);
+ });
+ }
+
+ getAlgorithmName = (): string => {
+ return "http://www.w3.org/2000/09/xmldsig#sha1";
+ };
+
+ private arrayBufferToBase64(buffer: ArrayBuffer): string {
+ const bytes = new Uint8Array(buffer);
+ let binary = "";
+ for (let i = 0; i < bytes.byteLength; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return btoa(binary);
+ }
+}
+
+/**
+ * WebCrypto-based SHA-256 hash algorithm
+ * Uses the Web Crypto API which is available in browsers and modern Node.js
+ */
+export class WebCryptoSha256 implements HashAlgorithm {
+ getHash(xml: string): string;
+ getHash(xml: string, callback: ErrorFirstCallback): void;
+ getHash(xml: string, callback?: ErrorFirstCallback): string | void {
+ if (!callback) {
+ throw new Error(
+ "WebCrypto hash algorithms are async and require a callback. Use getHash(xml, callback).",
+ );
+ }
+
+ const encoder = new TextEncoder();
+ const data = encoder.encode(xml);
+ crypto.subtle
+ .digest("SHA-256", data)
+ .then((hashBuffer) => {
+ const hash = this.arrayBufferToBase64(hashBuffer);
+ callback(null, hash);
+ })
+ .catch((err) => {
+ callback(err);
+ });
+ }
+
+ getAlgorithmName = (): string => {
+ return "http://www.w3.org/2001/04/xmlenc#sha256";
+ };
+
+ private arrayBufferToBase64(buffer: ArrayBuffer): string {
+ const bytes = new Uint8Array(buffer);
+ let binary = "";
+ for (let i = 0; i < bytes.byteLength; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return btoa(binary);
+ }
+}
+
+/**
+ * WebCrypto-based SHA-512 hash algorithm
+ * Uses the Web Crypto API which is available in browsers and modern Node.js
+ */
+export class WebCryptoSha512 implements HashAlgorithm {
+ getHash(xml: string): string;
+ getHash(xml: string, callback: ErrorFirstCallback): void;
+ getHash(xml: string, callback?: ErrorFirstCallback): string | void {
+ if (!callback) {
+ throw new Error(
+ "WebCrypto hash algorithms are async and require a callback. Use getHash(xml, callback).",
+ );
+ }
+
+ const encoder = new TextEncoder();
+ const data = encoder.encode(xml);
+ crypto.subtle
+ .digest("SHA-512", data)
+ .then((hashBuffer) => {
+ const hash = this.arrayBufferToBase64(hashBuffer);
+ callback(null, hash);
+ })
+ .catch((err) => {
+ callback(err);
+ });
+ }
+
+ getAlgorithmName = (): string => {
+ return "http://www.w3.org/2001/04/xmlenc#sha512";
+ };
+
+ private arrayBufferToBase64(buffer: ArrayBuffer): string {
+ const bytes = new Uint8Array(buffer);
+ let binary = "";
+ for (let i = 0; i < bytes.byteLength; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return btoa(binary);
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index 3c82b7a8..897450dd 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,3 +6,13 @@ export {
export { SignedXml } from "./signed-xml";
export * from "./types";
export * from "./utils";
+
+// WebCrypto implementations - no Node.js dependencies
+export { WebCryptoSha1, WebCryptoSha256, WebCryptoSha512 } from "./hash-algorithms-webcrypto";
+export {
+ WebCryptoRsaSha1,
+ WebCryptoRsaSha256,
+ WebCryptoRsaSha512,
+ WebCryptoHmacSha1,
+} from "./signature-algorithms-webcrypto";
+export * from "./webcrypto-utils";
diff --git a/src/signature-algorithms-webcrypto.ts b/src/signature-algorithms-webcrypto.ts
new file mode 100644
index 00000000..d0d427fd
--- /dev/null
+++ b/src/signature-algorithms-webcrypto.ts
@@ -0,0 +1,475 @@
+import {
+ type BinaryLike,
+ type ErrorFirstCallback,
+ type KeyLike,
+ type SignatureAlgorithm,
+} from "./types";
+import {
+ importRsaPrivateKey,
+ importRsaPublicKey,
+ importHmacKey,
+ arrayBufferToBase64,
+ base64ToArrayBuffer,
+} from "./webcrypto-utils";
+import * as nodeCrypto from "crypto";
+
+/**
+ * Check if a value is a CryptoKey (not a KeyObject)
+ * Guards against ReferenceError in environments without Web Crypto API
+ */
+function isCryptoKey(key: unknown): key is CryptoKey {
+ // CryptoKey has specific properties that KeyObject doesn't have
+ return (
+ (typeof CryptoKey !== "undefined" && key instanceof CryptoKey) ||
+ (typeof key === "object" &&
+ key !== null &&
+ "type" in key &&
+ "algorithm" in key &&
+ "extractable" in key &&
+ "usages" in key &&
+ !("export" in key)) // KeyObject has export, CryptoKey doesn't
+ );
+}
+
+/**
+ * Check if a value is a Node.js Buffer without directly referencing the Buffer global.
+ * This is browser-safe: it never calls Buffer.isBuffer() or accesses the Buffer global,
+ * preventing ReferenceError in environments where Buffer is not defined.
+ * In browsers, this will always return false; in Node.js, it correctly identifies Buffers.
+ */
+function isBuffer(value: unknown): value is Uint8Array {
+ // Safe: checks constructor name without accessing Buffer global
+ return value instanceof Uint8Array && value.constructor.name === "Buffer";
+}
+
+/**
+ * Check if a Uint8Array/Buffer contains valid UTF-8 text (like PEM format).
+ * This helps us distinguish between text-based keys (PEM) and binary keys (DER, raw bytes).
+ */
+function isPemText(data: Uint8Array): boolean {
+ try {
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(data);
+ // Check if it looks like PEM format
+ return text.includes("-----BEGIN") && text.includes("-----END");
+ } catch {
+ // Not valid UTF-8 text
+ return false;
+ }
+}
+
+/**
+ * Normalize various key input types to a format suitable for Web Crypto API.
+ * Returns either a PEM string (for RSA keys) or ArrayBuffer (for raw binary keys like HMAC).
+ * Preserves binary data without UTF-8 mangling.
+ */
+function normalizeKey(key: unknown): string | ArrayBuffer {
+ if (typeof key === "string") {
+ return key;
+ }
+
+ // Handle Uint8Array or Buffer
+ if (key instanceof Uint8Array || isBuffer(key)) {
+ const uint8Array = key as Uint8Array;
+
+ // Check if this contains PEM text (common case: Buffer wrapping PEM string)
+ if (isPemText(uint8Array)) {
+ // Decode as UTF-8 text
+ return new TextDecoder("utf-8").decode(uint8Array);
+ }
+
+ // Otherwise, preserve as binary (for DER keys, raw HMAC keys, etc.)
+ const buffer = new ArrayBuffer(uint8Array.byteLength);
+ const view = new Uint8Array(buffer);
+ view.set(uint8Array);
+ return buffer;
+ }
+
+ // Handle ArrayBuffer - return as-is (assume binary)
+ if (key instanceof ArrayBuffer) {
+ return key;
+ }
+
+ // Handle Node.js KeyObject
+ if (
+ typeof key === "object" &&
+ key !== null &&
+ "type" in key &&
+ "export" in key &&
+ typeof (key as { export: unknown }).export === "function" &&
+ !("algorithm" in key && "extractable" in key && "usages" in key) // Not a CryptoKey
+ ) {
+ const keyObject = key as nodeCrypto.KeyObject;
+ if (keyObject.type === "private") {
+ return keyObject.export({ type: "pkcs8", format: "pem" }) as string;
+ } else if (keyObject.type === "public") {
+ return keyObject.export({ type: "spki", format: "pem" }) as string;
+ } else if (keyObject.type === "secret") {
+ // For secret keys (HMAC), export as buffer and preserve binary data
+ const secretBuffer = keyObject.export();
+ // Convert Node.js Buffer to ArrayBuffer properly
+ // Note: Buffer.buffer may be a pooled ArrayBuffer, so we need to copy the data
+ const arrayBuffer = new ArrayBuffer(secretBuffer.byteLength);
+ const view = new Uint8Array(arrayBuffer);
+ // Create a proper Uint8Array view of the Buffer to ensure compatibility
+ const bytes = new Uint8Array(
+ secretBuffer.buffer,
+ secretBuffer.byteOffset,
+ secretBuffer.byteLength,
+ );
+ view.set(bytes);
+ return arrayBuffer;
+ }
+ }
+ throw new Error(
+ "Unsupported key type. Expected string (PEM), Buffer, Uint8Array, ArrayBuffer, KeyObject, or CryptoKey",
+ );
+}
+
+/**
+ * Convert various input types to ArrayBuffer for Web Crypto API.
+ *
+ * BROWSER SAFETY: This function never references the global Buffer object directly.
+ * It uses the browser-safe isBuffer() helper which only checks constructor.name,
+ * preventing ReferenceError in environments where Buffer is not defined.
+ */
+function toArrayBuffer(data: unknown): ArrayBuffer {
+ if (typeof data === "string") {
+ return new TextEncoder().encode(data).buffer;
+ }
+ if (data instanceof ArrayBuffer) {
+ return data;
+ }
+ // Browser-safe: isBuffer() never calls Buffer.isBuffer() or accesses Buffer global
+ if (data instanceof Uint8Array || isBuffer(data)) {
+ // Create a new ArrayBuffer from the Uint8Array/Buffer
+ const buffer = new ArrayBuffer((data as Uint8Array).byteLength);
+ const view = new Uint8Array(buffer);
+ view.set(data as Uint8Array);
+ return buffer;
+ }
+ throw new Error("Unsupported data type");
+}
+
+/**
+ * WebCrypto-based RSA-SHA1 signature algorithm
+ * Uses the Web Crypto API which is available in browsers and modern Node.js
+ */
+export class WebCryptoRsaSha1 implements SignatureAlgorithm {
+ getSignature(signedInfo: BinaryLike, privateKey: KeyLike): string;
+ getSignature(
+ signedInfo: BinaryLike,
+ privateKey: KeyLike,
+ callback: ErrorFirstCallback,
+ ): void;
+ getSignature(
+ signedInfo: BinaryLike,
+ privateKey: KeyLike,
+ callback?: ErrorFirstCallback,
+ ): string | void {
+ if (!callback) {
+ throw new Error("WebCrypto algorithms require a callback");
+ }
+
+ (async () => {
+ // If already a CryptoKey, use it directly
+ let key: CryptoKey;
+ if (isCryptoKey(privateKey)) {
+ key = privateKey;
+ } else {
+ // Normalize key (handles Buffer, KeyObject, etc.)
+ const normalizedKey = normalizeKey(privateKey);
+ key = await importRsaPrivateKey(normalizedKey, "SHA-1");
+ }
+
+ const data = toArrayBuffer(signedInfo);
+ const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, data);
+ return arrayBufferToBase64(signature);
+ })()
+ .then((result) => callback(null, result))
+ .catch((err) => callback(err instanceof Error ? err : new Error("Unknown error")));
+ }
+
+ verifySignature(material: string, key: KeyLike, signatureValue: string): boolean;
+ verifySignature(
+ material: string,
+ key: KeyLike,
+ signatureValue: string,
+ callback: ErrorFirstCallback,
+ ): void;
+ verifySignature(
+ material: string,
+ key: KeyLike,
+ signatureValue: string,
+ callback?: ErrorFirstCallback,
+ ): boolean | void {
+ if (!callback) {
+ throw new Error("WebCrypto algorithms require a callback");
+ }
+
+ (async () => {
+ // If already a CryptoKey, use it directly
+ let publicKey: CryptoKey;
+ if (isCryptoKey(key)) {
+ publicKey = key;
+ } else {
+ // Normalize key (handles Buffer, KeyObject, etc.)
+ const normalizedKey = normalizeKey(key);
+ publicKey = await importRsaPublicKey(normalizedKey, "SHA-1");
+ }
+
+ const data = new TextEncoder().encode(material);
+ const signature = base64ToArrayBuffer(signatureValue);
+ return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", publicKey, signature, data);
+ })()
+ .then((result) => callback(null, result))
+ .catch((err) => callback(err instanceof Error ? err : new Error("Unknown error")));
+ }
+
+ getAlgorithmName(): string {
+ return "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
+ }
+}
+
+/**
+ * WebCrypto-based RSA-SHA256 signature algorithm
+ * Uses the Web Crypto API which is available in browsers and modern Node.js
+ */
+export class WebCryptoRsaSha256 implements SignatureAlgorithm {
+ getSignature(signedInfo: BinaryLike, privateKey: KeyLike): string;
+ getSignature(
+ signedInfo: BinaryLike,
+ privateKey: KeyLike,
+ callback: ErrorFirstCallback,
+ ): void;
+ getSignature(
+ signedInfo: BinaryLike,
+ privateKey: KeyLike,
+ callback?: ErrorFirstCallback,
+ ): string | void {
+ if (!callback) {
+ throw new Error("WebCrypto algorithms require a callback");
+ }
+
+ (async () => {
+ // If already a CryptoKey, use it directly
+ let key: CryptoKey;
+ if (isCryptoKey(privateKey)) {
+ key = privateKey;
+ } else {
+ // Normalize key (handles Buffer, KeyObject, etc.)
+ const normalizedKey = normalizeKey(privateKey);
+ key = await importRsaPrivateKey(normalizedKey, "SHA-256");
+ }
+
+ const data = toArrayBuffer(signedInfo);
+ const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, data);
+ return arrayBufferToBase64(signature);
+ })()
+ .then((result) => callback(null, result))
+ .catch((err) => callback(err instanceof Error ? err : new Error("Unknown error")));
+ }
+
+ verifySignature(material: string, key: KeyLike, signatureValue: string): boolean;
+ verifySignature(
+ material: string,
+ key: KeyLike,
+ signatureValue: string,
+ callback: ErrorFirstCallback,
+ ): void;
+ verifySignature(
+ material: string,
+ key: KeyLike,
+ signatureValue: string,
+ callback?: ErrorFirstCallback,
+ ): boolean | void {
+ if (!callback) {
+ throw new Error("WebCrypto algorithms require a callback");
+ }
+
+ (async () => {
+ // If already a CryptoKey, use it directly
+ let publicKey: CryptoKey;
+ if (isCryptoKey(key)) {
+ publicKey = key;
+ } else {
+ // Normalize key (handles Buffer, KeyObject, etc.)
+ const normalizedKey = normalizeKey(key);
+ publicKey = await importRsaPublicKey(normalizedKey, "SHA-256");
+ }
+
+ const data = new TextEncoder().encode(material);
+ const signature = base64ToArrayBuffer(signatureValue);
+ return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", publicKey, signature, data);
+ })()
+ .then((result) => callback(null, result))
+ .catch((err) => callback(err instanceof Error ? err : new Error("Unknown error")));
+ }
+
+ getAlgorithmName(): string {
+ return "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
+ }
+}
+
+/**
+ * WebCrypto-based RSA-SHA512 signature algorithm
+ * Uses the Web Crypto API which is available in browsers and modern Node.js
+ */
+export class WebCryptoRsaSha512 implements SignatureAlgorithm {
+ getSignature(signedInfo: BinaryLike, privateKey: KeyLike): string;
+ getSignature(
+ signedInfo: BinaryLike,
+ privateKey: KeyLike,
+ callback: ErrorFirstCallback,
+ ): void;
+ getSignature(
+ signedInfo: BinaryLike,
+ privateKey: KeyLike,
+ callback?: ErrorFirstCallback,
+ ): string | void {
+ if (!callback) {
+ throw new Error("WebCrypto algorithms require a callback");
+ }
+
+ (async () => {
+ // If already a CryptoKey, use it directly
+ let key: CryptoKey;
+ if (isCryptoKey(privateKey)) {
+ key = privateKey;
+ } else {
+ // Normalize key (handles Buffer, KeyObject, etc.)
+ const normalizedKey = normalizeKey(privateKey);
+ key = await importRsaPrivateKey(normalizedKey, "SHA-512");
+ }
+
+ const data = toArrayBuffer(signedInfo);
+ const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, data);
+ return arrayBufferToBase64(signature);
+ })()
+ .then((result) => callback(null, result))
+ .catch((err) => callback(err instanceof Error ? err : new Error("Unknown error")));
+ }
+
+ verifySignature(material: string, key: KeyLike, signatureValue: string): boolean;
+ verifySignature(
+ material: string,
+ key: KeyLike,
+ signatureValue: string,
+ callback: ErrorFirstCallback,
+ ): void;
+ verifySignature(
+ material: string,
+ key: KeyLike,
+ signatureValue: string,
+ callback?: ErrorFirstCallback,
+ ): boolean | void {
+ if (!callback) {
+ throw new Error("WebCrypto algorithms require a callback");
+ }
+
+ (async () => {
+ // If already a CryptoKey, use it directly
+ let publicKey: CryptoKey;
+ if (isCryptoKey(key)) {
+ publicKey = key;
+ } else {
+ // Normalize key (handles Buffer, KeyObject, etc.)
+ const normalizedKey = normalizeKey(key);
+ publicKey = await importRsaPublicKey(normalizedKey, "SHA-512");
+ }
+
+ const data = toArrayBuffer(material);
+ const signature = base64ToArrayBuffer(signatureValue);
+ return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", publicKey, signature, data);
+ })()
+ .then((result) => callback(null, result))
+ .catch((err) => callback(err instanceof Error ? err : new Error("Unknown error")));
+ }
+
+ getAlgorithmName(): string {
+ return "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512";
+ }
+}
+
+/**
+ * WebCrypto-based HMAC-SHA1 signature algorithm
+ * Uses the Web Crypto API which is available in browsers and modern Node.js
+ */
+export class WebCryptoHmacSha1 implements SignatureAlgorithm {
+ getSignature(signedInfo: BinaryLike, privateKey: KeyLike): string;
+ getSignature(
+ signedInfo: BinaryLike,
+ privateKey: KeyLike,
+ callback: ErrorFirstCallback,
+ ): void;
+ getSignature(
+ signedInfo: BinaryLike,
+ privateKey: KeyLike,
+ callback?: ErrorFirstCallback,
+ ): string | void {
+ if (!callback) {
+ throw new Error("WebCrypto algorithms require a callback");
+ }
+
+ (async () => {
+ // If already a CryptoKey, use it directly
+ let key: CryptoKey;
+ if (isCryptoKey(privateKey)) {
+ key = privateKey;
+ } else {
+ // Normalize key (handles Buffer, KeyObject, etc.)
+ // HMAC keys can be binary (ArrayBuffer) or string
+ const normalizedKey = normalizeKey(privateKey);
+ key = await importHmacKey(normalizedKey, "SHA-1");
+ }
+
+ const data = toArrayBuffer(signedInfo);
+ const signature = await crypto.subtle.sign("HMAC", key, data);
+ return arrayBufferToBase64(signature);
+ })()
+ .then((result) => callback(null, result))
+ .catch((err) => callback(err instanceof Error ? err : new Error("Unknown error")));
+ }
+
+ verifySignature(material: string, key: KeyLike, signatureValue: string): boolean;
+ verifySignature(
+ material: string,
+ key: KeyLike,
+ signatureValue: string,
+ callback: ErrorFirstCallback,
+ ): void;
+ verifySignature(
+ material: string,
+ key: KeyLike,
+ signatureValue: string,
+ callback?: ErrorFirstCallback,
+ ): boolean | void {
+ if (!callback) {
+ throw new Error("WebCrypto algorithms require a callback");
+ }
+
+ (async () => {
+ // If already a CryptoKey, use it directly
+ let hmacKey: CryptoKey;
+ if (isCryptoKey(key)) {
+ hmacKey = key;
+ } else {
+ // Normalize key (handles Buffer, KeyObject, etc.)
+ // HMAC keys can be binary (ArrayBuffer) or string
+ const normalizedKey = normalizeKey(key);
+ hmacKey = await importHmacKey(normalizedKey, "SHA-1");
+ }
+
+ const data = new TextEncoder().encode(material);
+ const signature = base64ToArrayBuffer(signatureValue);
+
+ // Use crypto.subtle.verify for constant-time comparison (prevents timing attacks)
+ return await crypto.subtle.verify("HMAC", hmacKey, signature, data);
+ })()
+ .then((result) => callback(null, result))
+ .catch((err) => callback(err instanceof Error ? err : new Error("Unknown error")));
+ }
+
+ getAlgorithmName(): string {
+ return "http://www.w3.org/2000/09/xmldsig#hmac-sha1";
+ }
+}
diff --git a/src/signature-algorithms.ts b/src/signature-algorithms.ts
index ab1e919f..f532c858 100644
--- a/src/signature-algorithms.ts
+++ b/src/signature-algorithms.ts
@@ -1,22 +1,27 @@
import * as crypto from "crypto";
-import { type SignatureAlgorithm, createOptionalCallbackFunction } from "./types";
+import {
+ type SignatureAlgorithm,
+ type BinaryLike,
+ type KeyLike,
+ createOptionalCallbackFunction,
+} from "./types";
export class RsaSha1 implements SignatureAlgorithm {
getSignature = createOptionalCallbackFunction(
- (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string => {
+ (signedInfo: BinaryLike, privateKey: KeyLike): string => {
const signer = crypto.createSign("RSA-SHA1");
- signer.update(signedInfo);
- const res = signer.sign(privateKey, "base64");
+ signer.update(signedInfo as crypto.BinaryLike);
+ const res = signer.sign(privateKey as crypto.KeyLike, "base64");
return res;
},
);
verifySignature = createOptionalCallbackFunction(
- (material: string, key: crypto.KeyLike, signatureValue: string): boolean => {
+ (material: string, key: KeyLike, signatureValue: string): boolean => {
const verifier = crypto.createVerify("RSA-SHA1");
verifier.update(material);
- const res = verifier.verify(key, signatureValue, "base64");
+ const res = verifier.verify(key as crypto.KeyLike, signatureValue, "base64");
return res;
},
@@ -29,20 +34,20 @@ export class RsaSha1 implements SignatureAlgorithm {
export class RsaSha256 implements SignatureAlgorithm {
getSignature = createOptionalCallbackFunction(
- (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string => {
+ (signedInfo: BinaryLike, privateKey: KeyLike): string => {
const signer = crypto.createSign("RSA-SHA256");
- signer.update(signedInfo);
- const res = signer.sign(privateKey, "base64");
+ signer.update(signedInfo as crypto.BinaryLike);
+ const res = signer.sign(privateKey as crypto.KeyLike, "base64");
return res;
},
);
verifySignature = createOptionalCallbackFunction(
- (material: string, key: crypto.KeyLike, signatureValue: string): boolean => {
+ (material: string, key: KeyLike, signatureValue: string): boolean => {
const verifier = crypto.createVerify("RSA-SHA256");
verifier.update(material);
- const res = verifier.verify(key, signatureValue, "base64");
+ const res = verifier.verify(key as crypto.KeyLike, signatureValue, "base64");
return res;
},
@@ -55,20 +60,20 @@ export class RsaSha256 implements SignatureAlgorithm {
export class RsaSha512 implements SignatureAlgorithm {
getSignature = createOptionalCallbackFunction(
- (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string => {
+ (signedInfo: BinaryLike, privateKey: KeyLike): string => {
const signer = crypto.createSign("RSA-SHA512");
- signer.update(signedInfo);
- const res = signer.sign(privateKey, "base64");
+ signer.update(signedInfo as crypto.BinaryLike);
+ const res = signer.sign(privateKey as crypto.KeyLike, "base64");
return res;
},
);
verifySignature = createOptionalCallbackFunction(
- (material: string, key: crypto.KeyLike, signatureValue: string): boolean => {
+ (material: string, key: KeyLike, signatureValue: string): boolean => {
const verifier = crypto.createVerify("RSA-SHA512");
verifier.update(material);
- const res = verifier.verify(key, signatureValue, "base64");
+ const res = verifier.verify(key as crypto.KeyLike, signatureValue, "base64");
return res;
},
@@ -81,9 +86,9 @@ export class RsaSha512 implements SignatureAlgorithm {
export class HmacSha1 implements SignatureAlgorithm {
getSignature = createOptionalCallbackFunction(
- (signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string => {
- const signer = crypto.createHmac("SHA1", privateKey);
- signer.update(signedInfo);
+ (signedInfo: BinaryLike, privateKey: KeyLike): string => {
+ const signer = crypto.createHmac("SHA1", privateKey as crypto.BinaryLike | crypto.KeyObject);
+ signer.update(signedInfo as crypto.BinaryLike);
const res = signer.digest("base64");
return res;
@@ -91,8 +96,8 @@ export class HmacSha1 implements SignatureAlgorithm {
);
verifySignature = createOptionalCallbackFunction(
- (material: string, key: crypto.KeyLike, signatureValue: string): boolean => {
- const verifier = crypto.createHmac("SHA1", key);
+ (material: string, key: KeyLike, signatureValue: string): boolean => {
+ const verifier = crypto.createHmac("SHA1", key as crypto.BinaryLike | crypto.KeyObject);
verifier.update(material);
const res = verifier.digest("base64");
diff --git a/src/signed-xml.ts b/src/signed-xml.ts
index 05dae417..6014f363 100644
--- a/src/signed-xml.ts
+++ b/src/signed-xml.ts
@@ -8,6 +8,7 @@ import type {
GetKeyInfoContentArgs,
HashAlgorithm,
HashAlgorithmType,
+ KeyLike,
Reference,
SignatureAlgorithm,
SignatureAlgorithmType,
@@ -16,7 +17,6 @@ import type {
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";
import * as c14n from "./c14n-canonicalization";
@@ -32,8 +32,8 @@ export class SignedXml {
/**
* A {@link Buffer} or pem encoded {@link String} containing your private key
*/
- privateKey?: crypto.KeyLike;
- publicCert?: crypto.KeyLike;
+ privateKey?: KeyLike;
+ publicCert?: KeyLike;
/**
* One of the supported signature algorithms.
* @see {@link SignatureAlgorithmType}
@@ -312,6 +312,36 @@ export class SignedXml {
this.loadReference(reference);
}
+ // Use callback-based validation if callback provided
+ if (callback) {
+ let completed = 0;
+ let hasError = false;
+
+ this.references.forEach((ref) => {
+ this.validateReference(ref, doc, (err, isValid) => {
+ if (hasError) return; // Already failed
+
+ if (err || !isValid) {
+ hasError = true;
+ this.signedReferences = [];
+ this.references.forEach((ref) => {
+ ref.signedReference = undefined;
+ });
+ callback(err || new Error("Could not validate all references"), false);
+ return;
+ }
+
+ completed++;
+ if (completed === this.references.length) {
+ // Continue with signature verification
+ this.verifySignatureValue(unverifiedSignedInfoCanon, callback);
+ }
+ });
+ });
+ return;
+ }
+
+ // Synchronous validation (for Node.js crypto algorithms)
/* eslint-disable-next-line deprecation/deprecation */
if (!this.getReferences().every((ref) => this.validateReference(ref, doc))) {
/* Trustworthiness can only be determined if SignedInfo's (which holds References' DigestValue(s)
@@ -328,11 +358,6 @@ export class SignedXml {
});
// TODO: add this breaking change here later on for even more security: `this.references = [];`
- if (callback) {
- callback(new Error("Could not validate all references"), false);
- return;
- }
-
// We return false because some references validated, but not all
// We should actually be throwing an error here, but that would be a breaking change
// See https://www.w3.org/TR/xmldsig-core/#sec-CoreValidation
@@ -340,7 +365,6 @@ export class SignedXml {
}
// (Stage B authentication step, show that the `signedInfoCanon` is signed)
-
// First find the key & signature algorithm, these should match
// Stage B: Take the signature algorithm and key and verify the `SignatureValue` against the canonicalized `SignedInfo`
const signer = this.findSignatureAlgorithm(this.signatureAlgorithm);
@@ -352,11 +376,7 @@ export class SignedXml {
// Check the signature verification to know whether to reset signature value or not.
const sigRes = signer.verifySignature(unverifiedSignedInfoCanon, key, this.signatureValue);
if (sigRes === true) {
- if (callback) {
- callback(null, true);
- } else {
- return true;
- }
+ return true;
} else {
// Ideally, we would start by verifying the `signedInfoCanon` first,
// but that may cause some breaking changes, so we'll handle that in v7.x.
@@ -367,17 +387,43 @@ export class SignedXml {
});
// TODO: add this breaking change here later on for even more security: `this.references = [];`
- if (callback) {
+ throw new Error(`invalid signature: the signature value ${this.signatureValue} is incorrect`);
+ }
+ }
+
+ private verifySignatureValue(
+ unverifiedSignedInfoCanon: string,
+ callback: (error: Error | null, isValid?: boolean) => void,
+ ): void {
+ const signer = this.findSignatureAlgorithm(this.signatureAlgorithm);
+ const key = this.getCertFromKeyInfo(this.keyInfo) || this.publicCert || this.privateKey;
+ if (key == null) {
+ throw new Error("KeyInfo or publicCert or privateKey is required to validate signature");
+ }
+
+ // Use callback for signature verification (required for WebCrypto)
+ signer.verifySignature(unverifiedSignedInfoCanon, key, this.signatureValue, (err, isValid) => {
+ if (err) {
+ this.signedReferences = [];
+ this.references.forEach((ref) => {
+ ref.signedReference = undefined;
+ });
+ callback(err);
+ return;
+ }
+
+ if (isValid) {
+ callback(null, true);
+ } else {
+ this.signedReferences = [];
+ this.references.forEach((ref) => {
+ ref.signedReference = undefined;
+ });
callback(
new Error(`invalid signature: the signature value ${this.signatureValue} is incorrect`),
);
- return; // return early
- } else {
- throw new Error(
- `invalid signature: the signature value ${this.signatureValue} is incorrect`,
- );
}
- }
+ });
}
private getCanonSignedInfoXml(doc: Document) {
@@ -517,7 +563,17 @@ export class SignedXml {
throw new Error("No references passed validation");
}
- private validateReference(ref: Reference, doc: Document) {
+ private validateReference(ref: Reference, doc: Document): boolean;
+ private validateReference(
+ ref: Reference,
+ doc: Document,
+ callback: (error: Error | null, isValid?: boolean) => void,
+ ): void;
+ private validateReference(
+ ref: Reference,
+ doc: Document,
+ callback?: (error: Error | null, isValid?: boolean) => void,
+ ): boolean | void {
const uri = ref.uri?.[0] === "#" ? ref.uri.substring(1) : ref.uri;
let elem: xpath.SelectSingleReturnType = null;
@@ -525,7 +581,12 @@ export class SignedXml {
elem = xpath.select1("//*", doc);
} else if (uri?.indexOf("'") !== -1) {
// xpath injection
- throw new Error("Cannot validate a uri with quotes inside it");
+ const err = new Error("Cannot validate a uri with quotes inside it");
+ if (callback) {
+ callback(err, false);
+ return;
+ }
+ throw err;
} else {
let num_elements_for_id = 0;
for (const attr of this.idAttributes) {
@@ -535,11 +596,16 @@ export class SignedXml {
num_elements_for_id += tmp_elem.length;
if (num_elements_for_id > 1) {
- throw new Error(
+ const err = new Error(
"Cannot validate a document which contains multiple elements with the " +
"same value for the ID / Id / Id attributes, in order to prevent " +
"signature wrapping attack.",
);
+ if (callback) {
+ callback(err, false);
+ return;
+ }
+ throw err;
}
elem = tmp_elem[0];
@@ -562,11 +628,42 @@ export class SignedXml {
`invalid signature: the signature references an element with uri ${ref.uri} but could not find such element in the xml`,
);
ref.validationError = validationError;
+ if (callback) {
+ callback(validationError, false);
+ return;
+ }
return false;
}
const canonXml = this.getCanonReferenceXml(doc, ref, elem);
const hash = this.findHashAlgorithm(ref.digestAlgorithm);
+
+ // If callback provided, use async hash computation (for WebCrypto)
+ if (callback) {
+ hash.getHash(canonXml, (err, computedDigest) => {
+ if (err) {
+ ref.validationError = err;
+ callback(err, false);
+ return;
+ }
+
+ if (!utils.validateDigestValue(computedDigest!, ref.digestValue)) {
+ const validationError = new Error(
+ `invalid signature: for uri ${ref.uri} calculated digest is ${computedDigest} but the xml to validate supplies digest ${ref.digestValue}`,
+ );
+ ref.validationError = validationError;
+ callback(validationError, false);
+ return;
+ }
+
+ this.signedReferences.push(canonXml);
+ ref.signedReference = canonXml;
+ callback(null, true);
+ });
+ return;
+ }
+
+ // Synchronous path (for Node.js crypto)
const digest = hash.getHash(canonXml);
if (!utils.validateDigestValue(digest, ref.digestValue)) {
@@ -936,12 +1033,12 @@ export class SignedXml {
location.action
}, must be any of the following values: ${validActions.join(", ")}`,
);
- if (!callback) {
- throw err;
- } else {
+ if (callback) {
callback(err);
- return;
+ } else {
+ throw err;
}
+ return;
}
// automatic insertion of `:`
@@ -961,90 +1058,150 @@ export class SignedXml {
// add the xml namespace attribute
signatureAttrs.push(`${xmlNsAttr}="http://www.w3.org/2000/09/xmldsig#"`);
- let signatureXml = `<${currentPrefix}Signature ${signatureAttrs.join(" ")}>`;
+ if (callback) {
+ this.createSignedInfo(doc, prefix, (err, signedInfoXml) => {
+ if (err) {
+ callback(err);
+ return;
+ }
- signatureXml += this.createSignedInfo(doc, prefix);
- signatureXml += this.getKeyInfo(prefix);
- signatureXml += `${currentPrefix}Signature>`;
+ // Build the signature XML
+ let signatureXml = `<${currentPrefix}Signature ${signatureAttrs.join(" ")}>`;
+ signatureXml += signedInfoXml;
+ signatureXml += this.getKeyInfo(prefix);
+ signatureXml += `${currentPrefix}Signature>`;
+
+ this.originalXmlWithIds = doc.toString();
+
+ let existingPrefixesString = "";
+ Object.keys(existingPrefixes).forEach(function (key) {
+ existingPrefixesString += `xmlns:${key}="${existingPrefixes[key]}" `;
+ });
+
+ // 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);
+
+ // 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
+ const signatureDoc = nodeXml.documentElement.firstChild!;
+
+ const referenceNode = xpath.select1(location.reference!, doc);
+
+ if (!isDomNode.isNodeLike(referenceNode)) {
+ callback(
+ new Error(
+ `the following xpath cannot be used because it was not found: ${location.reference}`,
+ ),
+ );
+ return;
+ }
- this.originalXmlWithIds = doc.toString();
+ if (location.action === "append") {
+ referenceNode.appendChild(signatureDoc);
+ } else if (location.action === "prepend") {
+ referenceNode.insertBefore(signatureDoc, referenceNode.firstChild);
+ } else if (location.action === "before") {
+ if (referenceNode.parentNode == null) {
+ callback(
+ new Error(
+ "`location.reference` refers to the root node (by default), so we can't insert `before`",
+ ),
+ );
+ return;
+ }
+ referenceNode.parentNode.insertBefore(signatureDoc, referenceNode);
+ } else if (location.action === "after") {
+ if (referenceNode.parentNode == null) {
+ callback(
+ new Error(
+ "`location.reference` refers to the root node (by default), so we can't insert `after`",
+ ),
+ );
+ return;
+ }
+ referenceNode.parentNode.insertBefore(signatureDoc, referenceNode.nextSibling);
+ }
- let existingPrefixesString = "";
- Object.keys(existingPrefixes).forEach(function (key) {
- existingPrefixesString += `xmlns:${key}="${existingPrefixes[key]}" `;
- });
+ this.signatureNode = signatureDoc;
+ const signedInfoNodes = utils.findChildren(this.signatureNode, "SignedInfo");
+ if (signedInfoNodes.length === 0) {
+ callback(new Error("could not find SignedInfo element in the message"));
+ return;
+ }
+ const signedInfoNode = signedInfoNodes[0];
- // 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);
+ this.calculateSignatureValue(doc, (err, signature) => {
+ if (err) {
+ callback(err);
+ } else {
+ this.signatureValue = signature || "";
+ signatureDoc.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling);
+ this.signatureXml = signatureDoc.toString();
+ this.signedXml = doc.toString();
+ callback(null, this);
+ }
+ });
+ });
+ } else {
+ // Build the signature XML
+ let signatureXml = `<${currentPrefix}Signature ${signatureAttrs.join(" ")}>`;
+ signatureXml += this.createSignedInfo(doc, prefix);
+ signatureXml += this.getKeyInfo(prefix);
+ signatureXml += `${currentPrefix}Signature>`;
- // 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
- const signatureDoc = nodeXml.documentElement.firstChild!;
+ this.originalXmlWithIds = doc.toString();
- const referenceNode = xpath.select1(location.reference, doc);
+ let existingPrefixesString = "";
+ Object.keys(existingPrefixes).forEach(function (key) {
+ existingPrefixesString += `xmlns:${key}="${existingPrefixes[key]}" `;
+ });
- if (!isDomNode.isNodeLike(referenceNode)) {
- const err2 = new Error(
- `the following xpath cannot be used because it was not found: ${location.reference}`,
- );
- if (!callback) {
- throw err2;
- } else {
- callback(err2);
- return;
- }
- }
+ // 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);
+
+ // 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
+ const signatureDoc = nodeXml.documentElement.firstChild!;
+
+ const referenceNode = xpath.select1(location.reference, doc);
- if (location.action === "append") {
- referenceNode.appendChild(signatureDoc);
- } else if (location.action === "prepend") {
- referenceNode.insertBefore(signatureDoc, referenceNode.firstChild);
- } else if (location.action === "before") {
- if (referenceNode.parentNode == null) {
+ if (!isDomNode.isNodeLike(referenceNode)) {
throw new Error(
- "`location.reference` refers to the root node (by default), so we can't insert `before`",
+ `the following xpath cannot be used because it was not found: ${location.reference}`,
);
}
- referenceNode.parentNode.insertBefore(signatureDoc, referenceNode);
- } else if (location.action === "after") {
- if (referenceNode.parentNode == null) {
- throw new Error(
- "`location.reference` refers to the root node (by default), so we can't insert `after`",
- );
+
+ if (location.action === "append") {
+ referenceNode.appendChild(signatureDoc);
+ } else if (location.action === "prepend") {
+ referenceNode.insertBefore(signatureDoc, referenceNode.firstChild);
+ } else if (location.action === "before") {
+ if (referenceNode.parentNode == null) {
+ throw new Error(
+ "`location.reference` refers to the root node (by default), so we can't insert `before`",
+ );
+ }
+ referenceNode.parentNode.insertBefore(signatureDoc, referenceNode);
+ } else if (location.action === "after") {
+ if (referenceNode.parentNode == null) {
+ throw new Error(
+ "`location.reference` refers to the root node (by default), so we can't insert `after`",
+ );
+ }
+ referenceNode.parentNode.insertBefore(signatureDoc, referenceNode.nextSibling);
}
- referenceNode.parentNode.insertBefore(signatureDoc, referenceNode.nextSibling);
- }
- this.signatureNode = signatureDoc;
- const signedInfoNodes = utils.findChildren(this.signatureNode, "SignedInfo");
- if (signedInfoNodes.length === 0) {
- const err3 = new Error("could not find SignedInfo element in the message");
- if (!callback) {
- throw err3;
- } else {
- callback(err3);
- return;
+ this.signatureNode = signatureDoc;
+ const signedInfoNodes = utils.findChildren(this.signatureNode, "SignedInfo");
+ if (signedInfoNodes.length === 0) {
+ throw new Error("could not find SignedInfo element in the message");
}
- }
- const signedInfoNode = signedInfoNodes[0];
+ const signedInfoNode = signedInfoNodes[0];
- if (typeof callback === "function") {
- // Asynchronous flow
- this.calculateSignatureValue(doc, (err, signature) => {
- if (err) {
- callback(err);
- } else {
- this.signatureValue = signature || "";
- signatureDoc.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling);
- this.signatureXml = signatureDoc.toString();
- this.signedXml = doc.toString();
- callback(null, this);
- }
- });
- } else {
- // Synchronous flow
this.calculateSignatureValue(doc);
signatureDoc.insertBefore(this.createSignature(prefix), signedInfoNode.nextSibling);
this.signatureXml = signatureDoc.toString();
@@ -1074,20 +1231,37 @@ export class SignedXml {
* Generate the Reference nodes (as part of the signature process)
*
*/
- private createReferences(doc, prefix) {
- let res = "";
-
+ private createReferences(doc, prefix): string;
+ private createReferences(
+ doc,
+ prefix,
+ callback: (error: Error | null, xml?: string) => void,
+ ): void;
+ private createReferences(
+ doc,
+ prefix,
+ callback?: (error: Error | null, xml?: string) => void,
+ ): string | void {
prefix = prefix || "";
prefix = prefix ? `${prefix}:` : prefix;
/* eslint-disable-next-line deprecation/deprecation */
- for (const ref of this.getReferences()) {
+ const refs = this.getReferences();
+ let res = "";
+ const referenceXmls: string[] = [];
+
+ for (const ref of refs) {
const nodes = xpath.selectWithResolver(ref.xpath ?? "", doc, this.namespaceResolver);
if (!utils.isArrayHasLength(nodes)) {
- throw new Error(
+ const err = new Error(
`the following xpath cannot be signed because it was not found: ${ref.xpath}`,
);
+ if (callback) {
+ callback(err);
+ return;
+ }
+ throw err;
}
for (const node of nodes) {
@@ -1112,19 +1286,44 @@ export class SignedXml {
res += " />";
}
}
+ res += `${prefix}Transforms>`;
const canonXml = this.getCanonReferenceXml(doc, ref, node);
-
const digestAlgorithm = this.findHashAlgorithm(ref.digestAlgorithm);
- res +=
- `${prefix}Transforms>` +
- `<${prefix}DigestMethod Algorithm="${digestAlgorithm.getAlgorithmName()}" />` +
- `<${prefix}DigestValue>${digestAlgorithm.getHash(canonXml)}${prefix}DigestValue>` +
- `${prefix}Reference>`;
+
+ if (!callback) {
+ const digestValue = digestAlgorithm.getHash(canonXml);
+ res += `<${prefix}DigestMethod Algorithm="${digestAlgorithm.getAlgorithmName()}" />`;
+ res += `<${prefix}DigestValue>${digestValue}${prefix}DigestValue>`;
+ res += `${prefix}Reference>`;
+ } else {
+ // Capture the current reference XML prefix before the async callback
+ const refXmlPrefix = res;
+ res = ""; // Reset for next iteration
+
+ digestAlgorithm.getHash(canonXml, (err, digest) => {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ let refXml = refXmlPrefix;
+ refXml += `<${prefix}DigestMethod Algorithm="${digestAlgorithm.getAlgorithmName()}" />`;
+ refXml += `<${prefix}DigestValue>${digest}${prefix}DigestValue>`;
+ refXml += `${prefix}Reference>`;
+ referenceXmls.push(refXml);
+
+ if (referenceXmls.length === nodes.length * refs.length) {
+ callback(null, referenceXmls.join(""));
+ }
+ });
+ }
}
}
- return res;
+ if (!callback) {
+ return res;
+ }
}
getCanonXml(
@@ -1205,11 +1404,26 @@ export class SignedXml {
* Create the SignedInfo element
*
*/
- private createSignedInfo(doc, prefix) {
+ private createSignedInfo(doc, prefix): string;
+ private createSignedInfo(
+ doc,
+ prefix,
+ callback: (error: Error | null, xml?: string) => void,
+ ): void;
+ private createSignedInfo(
+ doc,
+ prefix,
+ callback?: (error: Error | null, xml?: string) => void,
+ ): string | void {
if (typeof this.canonicalizationAlgorithm !== "string") {
- throw new Error(
+ const err = new Error(
"Missing canonicalizationAlgorithm when trying to create signed info for XML",
);
+ if (callback) {
+ callback(err);
+ return;
+ }
+ throw err;
}
const transform = this.findCanonicalizationAlgorithm(this.canonicalizationAlgorithm);
const algo = this.findSignatureAlgorithm(this.signatureAlgorithm);
@@ -1231,9 +1445,21 @@ export class SignedXml {
}
res += `<${currentPrefix}SignatureMethod Algorithm="${algo.getAlgorithmName()}" />`;
- res += this.createReferences(doc, prefix);
- res += `${currentPrefix}SignedInfo>`;
- return res;
+ // Async mode
+ if (callback) {
+ this.createReferences(doc, prefix, (err, referencesXml) => {
+ if (err) {
+ callback(err);
+ return;
+ }
+ callback(null, res + referencesXml + `${currentPrefix}SignedInfo>`);
+ });
+ return;
+ }
+
+ // Sync mode
+ const referencesXml = this.createReferences(doc, prefix);
+ return res + referencesXml + `${currentPrefix}SignedInfo>`;
}
/**
diff --git a/src/types.ts b/src/types.ts
index f102c4c7..27732791 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -10,6 +10,19 @@ import * as crypto from "crypto";
export type ErrorFirstCallback = (err: Error | null, result?: T) => void;
+/**
+ * Binary data types that can be used for signing and verification.
+ * Compatible with both Node.js crypto and Web Crypto API.
+ */
+export type BinaryLike = crypto.BinaryLike | ArrayBuffer;
+
+/**
+ * Key types that can be used with xml-crypto.
+ * Includes Node.js crypto.KeyLike (for Node.js crypto) and Web Crypto API CryptoKey.
+ * Also supports Uint8Array for raw key material.
+ */
+export type KeyLike = crypto.KeyLike | CryptoKey | Uint8Array;
+
export type CanonicalizationAlgorithmType =
| "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
| "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
@@ -39,7 +52,7 @@ export type SignatureAlgorithmType =
* @param prefix an optional namespace alias to be used for the generated XML
*/
export interface GetKeyInfoContentArgs {
- publicCert?: crypto.KeyLike;
+ publicCert?: KeyLike;
prefix?: string | null;
}
@@ -49,8 +62,8 @@ export interface GetKeyInfoContentArgs {
export interface SignedXmlOptions {
idMode?: "wssecurity";
idAttribute?: string;
- privateKey?: crypto.KeyLike;
- publicCert?: crypto.KeyLike;
+ privateKey?: KeyLike;
+ publicCert?: KeyLike;
signatureAlgorithm?: SignatureAlgorithmType;
canonicalizationAlgorithm?: CanonicalizationAlgorithmType;
inclusiveNamespacesPrefixList?: string | string[];
@@ -152,6 +165,7 @@ export interface HashAlgorithm {
getAlgorithmName(): HashAlgorithmType;
getHash(xml: string): string;
+ getHash(xml: string, callback: ErrorFirstCallback): void;
}
/** Extend this to create a new SignatureAlgorithm */
@@ -159,23 +173,23 @@ export interface SignatureAlgorithm {
/**
* Sign the given string using the given key
*/
- getSignature(signedInfo: crypto.BinaryLike, privateKey: crypto.KeyLike): string;
+ getSignature(signedInfo: BinaryLike, privateKey: KeyLike): string;
getSignature(
- signedInfo: crypto.BinaryLike,
- privateKey: crypto.KeyLike,
- callback?: ErrorFirstCallback,
+ signedInfo: BinaryLike,
+ privateKey: 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: KeyLike, signatureValue: string): boolean;
verifySignature(
material: string,
- key: crypto.KeyLike,
+ key: KeyLike,
signatureValue: string,
- callback?: ErrorFirstCallback,
+ callback: ErrorFirstCallback,
): void;
getAlgorithmName(): SignatureAlgorithmType;
diff --git a/src/webcrypto-utils.ts b/src/webcrypto-utils.ts
new file mode 100644
index 00000000..5482e0d8
--- /dev/null
+++ b/src/webcrypto-utils.ts
@@ -0,0 +1,170 @@
+/**
+ * Utility functions for working with Web Crypto API
+ */
+
+/**
+ * Convert a PEM string to an ArrayBuffer
+ * @param pem PEM-encoded key (with or without headers)
+ * @returns ArrayBuffer containing the binary key data
+ */
+export function pemToArrayBuffer(pem: string): ArrayBuffer {
+ // Remove PEM headers and whitespace
+ const pemContent = pem
+ .replace(/-----BEGIN [A-Z ]+-----/, "")
+ .replace(/-----END [A-Z ]+-----/, "")
+ .replace(/\s/g, "");
+
+ // Decode base64 to binary string
+ const binaryString = atob(pemContent);
+
+ // Convert binary string to ArrayBuffer
+ const bytes = new Uint8Array(binaryString.length);
+ for (let i = 0; i < binaryString.length; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+
+ return bytes.buffer;
+}
+
+/**
+ * Convert an ArrayBuffer to base64 string
+ * @param buffer ArrayBuffer to convert
+ * @returns Base64-encoded string
+ */
+export function arrayBufferToBase64(buffer: ArrayBuffer): string {
+ const bytes = new Uint8Array(buffer);
+ let binary = "";
+ for (let i = 0; i < bytes.byteLength; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return btoa(binary);
+}
+
+/**
+ * Convert a base64 string to ArrayBuffer
+ * @param base64 Base64-encoded string
+ * @returns ArrayBuffer
+ */
+export function base64ToArrayBuffer(base64: string): ArrayBuffer {
+ const binaryString = atob(base64);
+ const bytes = new Uint8Array(binaryString.length);
+ for (let i = 0; i < binaryString.length; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+ return bytes.buffer;
+}
+
+/**
+ * Import a PEM-encoded RSA private key for signing
+ * @param pem PEM-encoded private key
+ * @param hashAlgorithm Hash algorithm name (e.g., "SHA-1", "SHA-256", "SHA-512")
+ * @returns CryptoKey for signing
+ */
+export async function importRsaPrivateKey(
+ pem: string | ArrayBuffer,
+ hashAlgorithm: string,
+): Promise {
+ const keyData = typeof pem === "string" ? pemToArrayBuffer(pem) : pem;
+
+ return await crypto.subtle.importKey(
+ "pkcs8",
+ keyData,
+ {
+ name: "RSASSA-PKCS1-v1_5",
+ hash: { name: hashAlgorithm },
+ },
+ false,
+ ["sign"],
+ );
+}
+
+/**
+ * Import a PEM-encoded RSA public key for verification
+ * @param pem PEM-encoded public key or certificate
+ * @param hashAlgorithm Hash algorithm name (e.g., "SHA-1", "SHA-256", "SHA-512")
+ * @returns CryptoKey for verification
+ */
+export async function importRsaPublicKey(
+ pem: string | ArrayBuffer,
+ hashAlgorithm: string,
+): Promise {
+ let keyData: ArrayBuffer;
+
+ if (typeof pem === "string") {
+ // Check if this is a certificate
+ if (pem.includes("BEGIN CERTIFICATE")) {
+ // For certificates, we need to extract the public key
+ // This is a basic implementation - for production use, consider using a proper ASN.1 parser
+ // Web Crypto API doesn't support X.509 certificates directly
+ // For now, we'll try to parse it as SPKI and provide a helpful error
+ keyData = pemToArrayBuffer(pem);
+
+ // Try to extract the public key from the certificate
+ // This is a simplified approach and may not work for all certificates
+ try {
+ return await crypto.subtle.importKey(
+ "spki",
+ keyData,
+ {
+ name: "RSASSA-PKCS1-v1_5",
+ hash: { name: hashAlgorithm },
+ },
+ false,
+ ["verify"],
+ );
+ } catch (error) {
+ throw new Error(
+ "X.509 certificates are not directly supported by Web Crypto API. " +
+ "Please extract the public key from the certificate and provide it in SPKI format, " +
+ "or use Node.js crypto algorithms instead. " +
+ `Original error: ${error}`,
+ );
+ }
+ }
+ keyData = pemToArrayBuffer(pem);
+ } else {
+ keyData = pem;
+ }
+
+ // Try importing as SPKI (SubjectPublicKeyInfo) format
+ try {
+ return await crypto.subtle.importKey(
+ "spki",
+ keyData,
+ {
+ name: "RSASSA-PKCS1-v1_5",
+ hash: { name: hashAlgorithm },
+ },
+ false,
+ ["verify"],
+ );
+ } catch (error) {
+ throw new Error(
+ `Failed to import RSA public key. Please ensure the key is in SPKI format. ${error}`,
+ );
+ }
+}
+
+/**
+ * Import an HMAC key
+ * @param key Key material (string or ArrayBuffer)
+ * @param hashAlgorithm Hash algorithm name (e.g., "SHA-1", "SHA-256", "SHA-512")
+ * @returns CryptoKey for HMAC operations
+ */
+export async function importHmacKey(
+ key: string | ArrayBuffer,
+ hashAlgorithm: string,
+): Promise {
+ const keyData = typeof key === "string" ? new TextEncoder().encode(key) : key;
+
+ return await crypto.subtle.importKey(
+ "raw",
+ keyData,
+ {
+ name: "HMAC",
+ hash: { name: hashAlgorithm },
+ },
+ false,
+ ["sign", "verify"],
+ );
+}
diff --git a/test/signature-unit-tests.spec.ts b/test/signature-unit-tests.spec.ts
index baa382db..506718fa 100644
--- a/test/signature-unit-tests.spec.ts
+++ b/test/signature-unit-tests.spec.ts
@@ -6,7 +6,74 @@ import * as crypto from "crypto";
import { expect } from "chai";
import * as isDomNode from "@xmldom/is-dom-node";
+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/2001/04/xmldsig-more#rsa-sha512",
+];
+
describe("Signature unit tests", function () {
+ describe("sign and verify", function () {
+ signatureAlgorithms.forEach((signatureAlgorithm) => {
+ function signWith(signatureAlgorithm: string): string {
+ const xml = '';
+ const sig = new SignedXml();
+ 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#"],
+ });
+
+ sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
+ sig.signatureAlgorithm = signatureAlgorithm;
+ sig.computeSignature(xml);
+ return sig.getSignedXml();
+ }
+
+ 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#']",
+ doc,
+ );
+ isDomNode.assertIsNodeLike(node);
+ const sig = new SignedXml();
+ sig.publicCert = fs.readFileSync("./test/static/client_public.pem");
+ sig.loadSignature(node);
+ return sig;
+ }
+
+ it(`should verify signed xml with ${signatureAlgorithm}`, function () {
+ const xml = signWith(signatureAlgorithm);
+ const sig = loadSignature(xml);
+ const res = sig.checkSignature(xml);
+ expect(
+ res,
+ `expected all signatures with ${signatureAlgorithm} to be valid, but some reported invalid`,
+ ).to.be.true;
+ });
+
+ it(`should fail verification of signed xml with ${signatureAlgorithm} after manipulation`, function () {
+ const xml = signWith(signatureAlgorithm);
+ const doc = new xmldom.DOMParser().parseFromString(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 sig = loadSignature(manipulatedXml);
+ const res = sig.checkSignature(manipulatedXml);
+ expect(
+ res,
+ `expected all signatures with ${signatureAlgorithm} to be invalid, but some reported valid`,
+ ).to.be.false;
+ });
+ });
+ });
+
describe("verify adds ID", function () {
function nodeExists(doc, xpathArg) {
if (!doc && !xpathArg) {
diff --git a/test/webcrypto-tests.spec.ts b/test/webcrypto-tests.spec.ts
new file mode 100644
index 00000000..5e866533
--- /dev/null
+++ b/test/webcrypto-tests.spec.ts
@@ -0,0 +1,885 @@
+import { SignedXml } from "../src/signed-xml";
+import { WebCryptoSha1, WebCryptoSha256, WebCryptoSha512 } from "../src/hash-algorithms-webcrypto";
+import {
+ WebCryptoRsaSha1,
+ WebCryptoRsaSha256,
+ WebCryptoRsaSha512,
+ WebCryptoHmacSha1,
+} from "../src/signature-algorithms-webcrypto";
+import { importRsaPrivateKey, importRsaPublicKey } from "../src/webcrypto-utils";
+import { expect } from "chai";
+import { readFileSync } from "fs";
+import * as xmldom from "@xmldom/xmldom";
+
+describe("WebCrypto Hash Algorithms", function () {
+ it("WebCryptoSha1 should compute hash correctly", function (done) {
+ const hash = new WebCryptoSha1();
+ const xml = "data";
+ hash.getHash(xml, (err, digest) => {
+ if (err) return done(err);
+
+ // Verify it returns a base64 string
+ expect(digest).to.be.a("string");
+ if (digest) {
+ expect(digest.length).to.be.greaterThan(0);
+ expect(() => Buffer.from(digest, "base64")).to.not.throw();
+ }
+
+ // Verify algorithm name
+ expect(hash.getAlgorithmName()).to.equal("http://www.w3.org/2000/09/xmldsig#sha1");
+ done();
+ });
+ });
+
+ it("WebCryptoSha256 should compute hash correctly", function (done) {
+ const hash = new WebCryptoSha256();
+ const xml = "data";
+ hash.getHash(xml, (err, digest) => {
+ if (err) return done(err);
+
+ expect(digest).to.be.a("string");
+ if (digest) {
+ expect(digest.length).to.be.greaterThan(0);
+ expect(() => Buffer.from(digest, "base64")).to.not.throw();
+ }
+ expect(hash.getAlgorithmName()).to.equal("http://www.w3.org/2001/04/xmlenc#sha256");
+ done();
+ });
+ });
+
+ it("WebCryptoSha512 should compute hash correctly", function (done) {
+ const hash = new WebCryptoSha512();
+ const xml = "data";
+ hash.getHash(xml, (err, digest) => {
+ if (err) return done(err);
+
+ expect(digest).to.be.a("string");
+ if (digest) {
+ expect(digest.length).to.be.greaterThan(0);
+ expect(() => Buffer.from(digest, "base64")).to.not.throw();
+ }
+ expect(hash.getAlgorithmName()).to.equal("http://www.w3.org/2001/04/xmlenc#sha512");
+ done();
+ });
+ });
+
+ it("should produce consistent hashes for same input", function (done) {
+ const hash = new WebCryptoSha256();
+ const xml = "consistent data";
+ hash.getHash(xml, (err1, digest1) => {
+ if (err1) return done(err1);
+
+ hash.getHash(xml, (err2, digest2) => {
+ if (err2) return done(err2);
+
+ expect(digest1).to.equal(digest2);
+ done();
+ });
+ });
+ });
+
+ it("should produce different hashes for different inputs", function (done) {
+ const hash = new WebCryptoSha256();
+ const xml1 = "data1";
+ const xml2 = "data2";
+ hash.getHash(xml1, (err1, digest1) => {
+ if (err1) return done(err1);
+
+ hash.getHash(xml2, (err2, digest2) => {
+ if (err2) return done(err2);
+
+ expect(digest1).to.not.equal(digest2);
+ done();
+ });
+ });
+ });
+});
+
+describe("WebCrypto RSA Signature Algorithms", function () {
+ let privateKey: string;
+ let publicKey: string;
+
+ before(function () {
+ privateKey = readFileSync("./test/static/client.pem", "utf8");
+ publicKey = readFileSync("./test/static/client_public.pem", "utf8");
+ });
+
+ describe("WebCryptoRsaSha256", function () {
+ it("should sign and verify data correctly", function (done) {
+ const algo = new WebCryptoRsaSha256();
+ const data = "test data to sign";
+
+ algo.getSignature(data, privateKey, async (err, signature) => {
+ if (err) return done(err);
+
+ expect(signature).to.be.a("string");
+ if (signature) {
+ expect(signature.length).to.be.greaterThan(0);
+ }
+
+ // Extract public key to SPKI format for WebCrypto
+ const crypto = await import("crypto");
+ const publicKeyObj = crypto.createPublicKey(publicKey);
+ const spkiPem = publicKeyObj.export({ type: "spki", format: "pem" }) as string;
+
+ algo.verifySignature(data, spkiPem, signature!, (verifyErr, isValid) => {
+ if (verifyErr) return done(verifyErr);
+
+ expect(isValid).to.be.true;
+ done();
+ });
+ });
+ });
+
+ it("should fail verification with wrong data", function (done) {
+ const algo = new WebCryptoRsaSha256();
+ const data = "test data to sign";
+
+ algo.getSignature(data, privateKey, async (err, signature) => {
+ if (err) return done(err);
+
+ const crypto = await import("crypto");
+ const publicKeyObj = crypto.createPublicKey(publicKey);
+ const spkiPem = publicKeyObj.export({ type: "spki", format: "pem" }) as string;
+
+ algo.verifySignature("wrong data", spkiPem, signature!, (verifyErr, isValid) => {
+ if (verifyErr) return done(verifyErr);
+
+ expect(isValid).to.be.false;
+ done();
+ });
+ });
+ });
+
+ it("should have correct algorithm name", function () {
+ const algo = new WebCryptoRsaSha256();
+ expect(algo.getAlgorithmName()).to.equal("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
+ });
+ });
+
+ describe("WebCryptoRsaSha1", function () {
+ it("should sign and verify data correctly", function (done) {
+ const algo = new WebCryptoRsaSha1();
+ const data = "test data to sign";
+
+ algo.getSignature(data, privateKey, async (err, signature) => {
+ if (err) return done(err);
+
+ expect(signature).to.be.a("string");
+
+ const crypto = await import("crypto");
+ const publicKeyObj = crypto.createPublicKey(publicKey);
+ const spkiPem = publicKeyObj.export({ type: "spki", format: "pem" }) as string;
+
+ algo.verifySignature(data, spkiPem, signature!, (verifyErr, isValid) => {
+ if (verifyErr) return done(verifyErr);
+
+ expect(isValid).to.be.true;
+ done();
+ });
+ });
+ });
+
+ it("should have correct algorithm name", function () {
+ const algo = new WebCryptoRsaSha1();
+ expect(algo.getAlgorithmName()).to.equal("http://www.w3.org/2000/09/xmldsig#rsa-sha1");
+ });
+ });
+
+ describe("WebCryptoRsaSha512", function () {
+ it("should sign and verify data correctly", function (done) {
+ const algo = new WebCryptoRsaSha512();
+ const data = "test data to sign";
+
+ algo.getSignature(data, privateKey, async (err, signature) => {
+ if (err) return done(err);
+
+ expect(signature).to.be.a("string");
+
+ const crypto = await import("crypto");
+ const publicKeyObj = crypto.createPublicKey(publicKey);
+ const spkiPem = publicKeyObj.export({ type: "spki", format: "pem" }) as string;
+
+ algo.verifySignature(data, spkiPem, signature!, (verifyErr, isValid) => {
+ if (verifyErr) return done(verifyErr);
+
+ expect(isValid).to.be.true;
+ done();
+ });
+ });
+ });
+
+ it("should have correct algorithm name", function () {
+ const algo = new WebCryptoRsaSha512();
+ expect(algo.getAlgorithmName()).to.equal("http://www.w3.org/2001/04/xmldsig-more#rsa-sha512");
+ });
+ });
+});
+
+describe("WebCrypto HMAC Signature Algorithm", function () {
+ describe("WebCryptoHmacSha1", function () {
+ it("should sign and verify data correctly", function (done) {
+ const algo = new WebCryptoHmacSha1();
+ const data = "test data to sign";
+ const key = "my-secret-key";
+
+ algo.getSignature(data, key, (err, signature) => {
+ if (err) return done(err);
+
+ expect(signature).to.be.a("string");
+ if (signature) {
+ expect(signature.length).to.be.greaterThan(0);
+ }
+
+ algo.verifySignature(data, key, signature!, (verifyErr, isValid) => {
+ if (verifyErr) return done(verifyErr);
+
+ expect(isValid).to.be.true;
+ done();
+ });
+ });
+ });
+
+ it("should fail verification with wrong key", function (done) {
+ const algo = new WebCryptoHmacSha1();
+ const data = "test data to sign";
+ const key = "my-secret-key";
+
+ algo.getSignature(data, key, (err, signature) => {
+ if (err) return done(err);
+
+ algo.verifySignature(data, "wrong-key", signature!, (verifyErr, isValid) => {
+ if (verifyErr) return done(verifyErr);
+
+ expect(isValid).to.be.false;
+ done();
+ });
+ });
+ });
+
+ it("should have correct algorithm name", function () {
+ const algo = new WebCryptoHmacSha1();
+ expect(algo.getAlgorithmName()).to.equal("http://www.w3.org/2000/09/xmldsig#hmac-sha1");
+ });
+ });
+});
+
+describe("WebCrypto XML Signing and Verification", function () {
+ let privateKey: string;
+ let publicKey: string;
+
+ before(function () {
+ privateKey = readFileSync("./test/static/client.pem", "utf8");
+ publicKey = readFileSync("./test/static/client_public.pem", "utf8");
+ });
+
+ it("should sign and verify XML with WebCrypto RSA-SHA256", function (done) {
+ const xml = "Harry Potter";
+
+ // Sign
+ const sig = new SignedXml();
+ sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
+ sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
+ sig.privateKey = privateKey;
+
+ sig.addReference({
+ xpath: "//*[local-name(.)='book']",
+ digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
+ transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
+ });
+
+ // Register WebCrypto algorithms
+ sig.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+ sig.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] =
+ WebCryptoRsaSha256;
+
+ sig.computeSignature(xml, (err) => {
+ if (err) {
+ return done(err);
+ }
+
+ const signedXml = sig.getSignedXml();
+
+ expect(signedXml).to.include("");
+
+ // Verify
+ const verifier = new SignedXml();
+
+ // Convert certificate to SPKI format for WebCrypto
+ import("crypto")
+ .then((crypto) => {
+ const publicKeyObj = crypto.createPublicKey(publicKey);
+ const spkiPem = publicKeyObj.export({ type: "spki", format: "pem" }) as string;
+
+ verifier.publicCert = spkiPem;
+
+ verifier.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+ verifier.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] =
+ WebCryptoRsaSha256;
+
+ // Load signature from the signed XML
+ const doc = new xmldom.DOMParser().parseFromString(signedXml);
+ const signature = verifier.findSignatures(doc)[0];
+ verifier.loadSignature(signature);
+
+ verifier.checkSignature(signedXml, (error, isValid) => {
+ if (error) {
+ return done(error);
+ }
+ expect(isValid).to.be.true;
+ done();
+ });
+ })
+ .catch(done);
+ });
+ });
+
+ it("should sign and verify XML with WebCrypto RSA-SHA1", function (done) {
+ const xml = "test content";
+
+ // Sign
+ const sig = new SignedXml();
+ sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
+ sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
+ sig.privateKey = privateKey;
+
+ sig.addReference({
+ xpath: "//*[local-name(.)='data']",
+ digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
+ transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
+ });
+
+ sig.HashAlgorithms["http://www.w3.org/2000/09/xmldsig#sha1"] = WebCryptoSha1;
+ sig.SignatureAlgorithms["http://www.w3.org/2000/09/xmldsig#rsa-sha1"] = WebCryptoRsaSha1;
+
+ sig.computeSignature(xml, (err) => {
+ if (err) {
+ return done(err);
+ }
+
+ const signedXml = sig.getSignedXml();
+
+ // Verify
+ const verifier = new SignedXml();
+
+ import("crypto")
+ .then((crypto) => {
+ const publicKeyObj = crypto.createPublicKey(publicKey);
+ const spkiPem = publicKeyObj.export({ type: "spki", format: "pem" }) as string;
+
+ verifier.publicCert = spkiPem;
+
+ verifier.HashAlgorithms["http://www.w3.org/2000/09/xmldsig#sha1"] = WebCryptoSha1;
+ verifier.SignatureAlgorithms["http://www.w3.org/2000/09/xmldsig#rsa-sha1"] =
+ WebCryptoRsaSha1;
+
+ // Load signature from the signed XML
+ const doc = new xmldom.DOMParser().parseFromString(signedXml);
+ const signature = verifier.findSignatures(doc)[0];
+ verifier.loadSignature(signature);
+
+ verifier.checkSignature(signedXml, (error, isValid) => {
+ if (error) {
+ return done(error);
+ }
+ expect(isValid).to.be.true;
+ done();
+ });
+ })
+ .catch(done);
+ });
+ });
+
+ it("should detect invalid signatures", function (done) {
+ const xml = "test content";
+
+ // Sign
+ const sig = new SignedXml();
+ sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
+ sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
+ sig.privateKey = privateKey;
+
+ sig.addReference({
+ xpath: "//*[local-name(.)='data']",
+ digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
+ transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
+ });
+
+ sig.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+ sig.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] =
+ WebCryptoRsaSha256;
+
+ sig.computeSignature(xml, (err) => {
+ if (err) {
+ return done(err);
+ }
+
+ let signedXml = sig.getSignedXml();
+
+ // Tamper with the signed data
+ signedXml = signedXml.replace("test content", "tampered content");
+
+ // Verify should fail
+ const verifier = new SignedXml();
+
+ import("crypto")
+ .then((crypto) => {
+ const publicKeyObj = crypto.createPublicKey(publicKey);
+ const spkiPem = publicKeyObj.export({ type: "spki", format: "pem" }) as string;
+
+ verifier.publicCert = spkiPem;
+
+ verifier.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+ verifier.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] =
+ WebCryptoRsaSha256;
+
+ // Load signature from the signed XML
+ const doc = new xmldom.DOMParser().parseFromString(signedXml);
+ const signature = verifier.findSignatures(doc)[0];
+ verifier.loadSignature(signature);
+
+ verifier.checkSignature(signedXml, (error, isValid) => {
+ expect(error).to.exist;
+ expect(error?.message).to.include("invalid signature");
+ expect(isValid).to.be.false;
+ done();
+ });
+ })
+ .catch(done);
+ });
+ });
+
+ it("should throw error when using async algorithms with sync methods", function () {
+ const xml = "test";
+
+ const sig = new SignedXml();
+ sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
+ sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
+ sig.privateKey = privateKey;
+
+ sig.addReference({
+ xpath: "//*[local-name(.)='data']",
+ digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
+ transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
+ });
+
+ // Register WebCrypto algorithms
+ sig.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+ sig.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] =
+ WebCryptoRsaSha256;
+
+ // Should throw when using sync method with async algorithm
+ // Hash computation happens first, so we get hash algorithm error
+ expect(() => sig.computeSignature(xml)).to.throw(
+ "WebCrypto hash algorithms are async and require a callback",
+ );
+ });
+
+ it("should throw error when verifying with async algorithms using sync methods", function (done) {
+ const xml = "test";
+
+ // First, create a signed XML using callbacks
+ const signer = new SignedXml();
+ signer.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
+ signer.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
+ signer.privateKey = privateKey;
+
+ signer.addReference({
+ xpath: "//*[local-name(.)='data']",
+ digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
+ transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
+ });
+
+ signer.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+ signer.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] =
+ WebCryptoRsaSha256;
+
+ signer.computeSignature(xml, (err) => {
+ if (err) {
+ return done(err);
+ }
+
+ const signedXml = signer.getSignedXml();
+
+ // Now try to verify using sync method - should throw
+ const verifier = new SignedXml();
+
+ import("crypto")
+ .then((crypto) => {
+ const publicKeyObj = crypto.createPublicKey(publicKey);
+ const spkiPem = publicKeyObj.export({ type: "spki", format: "pem" }) as string;
+
+ verifier.publicCert = spkiPem;
+ verifier.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+ verifier.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] =
+ WebCryptoRsaSha256;
+
+ // Load signature first
+ const doc = new xmldom.DOMParser().parseFromString(signedXml);
+ const signature = verifier.findSignatures(doc)[0];
+ verifier.loadSignature(signature);
+
+ // Should throw when using sync method with async algorithm for verification
+ // Hash validation happens first, so we get hash algorithm error
+ expect(() => verifier.checkSignature(signedXml)).to.throw(
+ "WebCrypto hash algorithms are async and require a callback",
+ );
+ done();
+ })
+ .catch(done);
+ });
+ });
+
+ it("should work with multiple references", function (done) {
+ const xml = "- First
- Second
";
+
+ const sig = new SignedXml();
+ sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
+ sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
+ sig.privateKey = privateKey;
+
+ // Add multiple references
+ sig.addReference({
+ xpath: "//*[local-name(.)='item'][@id='1']",
+ digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
+ transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
+ });
+
+ sig.addReference({
+ xpath: "//*[local-name(.)='item'][@id='2']",
+ digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
+ transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
+ });
+
+ sig.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+ sig.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] =
+ WebCryptoRsaSha256;
+
+ sig.computeSignature(xml, (err) => {
+ if (err) {
+ return done(err);
+ }
+
+ const signedXml = sig.getSignedXml();
+
+ // Verify
+ const verifier = new SignedXml();
+
+ import("crypto")
+ .then((crypto) => {
+ const publicKeyObj = crypto.createPublicKey(publicKey);
+ const spkiPem = publicKeyObj.export({ type: "spki", format: "pem" }) as string;
+
+ verifier.publicCert = spkiPem;
+
+ verifier.HashAlgorithms["http://www.w3.org/2001/04/xmlenc#sha256"] = WebCryptoSha256;
+ verifier.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] =
+ WebCryptoRsaSha256;
+
+ // Load signature from the signed XML
+ const doc = new xmldom.DOMParser().parseFromString(signedXml);
+ const signature = verifier.findSignatures(doc)[0];
+ verifier.loadSignature(signature);
+
+ verifier.checkSignature(signedXml, (error, isValid) => {
+ if (error) {
+ return done(error);
+ }
+ expect(isValid).to.be.true;
+ done();
+ });
+ })
+ .catch(done);
+ });
+ });
+});
+
+describe("WebCrypto HMAC XML Signing", function () {
+ it("should sign and verify XML with HMAC-SHA1", function (done) {
+ const xml = "HMAC test";
+ const hmacKey = "my-secret-hmac-key";
+
+ // Sign
+ const sig = new SignedXml();
+ sig.signatureAlgorithm = "http://www.w3.org/2000/09/xmldsig#hmac-sha1";
+ sig.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
+ sig.privateKey = hmacKey;
+
+ sig.addReference({
+ xpath: "//*[local-name(.)='data']",
+ digestAlgorithm: "http://www.w3.org/2000/09/xmldsig#sha1",
+ transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
+ });
+
+ sig.HashAlgorithms["http://www.w3.org/2000/09/xmldsig#sha1"] = WebCryptoSha1;
+ sig.SignatureAlgorithms["http://www.w3.org/2000/09/xmldsig#hmac-sha1"] = WebCryptoHmacSha1;
+
+ sig.computeSignature(xml, (err) => {
+ if (err) {
+ return done(err);
+ }
+
+ const signedXml = sig.getSignedXml();
+
+ // Verify
+ const verifier = new SignedXml();
+ verifier.publicCert = hmacKey;
+
+ verifier.HashAlgorithms["http://www.w3.org/2000/09/xmldsig#sha1"] = WebCryptoSha1;
+ verifier.SignatureAlgorithms["http://www.w3.org/2000/09/xmldsig#hmac-sha1"] =
+ WebCryptoHmacSha1;
+
+ // Load signature from the signed XML
+ const doc = new xmldom.DOMParser().parseFromString(signedXml);
+ const signature = verifier.findSignatures(doc)[0];
+ verifier.loadSignature(signature);
+
+ verifier.checkSignature(signedXml, (error, isValid) => {
+ if (error) {
+ return done(error);
+ }
+ expect(isValid).to.be.true;
+ done();
+ });
+ });
+ });
+});
+
+describe("WebCrypto Callback-Style API", function () {
+ let privateKey: string;
+ let publicKey: string;
+
+ before(function () {
+ privateKey = readFileSync("./test/static/client.pem", "utf8");
+ publicKey = readFileSync("./test/static/client_public.pem", "utf8");
+ });
+
+ it("should support callback-style getSignature for RSA-SHA1", function (done) {
+ const signer = new WebCryptoRsaSha1();
+ const data = "test data";
+
+ signer.getSignature(data, privateKey, (err, signature) => {
+ if (err) {
+ return done(err);
+ }
+ expect(signature).to.be.a("string");
+ if (signature) {
+ expect(signature.length).to.be.greaterThan(0);
+ }
+ done();
+ });
+ });
+
+ it("should support callback-style verifySignature for RSA-SHA256", function (done) {
+ const signer = new WebCryptoRsaSha256();
+ const data = "test data";
+
+ // First sign
+ signer.getSignature(data, privateKey, async (err, signature) => {
+ if (err || !signature) {
+ return done(err || new Error("No signature"));
+ }
+
+ // Then verify with callback
+ const crypto = await import("crypto");
+ const publicKeyObj = crypto.createPublicKey(publicKey);
+ const spkiPem = publicKeyObj.export({ type: "spki", format: "pem" }) as string;
+
+ signer.verifySignature(data, spkiPem, signature, (verifyErr, isValid) => {
+ if (verifyErr) {
+ return done(verifyErr);
+ }
+ expect(isValid).to.be.true;
+ done();
+ });
+ });
+ });
+
+ it("should support callback-style for HMAC-SHA1", function (done) {
+ const signer = new WebCryptoHmacSha1();
+ const data = "test data";
+ const key = "my-hmac-key";
+
+ signer.getSignature(data, key, (err, signature) => {
+ if (err || !signature) {
+ return done(err || new Error("No signature"));
+ }
+
+ signer.verifySignature(data, key, signature, (verifyErr, isValid) => {
+ if (verifyErr) {
+ return done(verifyErr);
+ }
+ expect(isValid).to.be.true;
+ done();
+ });
+ });
+ });
+
+ it("should handle errors in callback-style API", function (done) {
+ const signer = new WebCryptoRsaSha1();
+ const data = "test data";
+ const invalidKey = "not a valid key";
+
+ signer.getSignature(data, invalidKey, (err) => {
+ expect(err).to.exist;
+ expect(err).to.be.instanceOf(Error);
+ done();
+ });
+ });
+});
+
+describe("WebCrypto Key Type Support", function () {
+ let privateKeyString: string;
+ let privateKeyBuffer: Buffer;
+ let publicKeyString: string;
+ let publicKeyBuffer: Buffer;
+
+ before(function () {
+ privateKeyString = readFileSync("./test/static/client.pem", "utf8");
+ privateKeyBuffer = readFileSync("./test/static/client.pem");
+ publicKeyString = readFileSync("./test/static/client_public.pem", "utf8");
+ publicKeyBuffer = readFileSync("./test/static/client_public.pem");
+ });
+
+ it("should accept Buffer as private key for signing", function (done) {
+ const signer = new WebCryptoRsaSha256();
+ const data = "test data with buffer key";
+
+ signer.getSignature(data, privateKeyBuffer, (err, signature) => {
+ if (err) return done(err);
+
+ expect(signature).to.be.a("string");
+ if (signature) {
+ expect(signature.length).to.be.greaterThan(0);
+ }
+ done();
+ });
+ });
+
+ it("should accept Buffer as public key for verification", function (done) {
+ const signer = new WebCryptoRsaSha256();
+ const data = "test data with buffer key";
+
+ // Sign with string key
+ signer.getSignature(data, privateKeyString, async (err, signature) => {
+ if (err || !signature) return done(err);
+
+ // Verify with buffer key
+ const crypto = await import("crypto");
+ const publicKeyObj = crypto.createPublicKey(publicKeyBuffer);
+ const spkiPem = publicKeyObj.export({ type: "spki", format: "pem" }) as string;
+
+ signer.verifySignature(data, Buffer.from(spkiPem), signature, (verifyErr, isValid) => {
+ if (verifyErr) return done(verifyErr);
+
+ expect(isValid).to.be.true;
+ done();
+ });
+ });
+ });
+
+ it("should accept KeyObject as private key for signing", function (done) {
+ import("crypto")
+ .then((crypto) => {
+ const signer = new WebCryptoRsaSha256();
+ const data = "test data with KeyObject";
+
+ const privateKeyObj = crypto.createPrivateKey(privateKeyString);
+ signer.getSignature(data, privateKeyObj, (err, signature) => {
+ if (err) return done(err);
+
+ expect(signature).to.be.a("string");
+ if (signature) {
+ expect(signature.length).to.be.greaterThan(0);
+ }
+ done();
+ });
+ })
+ .catch(done);
+ });
+
+ it("should accept KeyObject as public key for verification", function (done) {
+ import("crypto")
+ .then((crypto) => {
+ const signer = new WebCryptoRsaSha256();
+ const data = "test data with KeyObject";
+
+ // Sign with string key
+ signer.getSignature(data, privateKeyString, (err, signature) => {
+ if (err || !signature) return done(err);
+
+ // Verify with KeyObject
+ const publicKeyObj = crypto.createPublicKey(publicKeyString);
+
+ signer.verifySignature(data, publicKeyObj, signature, (verifyErr, isValid) => {
+ if (verifyErr) return done(verifyErr);
+
+ expect(isValid).to.be.true;
+ done();
+ });
+ });
+ })
+ .catch(done);
+ });
+
+ it("should accept secret KeyObject for HMAC signing", function (done) {
+ import("crypto")
+ .then((crypto) => {
+ const signer = new WebCryptoHmacSha1();
+ const data = "test data with secret KeyObject";
+
+ // Create a secret KeyObject
+ const secretKey = crypto.createSecretKey(
+ Uint8Array.from(Buffer.from("my-hmac-secret-key")),
+ );
+ signer.getSignature(data, secretKey, (err, signature) => {
+ if (err || !signature) return done(err);
+
+ expect(signature).to.be.a("string");
+ expect(signature.length).to.be.greaterThan(0);
+
+ // Verify with same secret KeyObject
+ signer.verifySignature(data, secretKey, signature, (verifyErr, isValid) => {
+ if (verifyErr) return done(verifyErr);
+
+ expect(isValid).to.be.true;
+ done();
+ });
+ });
+ })
+ .catch(done);
+ });
+
+ it("should accept Uint8Array as key", function (done) {
+ const signer = new WebCryptoRsaSha256();
+ const data = "test data with Uint8Array";
+
+ const privateKeyUint8 = new Uint8Array(privateKeyBuffer);
+ signer.getSignature(data, privateKeyUint8, (err, signature) => {
+ if (err) return done(err);
+
+ expect(signature).to.be.a("string");
+ if (signature) {
+ expect(signature.length).to.be.greaterThan(0);
+ }
+ done();
+ });
+ });
+
+ it("should work with Buffer keys in callback-style API", function (done) {
+ const signer = new WebCryptoRsaSha256();
+ const data = "test data with buffer in callback";
+
+ signer.getSignature(data, privateKeyBuffer, (err, signature) => {
+ if (err) {
+ return done(err);
+ }
+ expect(signature).to.be.a("string");
+ if (signature) {
+ expect(signature.length).to.be.greaterThan(0);
+ }
+ done();
+ });
+ });
+});