Skip to content

Commit

Permalink
Consolidating sign & signJwt
Browse files Browse the repository at this point in the history
  • Loading branch information
mattschoch committed Mar 7, 2024
1 parent 52331ce commit 56157b8
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 131 deletions.
2 changes: 1 addition & 1 deletion packages/signature/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { decode } from './lib/decode'
export { hash } from './lib/hash-request'
export { sign } from './lib/sign'
export * from './lib/sign'
export * from './lib/types'
export { verify } from './lib/verify'
export * from './lib/utils'
36 changes: 0 additions & 36 deletions packages/signature/src/lib/__test__/unit/eoa-keys.spec.ts

This file was deleted.

48 changes: 31 additions & 17 deletions packages/signature/src/lib/__test__/unit/sign.spec.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,47 @@
import { secp256k1 } from '@noble/curves/secp256k1'
import { sha256 as sha256Hash } from '@noble/hashes/sha256'
import { exportJWK, importPKCS8, jwtVerify } from 'jose'
import { verifyMessage } from 'viem'
import { signMessage } from 'viem/accounts'
import { buildSignerEip191, buildSignerEs256k, sign } from '../../sign'
import { Alg, SignatureInput } from '../../types'
import { HEADER_PART, PAYLOAD_PART, PRIVATE_KEY_PEM, REQUEST } from './mock'
import { buildSignerEip191, buildSignerEs256k, signJwt } from '../../sign'
import { Alg, JWK, Payload } from '../../types'
import { HEADER_PART, PAYLOAD_PART, PRIVATE_KEY_PEM } from './mock'

