diff --git a/packages/signature/src/lib/__test__/unit/sign.spec.ts b/packages/signature/src/lib/__test__/unit/sign.spec.ts index f27ec0a32..dd5105ffa 100644 --- a/packages/signature/src/lib/__test__/unit/sign.spec.ts +++ b/packages/signature/src/lib/__test__/unit/sign.spec.ts @@ -5,7 +5,7 @@ import { createPublicKey } from 'node:crypto' import { toHex, verifyMessage } from 'viem' import { privateKeyToAccount, signMessage } from 'viem/accounts' import { buildSignerEip191, buildSignerEs256k, signJwt } from '../../sign' -import { Alg, Payload, PrivateKey, SigningAlg } from '../../types' +import { Alg, Payload, SigningAlg } from '../../types' import { base64UrlToBytes, base64UrlToHex, @@ -38,7 +38,7 @@ describe('sign', () => { it('should sign build & sign es256 JWT correctly with a PEM', async () => { const key = await importPKCS8(PRIVATE_KEY_PEM, Alg.ES256) const jwk = await exportJWK(key) - const jwt = await signJwt(payload, { ...jwk, alg: Alg.ES256 } as PrivateKey) + const jwt = await signJwt(payload, { ...jwk, alg: Alg.ES256, crv: 'P-256', kty: 'EC', kid: 'somekid' }) const verified = await jwtVerify(jwt, key) expect(verified.payload).toEqual(payload) diff --git a/packages/signature/src/lib/__test__/unit/util.spec.ts b/packages/signature/src/lib/__test__/unit/util.spec.ts index dab91880e..e4ad7b2b8 100644 --- a/packages/signature/src/lib/__test__/unit/util.spec.ts +++ b/packages/signature/src/lib/__test__/unit/util.spec.ts @@ -1,4 +1,10 @@ +import { p256PrivateKeySchema, rsaPrivateKeySchema, secp256k1PrivateKeySchema } from '../../schemas' +import { buildSignerEip191, signJwt } from '../../sign' import { isHeader, isPayload } from '../../typeguards' +import { Alg, Secp256k1PrivateKey, SigningAlg } from '../../types' +import { generateJwk, secp256k1PrivateKeyToHex } from '../../utils' +import { validate } from '../../validate' +import { verifyJwt } from '../../verify' describe('isHeader', () => { it('returns true for a valid header object', () => { @@ -59,3 +65,38 @@ describe('isPayload', () => { expect(isPayload('string')).toBe(false) }) }) + +describe('generateKeys', () => { + it('generate a valid RSA key pair and return it as a JWK', async () => { + const key = await generateJwk(Alg.RS256) + expect(rsaPrivateKeySchema.safeParse(key).success).toBe(true) + }) + + it('generates a valid P-256 key pair and return it as a JWK', async () => { + const key = await generateJwk(Alg.ES256) + expect(p256PrivateKeySchema.safeParse(key).success).toBe(true) + }) + + it('generates a valid secp256k1 key pair and return it as a JWK', async () => { + const key = await generateJwk(Alg.ES256K) + expect(secp256k1PrivateKeySchema.safeParse(key).success).toBe(true) + }) + + it('can sign and verify with a generated secp256k1 key pair', async () => { + const key = await generateJwk(Alg.ES256K) + const message = 'test message' + const payload = { + requestHash: message + } + const validatedKey = validate( + secp256k1PrivateKeySchema, + key, + 'Invalid secp256k1 Private Key JWK' + ) + + const signer = buildSignerEip191(secp256k1PrivateKeyToHex(validatedKey)) + const signature = await signJwt(payload, key, { alg: SigningAlg.EIP191 }, signer) + const isValid = await verifyJwt(signature, key) + expect(isValid).not.toEqual(false) + }) +}) diff --git a/packages/signature/src/lib/types.ts b/packages/signature/src/lib/types.ts index f15319928..b86f47cbb 100644 --- a/packages/signature/src/lib/types.ts +++ b/packages/signature/src/lib/types.ts @@ -2,8 +2,11 @@ import { z } from 'zod' import { jwkEoaSchema, jwkSchema, + p256PrivateKeySchema, + p256PublicKeySchema, privateKeySchema, publicKeySchema, + rsaPrivateKeySchema, secp256k1KeySchema, secp256k1PrivateKeySchema, secp256k1PublicKeySchema @@ -48,6 +51,9 @@ export const Use = { export type Use = (typeof Use)[keyof typeof Use] export type Secp256k1PrivateKey = z.infer +export type P256PrivateKey = z.infer +export type P256PublicKey = z.infer +export type RsaPrivateKey = z.infer export type EoaPublicKey = z.infer export type Secp256k1PublicKey = z.infer export type Secp256k1KeySchema = z.infer diff --git a/packages/signature/src/lib/utils.ts b/packages/signature/src/lib/utils.ts index 78dde8d87..9ab6be8fc 100644 --- a/packages/signature/src/lib/utils.ts +++ b/packages/signature/src/lib/utils.ts @@ -1,8 +1,26 @@ +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 { toHex } from 'viem' import { publicKeyToAddress } from 'viem/utils' -import { Alg, Curves, Hex, KeyTypes, Secp256k1KeySchema, Secp256k1PrivateKey, Secp256k1PublicKey } from './types' +import { JwtError } from './error' +import { rsaPrivateKeySchema } from './schemas' +import { + Alg, + Curves, + Hex, + Jwk, + KeyTypes, + P256PrivateKey, + P256PublicKey, + RsaPrivateKey, + Secp256k1KeySchema, + Secp256k1PrivateKey, + Secp256k1PublicKey, + Use +} from './types' +import { validate } from './validate' export const algToJwk = ( alg: Alg @@ -55,6 +73,20 @@ export const secp256k1PublicKeyToJwk = (publicKey: Hex, keyId?: string): Secp256 } } +export const p256PublicKeyToJwk = (publicKey: Hex, keyId?: string): P256PublicKey => { + const hexPubKey = publicKey.slice(4) + const x = hexPubKey.slice(0, 64) + const y = hexPubKey.slice(64) + return { + kty: KeyTypes.EC, + crv: Curves.P256, + alg: Alg.ES256, + kid: keyId || publicKeyToAddress(publicKey), + x: hexToBase64Url(`0x${x}`), + y: hexToBase64Url(`0x${y}`) + } +} + // ES256k export const secp256k1PrivateKeyToJwk = (privateKey: Hex, keyId?: string): Secp256k1PrivateKey => { const publicKey = toHex(secp256k1.getPublicKey(privateKey.slice(2), false)) @@ -65,6 +97,19 @@ export const secp256k1PrivateKeyToJwk = (privateKey: Hex, keyId?: string): Secp2 } } +export const p256PrivateKeyToJwk = (privateKey: Hex, keyId?: string): P256PrivateKey => { + const publicKey = toHex(p256.getPublicKey(privateKey.slice(2), false)) + const publicJwk = p256PublicKeyToJwk(publicKey, keyId) + return { + ...publicJwk, + d: hexToBase64Url(privateKey) + } +} + +export const p256PrivateKeyToHex = (jwk: P256PrivateKey): Hex => { + return base64UrlToHex(jwk.d) +} + export const secp256k1PublicKeyToHex = (jwk: Secp256k1KeySchema): Hex => { const x = base64UrlToHex(jwk.x) const y = base64UrlToHex(jwk.y) @@ -97,3 +142,69 @@ export const base64UrlToHex = (base64Url: string): Hex => { export const base64UrlToBytes = (base64Url: string): Buffer => { return Buffer.from(base64UrlToBase64(base64Url), 'base64') } + +const rsaKeyToKid = (jwk: Jwk) => { + // Concatenate the 'n' and 'e' values, splitted by ':' + const dataToHash = `${jwk.n}:${jwk.e}` + + const binaryData = base64UrlToBytes(dataToHash) + const hash = sha256Hash(binaryData) + return toHex(hash) +} + +const generateRsaKeyPair = async ( + opts: { + keyId?: string + modulusLength?: number, + use?: Use, + } = { + modulusLength: 2048 + } +): Promise => { + const { privateKey } = await generateKeyPair(Alg.RS256, { + modulusLength: opts.modulusLength, + extractable: true + }) + + const partialJwk = await exportJWK(privateKey) + if (!partialJwk.n) { + throw new JwtError({ message: 'Invalid JWK; missing n', context: { partialJwk } }) + } + const jwk: Jwk = { + ...partialJwk, + alg: Alg.RS256, + kty: KeyTypes.RSA, + crv: undefined, + use: opts.use || undefined, + } + jwk.kid = opts.keyId || rsaKeyToKid(jwk); + + const pk = validate(rsaPrivateKeySchema, jwk, 'Invalid RSA Private Key JWK') + return pk +} + +export const generateJwk = async ( + alg: Alg, + opts?: { + keyId?: string + modulusLength?: number, + use?: Use, + } +): Promise => { + switch (alg) { + case Alg.ES256K: { + const privateKeyK1 = toHex(secp256k1.utils.randomPrivateKey()) + return secp256k1PrivateKeyToJwk(privateKeyK1, opts?.keyId) + } + case Alg.ES256: { + const privateKeyP256 = toHex(p256.utils.randomPrivateKey()) + return p256PrivateKeyToJwk(privateKeyP256, opts?.keyId) + } + case Alg.RS256: { + const jwk = await generateRsaKeyPair(opts) + return jwk + } + default: + throw new Error(`Unsupported algorithm: ${alg}`) + } +}