Skip to content

Commit

Permalink
Adding es256 & rs256 signing, removing jose dependency for signJWT
Browse files Browse the repository at this point in the history
  • Loading branch information
mattschoch committed Apr 3, 2024
1 parent f61cafe commit b5bdba1
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 25 deletions.
8 changes: 8 additions & 0 deletions packages/signature/src/lib/__test__/unit/sign.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
base64UrlToHex,
ellipticPrivateKeyToHex,
ellipticPublicKeyToHex,
generateJwk,
hexToBase64Url,
privateKeyToHex,
secp256k1PrivateKeyToJwk,
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions packages/signature/src/lib/__test__/unit/util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
// })
})
87 changes: 77 additions & 10 deletions packages/signature/src/lib/sign.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,51 @@
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<PartialJwk>({
schema: jwkBaseSchema,
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,
typ: 'JWT'
}
}

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,
Expand Down Expand Up @@ -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 }
})
}
}
}

Expand All @@ -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<string> => {
Expand Down Expand Up @@ -172,3 +214,28 @@ export const buildSignerEip191 =
const hexSignature = signatureToHex(signature)
return hexToBase64Url(hexSignature)
}

export const buildSignerEs256 =
(privateKey: Hex | string) =>
async (messageToSign: string): Promise<string> => {
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<string> => {
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)
}
33 changes: 18 additions & 15 deletions packages/signature/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<RsaPublicKey>({
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 => {
Expand All @@ -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 => {
Expand All @@ -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'
})
}
}
Expand Down

0 comments on commit b5bdba1

Please sign in to comment.