Skip to content

Commit

Permalink
replacing node:crypto with webcrypto SubtleCrypto
Browse files Browse the repository at this point in the history
  • Loading branch information
mattschoch committed Apr 3, 2024
1 parent b5bdba1 commit 2d760c3
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 51 deletions.
2 changes: 1 addition & 1 deletion packages/signature/src/lib/__test__/unit/sign.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 10 additions & 10 deletions packages/signature/src/lib/__test__/unit/util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})

Expand Down
26 changes: 15 additions & 11 deletions packages/signature/src/lib/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -229,13 +227,19 @@ export const buildSignerEs256 =
export const buildSignerRs256 =
(jwk: Jwk) =>
async (messageToSign: string): Promise<string> => {
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)
}
49 changes: 32 additions & 17 deletions packages/signature/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -194,44 +194,59 @@ const rsaKeyToKid = (jwk: Jwk) => {
return toHex(hash)
}

const rsaPubKeyToHex = (jwk: Jwk): Hex => {
const rsaPubKeyToHex = async (jwk: Jwk): Promise<Hex> => {
const key = validateJwk<RsaPublicKey>({
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<Hex> => {
const key = validateJwk<RsaPrivateKey>({
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<Hex> => {
if (jwk.kty === KeyTypes.EC) {
return ellipticPublicKeyToHex(jwk)
}
return rsaPubKeyToHex(jwk)
}

export const privateKeyToHex = (jwk: Jwk): Hex => {
export const privateKeyToHex = async (jwk: Jwk): Promise<Hex> => {
const key = validateJwk<PrivateKey>({
schema: privateKeySchema,
jwk,
Expand Down
29 changes: 17 additions & 12 deletions packages/signature/src/lib/verify.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'] : []),
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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<boolean> => {
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
}

Expand Down

0 comments on commit 2d760c3

Please sign in to comment.