From d1c787c447bd134e6a6da8be059c8353f92b2f9a Mon Sep 17 00:00:00 2001
From: Steven Luscher <steveluscher@users.noreply.github.com>
Date: Tue, 14 Jan 2025 23:18:22 -0800
Subject: [PATCH] Fix for key operations in Firefox (#60)

---
 .changeset/four-parents-retire.md                  |  5 +++++
 examples/deserialize-transaction/src/example.ts    |  4 +++-
 packages/keys/src/algorithm.ts                     |  4 ++++
 packages/keys/src/key-pair.ts                      |  9 ++++++---
 packages/keys/src/private-key.ts                   | 10 +++++++++-
 packages/keys/src/signatures.ts                    |  6 ++++--
 packages/webcrypto-ed25519-polyfill/src/install.ts |  2 +-
 7 files changed, 32 insertions(+), 8 deletions(-)
 create mode 100644 .changeset/four-parents-retire.md
 create mode 100644 packages/keys/src/algorithm.ts

diff --git a/.changeset/four-parents-retire.md b/.changeset/four-parents-retire.md
new file mode 100644
index 000000000..f16eea787
--- /dev/null
+++ b/.changeset/four-parents-retire.md
@@ -0,0 +1,5 @@
+---
+'@solana/keys': patch
+---
+
+Key operations now work in versions of Firefox that support `Ed25519` natively
diff --git a/examples/deserialize-transaction/src/example.ts b/examples/deserialize-transaction/src/example.ts
index 483f3f64c..bb397653d 100644
--- a/examples/deserialize-transaction/src/example.ts
+++ b/examples/deserialize-transaction/src/example.ts
@@ -218,7 +218,9 @@ const signedBySignature = decodedTransaction.signatures[signedByAddress]!;
 const sourceAddressBytes = getAddressEncoder().encode(signedByAddress);
 // Then we create a public Ed25519 key with those bytes
 // This is a SubtleCrypto CryptoKey object that we create with role `verify`
-const signedByPublicKey = await crypto.subtle.importKey('raw', sourceAddressBytes, 'Ed25519', true, ['verify']);
+const signedByPublicKey = await crypto.subtle.importKey('raw', sourceAddressBytes, { name: 'Ed25519' }, true, [
+    'verify',
+]);
 // Now we can verify the signature using that key
 const verifiedSignature = await verifySignature(signedByPublicKey, signedBySignature, decodedTransaction.messageBytes);
 log.info(
diff --git a/packages/keys/src/algorithm.ts b/packages/keys/src/algorithm.ts
new file mode 100644
index 000000000..e8adfdf99
--- /dev/null
+++ b/packages/keys/src/algorithm.ts
@@ -0,0 +1,4 @@
+export const ED25519_ALGORITHM_IDENTIFIER =
+    // Resist the temptation to convert this to a simple string; As of version 133.0.3, Firefox
+    // requires the object form of `AlgorithmIdentifier` and will throw a `DOMException` otherwise.
+    Object.freeze({ name: 'Ed25519' });
diff --git a/packages/keys/src/key-pair.ts b/packages/keys/src/key-pair.ts
index a6713a176..e61dbc64d 100644
--- a/packages/keys/src/key-pair.ts
+++ b/packages/keys/src/key-pair.ts
@@ -6,6 +6,7 @@ import {
     SolanaError,
 } from '@solana/errors';
 
+import { ED25519_ALGORITHM_IDENTIFIER } from './algorithm';
 import { createPrivateKeyFromBytes } from './private-key';
 import { getPublicKeyFromPrivateKey } from './public-key';
 import { signBytes, verifySignature } from './signatures';
@@ -13,11 +14,11 @@ import { signBytes, verifySignature } from './signatures';
 export async function generateKeyPair(): Promise<CryptoKeyPair> {
     await assertKeyGenerationIsAvailable();
     const keyPair = await crypto.subtle.generateKey(
-        /* algorithm */ 'Ed25519', // Native implementation status: https://github.com/WICG/webcrypto-secure-curves/issues/20
+        /* algorithm */ ED25519_ALGORITHM_IDENTIFIER, // Native implementation status: https://github.com/WICG/webcrypto-secure-curves/issues/20
         /* extractable */ false, // Prevents the bytes of the private key from being visible to JS.
         /* allowed uses */ ['sign', 'verify'],
     );
-    return keyPair;
+    return keyPair as CryptoKeyPair; // FIXME: See https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1878
 }
 
 export async function createKeyPairFromBytes(bytes: ReadonlyUint8Array, extractable?: boolean): Promise<CryptoKeyPair> {
@@ -27,7 +28,9 @@ export async function createKeyPairFromBytes(bytes: ReadonlyUint8Array, extracta
         throw new SolanaError(SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, { byteLength: bytes.byteLength });
     }
     const [publicKey, privateKey] = await Promise.all([
-        crypto.subtle.importKey('raw', bytes.slice(32), 'Ed25519', /* extractable */ true, ['verify']),
+        crypto.subtle.importKey('raw', bytes.slice(32), ED25519_ALGORITHM_IDENTIFIER, /* extractable */ true, [
+            'verify',
+        ]),
         createPrivateKeyFromBytes(bytes.slice(0, 32), extractable),
     ]);
 
diff --git a/packages/keys/src/private-key.ts b/packages/keys/src/private-key.ts
index c623f4630..991f90199 100644
--- a/packages/keys/src/private-key.ts
+++ b/packages/keys/src/private-key.ts
@@ -1,6 +1,8 @@
 import { ReadonlyUint8Array } from '@solana/codecs-core';
 import { SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH, SolanaError } from '@solana/errors';
 
+import { ED25519_ALGORITHM_IDENTIFIER } from './algorithm';
+
 function addPkcs8Header(bytes: ReadonlyUint8Array): ReadonlyUint8Array {
     // prettier-ignore
     return new Uint8Array([
@@ -46,5 +48,11 @@ export async function createPrivateKeyFromBytes(bytes: ReadonlyUint8Array, extra
         });
     }
     const privateKeyBytesPkcs8 = addPkcs8Header(bytes);
-    return await crypto.subtle.importKey('pkcs8', privateKeyBytesPkcs8, 'Ed25519', extractable ?? false, ['sign']);
+    return await crypto.subtle.importKey(
+        'pkcs8',
+        privateKeyBytesPkcs8,
+        ED25519_ALGORITHM_IDENTIFIER,
+        extractable ?? false,
+        ['sign'],
+    );
 }
diff --git a/packages/keys/src/signatures.ts b/packages/keys/src/signatures.ts
index f27a78871..e3881f95d 100644
--- a/packages/keys/src/signatures.ts
+++ b/packages/keys/src/signatures.ts
@@ -7,6 +7,8 @@ import {
     SolanaError,
 } from '@solana/errors';
 
+import { ED25519_ALGORITHM_IDENTIFIER } from './algorithm';
+
 export type Signature = string & { readonly __brand: unique symbol };
 export type SignatureBytes = Uint8Array & { readonly __brand: unique symbol };
 
@@ -58,7 +60,7 @@ export function isSignature(putativeSignature: string): putativeSignature is Sig
 
 export async function signBytes(key: CryptoKey, data: ReadonlyUint8Array): Promise<SignatureBytes> {
     assertSigningCapabilityIsAvailable();
-    const signedData = await crypto.subtle.sign('Ed25519', key, data);
+    const signedData = await crypto.subtle.sign(ED25519_ALGORITHM_IDENTIFIER, key, data);
     return new Uint8Array(signedData) as SignatureBytes;
 }
 
@@ -73,5 +75,5 @@ export async function verifySignature(
     data: ReadonlyUint8Array,
 ): Promise<boolean> {
     assertVerificationCapabilityIsAvailable();
-    return await crypto.subtle.verify('Ed25519', key, signature, data);
+    return await crypto.subtle.verify(ED25519_ALGORITHM_IDENTIFIER, key, signature, data);
 }
diff --git a/packages/webcrypto-ed25519-polyfill/src/install.ts b/packages/webcrypto-ed25519-polyfill/src/install.ts
index e238fce7d..0605787ab 100644
--- a/packages/webcrypto-ed25519-polyfill/src/install.ts
+++ b/packages/webcrypto-ed25519-polyfill/src/install.ts
@@ -12,7 +12,7 @@ import {
 function isAlgorithmEd25519(putativeEd25519Algorithm: AlgorithmIdentifier): boolean {
     const name =
         typeof putativeEd25519Algorithm === 'string' ? putativeEd25519Algorithm : putativeEd25519Algorithm.name;
-    return name.localeCompare('ed25519', 'en-US', { sensitivity: 'base' }) === 0;
+    return name.localeCompare('Ed25519', 'en-US', { sensitivity: 'base' }) === 0;
 }
 
 export function install() {