From b5bdba130c5419744eec2b8bb778c724d8c192f7 Mon Sep 17 00:00:00 2001 From: Matt Schoch Date: Wed, 3 Apr 2024 14:34:15 -0400 Subject: [PATCH] Adding es256 & rs256 signing, removing jose dependency for signJWT --- .../src/lib/__test__/unit/sign.spec.ts | 8 ++ .../src/lib/__test__/unit/util.spec.ts | 8 ++ packages/signature/src/lib/sign.ts | 87 ++++++++++++++++--- packages/signature/src/lib/utils.ts | 33 +++---- 4 files changed, 111 insertions(+), 25 deletions(-) diff --git a/packages/signature/src/lib/__test__/unit/sign.spec.ts b/packages/signature/src/lib/__test__/unit/sign.spec.ts index 30b1952c9..9be48b62e 100644 --- a/packages/signature/src/lib/__test__/unit/sign.spec.ts +++ b/packages/signature/src/lib/__test__/unit/sign.spec.ts @@ -11,6 +11,7 @@ import { base64UrlToHex, ellipticPrivateKeyToHex, ellipticPublicKeyToHex, + generateJwk, hexToBase64Url, privateKeyToHex, secp256k1PrivateKeyToJwk, @@ -94,6 +95,13 @@ describe('sign', () => { expect(signature).toBe('FFI6M5oFpbQqq0-xhe5DgPVHj4CKoVF4F3K3cg1MRY1COqWatQNsSn2MrqJ10BbGLe7i76KRMDj4biqnZkxwsw') }) + it('should sign RS256 correctly', async () => { + const key = await generateJwk(Alg.RS256) + const jwt = await signJwt(payload, key) + const verifiedJwt = await verifyJwt(jwt, key) + expect(verifiedJwt.payload).toEqual(payload) + }) + it('should sign EIP191 correctly', async () => { const message = [HEADER_PART, PAYLOAD_PART].join('.') const signer = buildSignerEip191(UNSAFE_PRIVATE_KEY) diff --git a/packages/signature/src/lib/__test__/unit/util.spec.ts b/packages/signature/src/lib/__test__/unit/util.spec.ts index cf28dcbfc..c2681564d 100644 --- a/packages/signature/src/lib/__test__/unit/util.spec.ts +++ b/packages/signature/src/lib/__test__/unit/util.spec.ts @@ -187,4 +187,12 @@ describe('privateKeyToJwk', () => { it('throws an error for RS256 alg', () => { expect(() => privateKeyToJwk(p256HexPrivateKey, Alg.RS256)).toThrow() }) + + // TODO: Implement this test & functionality. Not direct need today, but should be done. + // it('converts a valid rs256 hex private key to JWK', async () => { + // const key = await generateJwk(Alg.RS256) + // const hex = privateKeyToHex(key) + // const jwk = privateKeyToJwk(hex, Alg.RS256) + // expect(rsaPrivateKeySchema.safeParse(jwk).success).toBe(true) + // }) }) diff --git a/packages/signature/src/lib/sign.ts b/packages/signature/src/lib/sign.ts index 88e767e1a..2b68267da 100644 --- a/packages/signature/src/lib/sign.ts +++ b/packages/signature/src/lib/sign.ts @@ -1,14 +1,25 @@ +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 { SignJWT, importJWK } from 'jose' +import * as crypto from 'node:crypto' +import { promisify } from 'node:util' import { isHex, signatureToHex, toBytes, toHex } from 'viem' +import { JwtError } from './error' import { hash } from './hash' import { canonicalize } from './json.util' import { jwkBaseSchema, privateKeySchema } from './schemas' -import { EcdsaSignature, Header, Hex, Jwk, JwsdHeader, PartialJwk, Payload, PrivateKey, SigningAlg } from './types' +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, + [SigningAlg.ES256K]: Alg.ES256K, + [SigningAlg.ES256]: Alg.ES256, + [SigningAlg.RS256]: Alg.RS256 +} const buildHeader = (jwk: Jwk, alg?: SigningAlg): Header => { const key = validateJwk({ @@ -16,6 +27,18 @@ const buildHeader = (jwk: Jwk, alg?: SigningAlg): Header => { jwk, errorMessage: 'Invalid JWK: failed to validate basic fields' }) + // Validate that the alg & the key alg are compatible + const headerAlg = alg || key.alg + const validKeyAlg = SigningAlgToKey[headerAlg] + if (key.alg !== validKeyAlg) { + throw new JwtError({ + message: 'Mismatch between jwk & signing alg', + context: { + jwkAlg: key.alg, + signingAlg: headerAlg + } + }) + } return { alg: alg || key.alg, kid: key.kid, @@ -23,12 +46,6 @@ const buildHeader = (jwk: Jwk, alg?: SigningAlg): Header => { } } -const fallbackSigner = async (jwk: PrivateKey, payload: Payload, header: Header) => { - const pk = await importJWK(jwk) - const signature = await new SignJWT(payload).setProtectedHeader(header).sign(pk) - return signature -} - export async function signJwsd( rawBody: string | object, header: JwsdHeader, @@ -120,8 +137,18 @@ export async function signJwt( case SigningAlg.EIP191: signature = await buildSignerEip191(privateKeyHex)(messageToSign) break - default: - return fallbackSigner(privateKey, payload, header) + case SigningAlg.ES256: + signature = await buildSignerEs256(privateKeyHex)(messageToSign) + break + case SigningAlg.RS256: + signature = await buildSignerRs256(jwk)(messageToSign) + break + default: { + throw new JwtError({ + message: 'Unsupported signing algorithm', + context: { alg: header.alg } + }) + } } } @@ -142,6 +169,21 @@ export const signSecp256k1 = (hash: Uint8Array, privateKey: Hex | string, isEth? } } +export const signP256 = (hash: Uint8Array, privateKey: Hex | string): EcdsaSignature => { + const pk = isHex(privateKey) ? privateKey.slice(2) : privateKey + const sig = p256.sign(hash, pk) + const { r, s, recovery } = sig + const rHex = toHex(r, { size: 32 }) + const sHex = toHex(s, { size: 32 }) + const recoveryBn = BigInt(recovery) + + return { + r: rHex, + s: sHex, + v: recoveryBn + } +} + export const buildSignerEs256k = (privateKey: Hex | string) => async (messageToSign: string): Promise => { @@ -172,3 +214,28 @@ export const buildSignerEip191 = const hexSignature = signatureToHex(signature) return hexToBase64Url(hexSignature) } + +export const buildSignerEs256 = + (privateKey: Hex | string) => + async (messageToSign: string): Promise => { + const hash = sha256Hash(messageToSign) + + const signature = signP256(hash, privateKey) + + const hexSignature = signatureToHex(signature) + return hexToBase64Url(hexSignature) + } + +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 hexSignature = toHex(signature) + return hexToBase64Url(hexSignature) + } diff --git a/packages/signature/src/lib/utils.ts b/packages/signature/src/lib/utils.ts index e2826a88e..739f21389 100644 --- a/packages/signature/src/lib/utils.ts +++ b/packages/signature/src/lib/utils.ts @@ -2,6 +2,7 @@ import { p256 } from '@noble/curves/p256' import { secp256k1 } from '@noble/curves/secp256k1' import { sha256 as sha256Hash } from '@noble/hashes/sha256' import { exportJWK, generateKeyPair } from 'jose' +import * as crypto from 'node:crypto' import { toHex } from 'viem' import { publicKeyToAddress } from 'viem/utils' import { JwtError } from './error' @@ -193,18 +194,18 @@ const rsaKeyToKid = (jwk: Jwk) => { return toHex(hash) } -// TODO: Define if this function is necessary, what should be the behavior when passing a RSA key to be hexified const rsaPubKeyToHex = (jwk: Jwk): Hex => { const key = validateJwk({ schema: rsaPublicKeySchema, jwk, errorMessage: 'Invalid RSA Public Key' }) - const dataToHash = `${key.n}:${key.e}` - - const binaryData = base64UrlToBytes(dataToHash) - const hash = sha256Hash(binaryData) - return toHex(hash) + const imported = crypto.createPublicKey({ + format: 'jwk', + key + }) + const keyData = imported.export({ format: 'pem', type: 'spki' }) + return toHex(keyData) } const rsaPrivateKeyToHex = (jwk: Jwk): Hex => { @@ -213,11 +214,14 @@ const rsaPrivateKeyToHex = (jwk: Jwk): Hex => { jwk, errorMessage: 'Invalid RSA Private Key' }) - const dataToHash = `${key.n}:${key.e}:${key.d}` + const imported = crypto.createPrivateKey({ + key, + format: 'jwk', + type: 'pkcs8' + }) + const keyData = imported.export({ format: 'pem', type: 'pkcs8' }) - const binaryData = base64UrlToBytes(dataToHash) - const hash = sha256Hash(binaryData) - return toHex(hash) + return toHex(keyData) } export const publicKeyToHex = (jwk: Jwk): Hex => { @@ -239,16 +243,15 @@ export const privateKeyToHex = (jwk: Jwk): Hex => { return rsaPrivateKeyToHex(jwk) } -export const privateKeyToJwk = (jwk: Hex, alg: Alg): PrivateKey => { +export const privateKeyToJwk = (key: Hex, alg: Alg): PrivateKey => { switch (alg) { case Alg.ES256K: - return secp256k1PrivateKeyToJwk(jwk) + return secp256k1PrivateKeyToJwk(key) case Alg.ES256: - return p256PrivateKeyToJwk(jwk) + return p256PrivateKeyToJwk(key) case Alg.RS256: throw new JwtError({ - message: 'Conversion from Hex to JWK not supported for RSA keys', - context: { jwk } + message: 'Conversion from Hex to JWK not supported for RSA keys' }) } }