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() {