From 2d760c35c63e7661dee23e85ffdb5f7faf330ddc Mon Sep 17 00:00:00 2001 From: Matt Schoch Date: Wed, 3 Apr 2024 15:04:23 -0400 Subject: [PATCH] replacing node:crypto with webcrypto SubtleCrypto --- .../src/lib/__test__/unit/sign.spec.ts | 2 +- .../src/lib/__test__/unit/util.spec.ts | 20 ++++---- packages/signature/src/lib/sign.ts | 26 +++++----- packages/signature/src/lib/utils.ts | 49 ++++++++++++------- packages/signature/src/lib/verify.ts | 29 ++++++----- 5 files changed, 75 insertions(+), 51 deletions(-) diff --git a/packages/signature/src/lib/__test__/unit/sign.spec.ts b/packages/signature/src/lib/__test__/unit/sign.spec.ts index 9be48b62e..cad5f2b24 100644 --- a/packages/signature/src/lib/__test__/unit/sign.spec.ts +++ b/packages/signature/src/lib/__test__/unit/sign.spec.ts @@ -199,7 +199,7 @@ describe('sign', () => { throw new Error(`Key material for kid ${kid} not found.`) } const hash = sha256Hash(message) - const key = privateKeyToHex(privateKeyMaterial) + const key = await privateKeyToHex(privateKeyMaterial) const signature = signSecp256k1(hash, key) const hexSignature = signatureToHex(signature) diff --git a/packages/signature/src/lib/__test__/unit/util.spec.ts b/packages/signature/src/lib/__test__/unit/util.spec.ts index c2681564d..ceebdc1a9 100644 --- a/packages/signature/src/lib/__test__/unit/util.spec.ts +++ b/packages/signature/src/lib/__test__/unit/util.spec.ts @@ -142,34 +142,34 @@ describe('publicKeyToJwk', () => { }) describe('publicKeyToHex', () => { - it('converts a valid secp256k1 JWK to hex string', () => { - const hex = publicKeyToHex(k1Jwk) + it('converts a valid secp256k1 JWK to hex string', async () => { + const hex = await publicKeyToHex(k1Jwk) expect(hex).toBe(k1HexPublicKey) }) - it('converts a valid p256 JWK to hex string', () => { - const hex = publicKeyToHex(p256Jwk) + it('converts a valid p256 JWK to hex string', async () => { + const hex = await publicKeyToHex(p256Jwk) expect(hex).toBe(p256HexPublicKey) }) }) describe('privateKeyToHex', () => { - it('converts a valid secp256k1 private JWK to hex string', () => { - const hex = privateKeyToHex(k1Jwk) + it('converts a valid secp256k1 private JWK to hex string', async () => { + const hex = await privateKeyToHex(k1Jwk) expect(hex).toBe(k1HexPrivateKey) }) - it('converts a valid p256 private JWK to hex string', () => { - const hex = privateKeyToHex(p256Jwk) + it('converts a valid p256 private JWK to hex string', async () => { + const hex = await privateKeyToHex(p256Jwk) expect(hex).toBe(p256HexPrivateKey) }) - it('throws an error for invalid private key', () => { + it('throws an error for invalid private key', async () => { const jwk: Jwk = { kty: 'EC', crv: 'secp256k1' } - expect(() => privateKeyToHex(jwk)).toThrow('Invalid Private Key') + await expect(privateKeyToHex(jwk)).rejects.toThrow('Invalid Private Key') }) }) diff --git a/packages/signature/src/lib/sign.ts b/packages/signature/src/lib/sign.ts index 2b68267da..3815a1aa5 100644 --- a/packages/signature/src/lib/sign.ts +++ b/packages/signature/src/lib/sign.ts @@ -2,8 +2,7 @@ import { p256 } from '@noble/curves/p256' import { secp256k1 } from '@noble/curves/secp256k1' import { sha256 as sha256Hash } from '@noble/hashes/sha256' import { keccak_256 as keccak256 } from '@noble/hashes/sha3' -import * as crypto from 'node:crypto' -import { promisify } from 'node:util' +import { subtle } from 'crypto' import { isHex, signatureToHex, toBytes, toHex } from 'viem' import { JwtError } from './error' import { hash } from './hash' @@ -12,7 +11,6 @@ import { jwkBaseSchema, privateKeySchema } from './schemas' import { Alg, EcdsaSignature, Header, Hex, Jwk, JwsdHeader, PartialJwk, Payload, PrivateKey, SigningAlg } from './types' import { hexToBase64Url, privateKeyToHex, stringToBase64Url } from './utils' import { validateJwk } from './validate' -const cryptoSign = promisify(crypto.sign) const SigningAlgToKey = { [SigningAlg.EIP191]: Alg.ES256K, @@ -129,7 +127,7 @@ export async function signJwt( jwk, errorMessage: 'Invalid JWK: failed to validate private key' }) - const privateKeyHex = privateKeyToHex(privateKey) + const privateKeyHex = await privateKeyToHex(privateKey) switch (header.alg) { case SigningAlg.ES256K: signature = await buildSignerEs256k(privateKeyHex)(messageToSign) @@ -229,13 +227,19 @@ export const buildSignerEs256 = export const buildSignerRs256 = (jwk: Jwk) => async (messageToSign: string): Promise => { - const key = crypto.createPrivateKey({ - key: jwk, - format: 'jwk', - type: 'pkcs8' - }) - const signature = await cryptoSign('sha256', Buffer.from(messageToSign), key) + const key = await subtle.importKey( + 'jwk', + jwk, + { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256' + }, + false, + ['sign'] + ) + + const signature = await subtle.sign('RSASSA-PKCS1-v1_5', key, Buffer.from(messageToSign)) - const hexSignature = toHex(signature) + const hexSignature = toHex(new Uint8Array(signature)) return hexToBase64Url(hexSignature) } diff --git a/packages/signature/src/lib/utils.ts b/packages/signature/src/lib/utils.ts index 739f21389..b6e74cf54 100644 --- a/packages/signature/src/lib/utils.ts +++ b/packages/signature/src/lib/utils.ts @@ -1,8 +1,8 @@ import { p256 } from '@noble/curves/p256' import { secp256k1 } from '@noble/curves/secp256k1' import { sha256 as sha256Hash } from '@noble/hashes/sha256' +import { subtle } from 'crypto' import { exportJWK, generateKeyPair } from 'jose' -import * as crypto from 'node:crypto' import { toHex } from 'viem' import { publicKeyToAddress } from 'viem/utils' import { JwtError } from './error' @@ -194,44 +194,59 @@ const rsaKeyToKid = (jwk: Jwk) => { return toHex(hash) } -const rsaPubKeyToHex = (jwk: Jwk): Hex => { +const rsaPubKeyToHex = async (jwk: Jwk): Promise => { const key = validateJwk({ schema: rsaPublicKeySchema, jwk, errorMessage: 'Invalid RSA Public Key' }) - const imported = crypto.createPublicKey({ - format: 'jwk', - key - }) - const keyData = imported.export({ format: 'pem', type: 'spki' }) - return toHex(keyData) + + const imported = await subtle.importKey( + 'jwk', + key, + { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256' + }, + true, + ['verify'] + ) + + const keyData = await subtle.exportKey('spki', imported) + + return toHex(new Uint8Array(keyData)) } -const rsaPrivateKeyToHex = (jwk: Jwk): Hex => { +const rsaPrivateKeyToHex = async (jwk: Jwk): Promise => { const key = validateJwk({ schema: rsaPrivateKeySchema, jwk, errorMessage: 'Invalid RSA Private Key' }) - const imported = crypto.createPrivateKey({ + const imported = await subtle.importKey( + 'jwk', key, - format: 'jwk', - type: 'pkcs8' - }) - const keyData = imported.export({ format: 'pem', type: 'pkcs8' }) + { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256' + }, + true, + ['sign'] + ) + + const keyData = await subtle.exportKey('pkcs8', imported) - return toHex(keyData) + return toHex(new Uint8Array(keyData)) } -export const publicKeyToHex = (jwk: Jwk): Hex => { +export const publicKeyToHex = async (jwk: Jwk): Promise => { if (jwk.kty === KeyTypes.EC) { return ellipticPublicKeyToHex(jwk) } return rsaPubKeyToHex(jwk) } -export const privateKeyToHex = (jwk: Jwk): Hex => { +export const privateKeyToHex = async (jwk: Jwk): Promise => { const key = validateJwk({ schema: privateKeySchema, jwk, diff --git a/packages/signature/src/lib/verify.ts b/packages/signature/src/lib/verify.ts index 88e696c47..e8f54a6d0 100644 --- a/packages/signature/src/lib/verify.ts +++ b/packages/signature/src/lib/verify.ts @@ -1,8 +1,7 @@ import { p256 } from '@noble/curves/p256' import { secp256k1 } from '@noble/curves/secp256k1' import { sha256 as sha256Hash } from '@noble/hashes/sha256' -import * as crypto from 'node:crypto' -import { promisify } from 'node:util' +import { subtle } from 'crypto' import { hexToBytes, isAddressEqual, recoverAddress } from 'viem' import { decodeJwsd, decodeJwt } from './decode' import { JwtError } from './error' @@ -26,8 +25,6 @@ import { import { base64UrlToHex, hexToBase64Url, nowSeconds, publicKeyToHex } from './utils' import { buildJwkValidator } from './validate' -const cryptoVerify = promisify(crypto.verify) - export const checkRequiredClaims = (payload: Payload, opts: JwtVerifyOptions): boolean => { const requiredClaims = [ ...(opts.issuer ? ['iss'] : []), @@ -126,7 +123,7 @@ export const verifySecp256k1 = async (sig: Hex, hash: Uint8Array, jwk: PublicKey if (jwk.alg !== Alg.ES256K) { throw new JwtError({ message: 'Invalid JWK: signature requres ES256K', context: { jwk } }) } - const pubKey = publicKeyToHex(jwk) + const pubKey = await publicKeyToHex(jwk) // A eth sig has a `v` value of 27 or 28, so we need to remove that to get the signature // And we remove the 0x prefix. So that means we slice the first and last 2 bytes, leaving the 128 character signature const isValid = secp256k1.verify(sig.slice(2, 130), hash, pubKey.slice(2)) === true @@ -138,7 +135,7 @@ export const verifyP256 = async (sig: Hex, hash: Uint8Array, jwk: PublicKey): Pr throw new JwtError({ message: 'Invalid JWK: signature requires ES256', context: { jwk } }) } - const pubKey = publicKeyToHex(jwk) + const pubKey = await publicKeyToHex(jwk) const isValid = p256.verify(sig.slice(2, 130), hash, pubKey.slice(2)) === true return isValid } @@ -166,13 +163,21 @@ export const verifyEip191 = async (sig: Hex, msg: string, jwk: PublicKey): Promi } export const verifyRs256 = async (sig: Hex, msg: string, jwk: PublicKey): Promise => { - const key = crypto.createPublicKey({ - format: 'jwk', - key: jwk - }) - const msgBuffer = Buffer.from(msg) - const isValid = await cryptoVerify('sha256', msgBuffer, key, hexToBytes(sig)) + + const key = await subtle.importKey( + 'jwk', + jwk, + { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256' + }, + true, + ['verify'] + ) + + const isValid = await subtle.verify('RSASSA-PKCS1-v1_5', key, hexToBytes(sig), msgBuffer) + return !!isValid }