describe('sign', () => {
it('should sign build & sign es256 JWT correcty', async () => {
const signingInput: SignatureInput = {
request: REQUEST,
privateKey: PRIVATE_KEY_PEM,
algorithm: Alg.ES256,
kid: 'test-kid',
iat: new Date('2024-12-11T00:00:00Z').getTime() / 1000,
exp: new Date('2024-12-12T00:00:00Z').getTime() / 1000
const payload: Payload = {
requestHash: '608abe908cffeab1fc33edde6b44586f9dacbc9c6fe6f0a13fa307237290ce5a',
sub: 'test-root-user-uid',
iss: 'https://armory.narval.xyz',
cnf: {
kty: 'EC',
crv: 'secp256k1',
alg: 'ES256K',
use: 'sig',
kid: '0x000c0d191308A336356BEe3813CC17F6868972C4',
x: '04a9f3bcf6505059597f6f27ad8c0f03a3bd7a1763520b0bfec204488b8e5840',
y: '7ee92845ab1c35a784b05fdfa567715c53bb2f29949b27714e3c1760e3709009a6'
}
}
const jwt = await sign(signingInput)
const parts = jwt.split('.')
expect(parts.length).toBe(3)
expect(parts[0]).toBe('eyJhbGciOiJFUzI1NiIsImtpZCI6InRlc3Qta2lkIn0')
expect(parts[1]).toBe(
'eyJyZXF1ZXN0SGFzaCI6IjY4NjMxYmIyMmIxNzFkMjk2YTUyMmJiNmMzMjQ4MDU1NTk3YmY2M2VhYzJiYTk1ZjFmZDAyYTQ4YWUxZWRmOGMiLCJpYXQiOjE3MzM4NzUyMDAsImV4cCI6MTczMzk2MTYwMH0'
)

const key = await importPKCS8(PRIVATE_KEY_PEM, Alg.ES256)
const jwk = await exportJWK(key)
const jwt = await signJwt(payload, { ...jwk, alg: Alg.ES256 } as JWK)

const verified = await jwtVerify(jwt, key)
expect(verified.payload).toEqual(payload)
})

it('should sign ES256k correctly', async () => {
const ENGINE_PRIVATE_KEY = '7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'
const pubkey = secp256k1.getPublicKey(Buffer.from(ENGINE_PRIVATE_KEY, 'hex'), false)
const message = [HEADER_PART, PAYLOAD_PART].join('.')
const signer = buildSignerEs256k(ENGINE_PRIVATE_KEY)
const signature = await signer(message)
const msgHash = sha256Hash(message)

const isVerified = secp256k1.verify(Buffer.from(signature, 'base64'), msgHash, pubkey)
expect(isVerified).toBe(true)

expect(signature).toBe('FFI6M5oFpbQqq0-xhe5DgPVHj4CKoVF4F3K3cg1MRY1COqWatQNsSn2MrqJ10BbGLe7i76KRMDj4biqnZkxwsw')
})
Expand Down
91 changes: 20 additions & 71 deletions packages/signature/src/lib/sign.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,42 @@
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, base64url, importPKCS8 } from 'jose'
import { isHex, signatureToHex, toBytes, toHex } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { JwtError } from './error'
import { hash } from './hash-request'
import { Alg, EcdsaSignature, Header, JWK, Payload, SignatureInput, SigningAlg } from './types'
import { SignJWT, base64url, importJWK } from 'jose'
import { signatureToHex, toBytes, toHex } from 'viem'
import { EcdsaSignature, Header, JWK, Payload, SigningAlg } from './types'
import { hexToBase64Url } from './utils'

const DEF_EXP_TIME = '2h'

const eoaKeys = async (signingInput: SignatureInput): Promise<string> => {
const { request, privateKey, algorithm, kid, iat, exp } = signingInput

if (!isHex(privateKey)) {
throw new JwtError({ message: 'Invalid private key', context: { privateKey } })
}

const account = privateKeyToAccount(privateKey)
const iatNumeric = iat
const expNumeric = exp
const header = { alg: algorithm, kid }
const payload = {
requestHash: hash(request),
iat: iatNumeric,
exp: expNumeric
}

const encodedHeader = base64url.encode(JSON.stringify(header))
const encodedPayload = base64url.encode(JSON.stringify(payload))

const messageToSign = `${encodedHeader}.${encodedPayload}`
const signature = await account.signMessage({ message: messageToSign })

const completeJWT = `${messageToSign}.${base64url.encode(toBytes(signature))}`
return completeJWT
}

/**
* Signs a request using the provided private key and algorithm.
*
* @param {SignatureInput} signingInput - The input required to sign a request.
* @returns {Promise<string>} A promise that resolves with the signed JWT.
*/
export async function sign(signingInput: SignatureInput): Promise<string> {
const { request, privateKey: pk, algorithm, kid, iat, exp } = signingInput

try {
if (isHex(pk) && algorithm === Alg.ES256K) {
return eoaKeys(signingInput)
}
const privateKey = await importPKCS8(pk, algorithm)
const requestHash = hash(request)

const jwt = await new SignJWT({ requestHash })
.setProtectedHeader({ alg: algorithm, kid })
.setIssuedAt(iat)
.setExpirationTime(exp || DEF_EXP_TIME)
.sign(privateKey)
return jwt
} catch (error) {
console.log(error);
throw new JwtError({ message: 'Failed to sign request.', context: { error } })
}
}

// WIP to replace `sign`
export async function signJwt(
payload: Payload,
jwk: JWK,
opts: { alg?: SigningAlg },
signer: (payload: string) => Promise<string>
opts: { alg?: SigningAlg } = {},
signer?: (payload: string) => Promise<string>
): Promise<string> {
const header: Header = {
kid: jwk.kid,
alg: opts.alg || jwk.alg, // TODO: add separate type for `ES256k-KECCAK`
typ: 'JWT'
}

const encodedHeader = base64url.encode(JSON.stringify(header))
const encodedPayload = base64url.encode(JSON.stringify(payload))
if (header.alg === SigningAlg.ES256K || header.alg === SigningAlg.EIP191) {
if (!signer) {
throw new Error('Missing signer')
}
const encodedHeader = base64url.encode(JSON.stringify(header))
const encodedPayload = base64url.encode(JSON.stringify(payload))

const messageToSign = `${encodedHeader}.${encodedPayload}`

const messageToSign = `${encodedHeader}.${encodedPayload}`
const signature = await signer(messageToSign)

const signature = await signer(messageToSign)
const completeJWT = `${messageToSign}.${signature}`
return completeJWT
}

const completeJWT = `${messageToSign}.${signature}`
return completeJWT
const privateKey = await importJWK(jwk)
const jwt = await new SignJWT(payload).setProtectedHeader(header).sign(privateKey)
return jwt
}

export const signSecp256k1 = (hash: Uint8Array, privateKey: string, isEth?: boolean): EcdsaSignature => {
Expand Down
4 changes: 2 additions & 2 deletions packages/signature/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ export type Header = {
*/
export type Payload = {
sub: string
iat: number
exp: number
iat?: number
exp?: number
iss: string
aud?: string
jti?: string
Expand Down
8 changes: 4 additions & 4 deletions packages/signature/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ export const publicKeyToJwk = (publicKey: Hex, keyId?: string): JWK => {
kty: KeyTypes.EC,
crv: Curves.SECP256K1,
alg: Alg.ES256K,
use: Use.SIG,
// use: Use.SIG,
kid: keyId || publicKeyToAddress(publicKey), // add an opaque prefix that indicates the key type
x: publicKey.slice(2, 66),
y: publicKey.slice(66)
x: hexToBase64Url(`0x${publicKey.slice(2, 66)}`),
y: hexToBase64Url(`0x${publicKey.slice(66)}`)
}
}

Expand All @@ -52,7 +52,7 @@ export const privateKeyToJwk = (privateKey: Hex, keyId?: string): JWK => {
const publicJwk = publicKeyToJwk(publicKey, keyId)
return {
...publicJwk,
d: privateKey.slice(2)
d: hexToBase64Url(`0x${privateKey.slice(2)}`)
}
}

Expand Down

0 comments on commit 56157b8

Please sign in to comment.