diff --git a/packages/signature/src/index.ts b/packages/signature/src/index.ts index 8e79d8760..9733b6c0c 100644 --- a/packages/signature/src/index.ts +++ b/packages/signature/src/index.ts @@ -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' diff --git a/packages/signature/src/lib/__test__/unit/eoa-keys.spec.ts b/packages/signature/src/lib/__test__/unit/eoa-keys.spec.ts deleted file mode 100644 index d3c1a29f0..000000000 --- a/packages/signature/src/lib/__test__/unit/eoa-keys.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' -import { sign } from '../../sign' -import { Alg, SignatureInput, VerificationInput } from '../../types' -import { verify } from '../../verify' -import { DECODED_TOKEN, EXP, IAT, KID, REQUEST } from './mock' - -describe('flow with viem keypairs', () => { - it('should sign and verify a request successfully', async () => { - const viemPkAlg = Alg.ES256K - const pk = generatePrivateKey() - const { publicKey: viemPk } = privateKeyToAccount(pk) - const expected = { - header: { - ...DECODED_TOKEN.header, - alg: viemPkAlg - }, - payload: DECODED_TOKEN.payload, - signature: expect.any(String) - } - const signingInput: SignatureInput = { - request: REQUEST, - privateKey: pk, - algorithm: viemPkAlg, - kid: KID, - iat: IAT, - exp: EXP - } - const jwt = await sign(signingInput) - const verificationInput: VerificationInput = { - jwt, - publicKey: viemPk - } - const verifiedJwt = await verify(verificationInput) - expect(verifiedJwt).toEqual(expected) - }) -}) diff --git a/packages/signature/src/lib/__test__/unit/sign.spec.ts b/packages/signature/src/lib/__test__/unit/sign.spec.ts index 629987d2a..6c70af95b 100644 --- a/packages/signature/src/lib/__test__/unit/sign.spec.ts +++ b/packages/signature/src/lib/__test__/unit/sign.spec.ts @@ -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') }) diff --git a/packages/signature/src/lib/sign.ts b/packages/signature/src/lib/sign.ts index a572c2016..8b9c71b48 100644 --- a/packages/signature/src/lib/sign.ts +++ b/packages/signature/src/lib/sign.ts @@ -1,77 +1,17 @@ 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 => { - 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} A promise that resolves with the signed JWT. - */ -export async function sign(signingInput: SignatureInput): Promise { - 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 + opts: { alg?: SigningAlg } = {}, + signer?: (payload: string) => Promise ): Promise { const header: Header = { kid: jwk.kid, @@ -79,15 +19,24 @@ export async function signJwt( 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 => { diff --git a/packages/signature/src/lib/types.ts b/packages/signature/src/lib/types.ts index be557e32f..ea7996b12 100644 --- a/packages/signature/src/lib/types.ts +++ b/packages/signature/src/lib/types.ts @@ -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 diff --git a/packages/signature/src/lib/utils.ts b/packages/signature/src/lib/utils.ts index dc130b1a3..eed607bbf 100644 --- a/packages/signature/src/lib/utils.ts +++ b/packages/signature/src/lib/utils.ts @@ -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)}`) } } @@ -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)}`) } }