Skip to content

Commit

Permalink
Jwk Generation
Browse files Browse the repository at this point in the history
* add jwk generation, rename generic generation function to alg specific name

* add validation for public and private key

* adding test coverage on key generation

* wip fixing CI: signature invalid on fixture keys

* fixed fixture data

* reverting unecessary changes to test file

* fixed rebase errors, added a 'use' field to the opts of key generation

* removed unecessary console.log
  • Loading branch information
Ptroger authored Mar 25, 2024
1 parent 4ef84f7 commit 14f79db
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 3 deletions.
4 changes: 2 additions & 2 deletions packages/signature/src/lib/__test__/unit/sign.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
41 changes: 41 additions & 0 deletions packages/signature/src/lib/__test__/unit/util.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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<Secp256k1PrivateKey>(
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)
})
})
6 changes: 6 additions & 0 deletions packages/signature/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { z } from 'zod'
import {
jwkEoaSchema,
jwkSchema,
p256PrivateKeySchema,
p256PublicKeySchema,
privateKeySchema,
publicKeySchema,
rsaPrivateKeySchema,
secp256k1KeySchema,
secp256k1PrivateKeySchema,
secp256k1PublicKeySchema
Expand Down Expand Up @@ -48,6 +51,9 @@ export const Use = {
export type Use = (typeof Use)[keyof typeof Use]

export type Secp256k1PrivateKey = z.infer<typeof secp256k1PrivateKeySchema>
export type P256PrivateKey = z.infer<typeof p256PrivateKeySchema>
export type P256PublicKey = z.infer<typeof p256PublicKeySchema>
export type RsaPrivateKey = z.infer<typeof rsaPrivateKeySchema>
export type EoaPublicKey = z.infer<typeof jwkEoaSchema>
export type Secp256k1PublicKey = z.infer<typeof secp256k1PublicKeySchema>
export type Secp256k1KeySchema = z.infer<typeof secp256k1KeySchema>
Expand Down
113 changes: 112 additions & 1 deletion packages/signature/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand Down Expand Up @@ -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<RsaPrivateKey> => {
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<RsaPrivateKey>(rsaPrivateKeySchema, jwk, 'Invalid RSA Private Key JWK')
return pk
}

export const generateJwk = async (
alg: Alg,
opts?: {
keyId?: string
modulusLength?: number,
use?: Use,
}
): Promise<Jwk> => {
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}`)
}
}

0 comments on commit 14f79db

Please sign in to comment.