From f581e6b16cb11f8f881e6bdfa13e12dfc83f7008 Mon Sep 17 00:00:00 2001 From: Ptroger <44851272+Ptroger@users.noreply.github.com> Date: Fri, 1 Mar 2024 15:35:15 -0400 Subject: [PATCH] Simplify verify (#143) * renamed lib and divided tests * moved test to lib dir * removed duplication, signs viem keys * removed alg import from shared * lint * alg import * another alg import * signing header + payload, improved tests * fixed imports * removed useless config change * import in tests * Alg import in integration test * remove unecessary verifying step and args * renamed encoded token from rawToken to jwt * rename hash-request to hash * removed console.log --- .../historical-transfer-feed.service.ts | 4 +- .../core/service/price-feed.service.ts | 4 +- .../__test__/unit/cluster.service.spec.ts | 8 +-- .../core/service/cluster.service.ts | 6 +- apps/policy-engine/src/app/app.service.ts | 4 +- .../app/persistence/repository/mock_data.ts | 4 +- packages/signature/README.md | 63 ++++++++++++++++++- packages/signature/src/index.ts | 2 +- .../src/lib/__test__/unit/eoa-keys.spec.ts | 6 +- .../__test__/unit/hash-request.util.spec.ts | 8 +-- .../signature/src/lib/__test__/unit/mock.ts | 4 +- .../src/lib/__test__/unit/verify.spec.ts | 8 +-- packages/signature/src/lib/hash-request.ts | 2 +- packages/signature/src/lib/sign.ts | 6 +- packages/signature/src/lib/typeguards.ts | 3 - packages/signature/src/lib/types.ts | 5 +- packages/signature/src/lib/verify.ts | 45 +++++-------- 17 files changed, 108 insertions(+), 74 deletions(-) diff --git a/apps/armory/src/data-feed/core/service/historical-transfer-feed.service.ts b/apps/armory/src/data-feed/core/service/historical-transfer-feed.service.ts index db7379603..ecb5ba125 100644 --- a/apps/armory/src/data-feed/core/service/historical-transfer-feed.service.ts +++ b/apps/armory/src/data-feed/core/service/historical-transfer-feed.service.ts @@ -1,5 +1,5 @@ import { Feed, HistoricalTransfer, Signature } from '@narval/policy-engine-shared' -import { Alg, hashRequest } from '@narval/signature' +import { Alg, hash } from '@narval/signature' import { Injectable } from '@nestjs/common' import { ConfigService } from '@nestjs/config' import { mapValues, omit } from 'lodash/fp' @@ -30,7 +30,7 @@ export class HistoricalTransferFeedService implements DataFeed { const account = privateKeyToAccount(this.getPrivateKey()) const sig = await account.signMessage({ - message: hashRequest(data) + message: hash(data) }) return { diff --git a/apps/armory/src/data-feed/core/service/price-feed.service.ts b/apps/armory/src/data-feed/core/service/price-feed.service.ts index efdf29d50..71a79bd5f 100644 --- a/apps/armory/src/data-feed/core/service/price-feed.service.ts +++ b/apps/armory/src/data-feed/core/service/price-feed.service.ts @@ -1,5 +1,5 @@ import { Action, AssetId, Feed, Signature } from '@narval/policy-engine-shared' -import { Alg, hashRequest } from '@narval/signature' +import { Alg, hash } from '@narval/signature' import { InputType, Intents, safeDecode } from '@narval/transaction-request-intent' import { Injectable } from '@nestjs/common' import { ConfigService } from '@nestjs/config' @@ -29,7 +29,7 @@ export class PriceFeedService implements DataFeed { async sign(data: Prices): Promise { const account = privateKeyToAccount(this.getPrivateKey()) const sig = await account.signMessage({ - message: hashRequest(data) + message: hash(data) }) return { diff --git a/apps/armory/src/orchestration/core/service/__test__/unit/cluster.service.spec.ts b/apps/armory/src/orchestration/core/service/__test__/unit/cluster.service.spec.ts index 38a03553e..24cb5290b 100644 --- a/apps/armory/src/orchestration/core/service/__test__/unit/cluster.service.spec.ts +++ b/apps/armory/src/orchestration/core/service/__test__/unit/cluster.service.spec.ts @@ -1,5 +1,5 @@ import { Decision, EvaluationResponse, Feed, Prices } from '@narval/policy-engine-shared' -import { Alg, hashRequest } from '@narval/signature' +import { Alg, hash } from '@narval/signature' import { Test } from '@nestjs/testing' import { MockProxy, mock } from 'jest-mock-extended' import { PrivateKeyAccount, generatePrivateKey, privateKeyToAccount } from 'viem/accounts' @@ -99,8 +99,8 @@ describe(ClusterService.name, () => { authzRequest: AuthorizationRequest, partial?: Partial ): Promise => { - const hash = hashRequest(authzRequest.request) - const signature = await nodeAccount.signMessage({ message: hash }) + const requestHash = hash(authzRequest.request) + const signature = await nodeAccount.signMessage({ message: requestHash }) return { decision: Decision.PERMIT, @@ -168,7 +168,7 @@ describe(ClusterService.name, () => { it('throws when node attestation is invalid', async () => { const permit = await generateEvaluationResponse(nodeAccount, authzRequest) const signature = await nodeAccount.signMessage({ - message: hashRequest({ notTheOriginalRequest: true }) + message: hash({ notTheOriginalRequest: true }) }) authzApplicationClientMock.evaluation.mockResolvedValue({ diff --git a/apps/armory/src/orchestration/core/service/cluster.service.ts b/apps/armory/src/orchestration/core/service/cluster.service.ts index cdcc2693e..acb64c1d3 100644 --- a/apps/armory/src/orchestration/core/service/cluster.service.ts +++ b/apps/armory/src/orchestration/core/service/cluster.service.ts @@ -1,5 +1,5 @@ import { Decision, EvaluationRequest, EvaluationResponse } from '@narval/policy-engine-shared' -import { hashRequest } from '@narval/signature' +import { hash } from '@narval/signature' import { Injectable, Logger } from '@nestjs/common' import { zip } from 'lodash/fp' import { ClusterNotFoundException } from '../../core/exception/cluster-not-found.exception' @@ -120,10 +120,10 @@ export class ClusterService { } private async recoverPubKey(response: EvaluationResponse) { - const hash = hashRequest(response.request) as `0x${string}` + const requestHash = hash(response.request) as `0x${string}` return recoverMessageAddress({ - message: hash, + message: requestHash, signature: response.attestation?.sig as `0x${string}` }) } diff --git a/apps/policy-engine/src/app/app.service.ts b/apps/policy-engine/src/app/app.service.ts index f7c2998ba..942b66046 100644 --- a/apps/policy-engine/src/app/app.service.ts +++ b/apps/policy-engine/src/app/app.service.ts @@ -8,7 +8,7 @@ import { Request, Signature } from '@narval/policy-engine-shared' -import { Alg, hashRequest } from '@narval/signature' +import { Alg, hash } from '@narval/signature' import { safeDecode } from '@narval/transaction-request-intent' import { BadRequestException, @@ -159,7 +159,7 @@ export class AppService { }: EvaluationRequest): Promise { // Pre-Process // verify the signatures of the Principal and any Approvals - const verificationMessage = hashRequest(request) + const verificationMessage = hash(request) const principalCredential = await this.#verifySignature(authentication, verificationMessage) if (!principalCredential) throw new Error(`Could not find principal`) diff --git a/apps/policy-engine/src/app/persistence/repository/mock_data.ts b/apps/policy-engine/src/app/persistence/repository/mock_data.ts index 2d01051f1..9a626a3e5 100644 --- a/apps/policy-engine/src/app/persistence/repository/mock_data.ts +++ b/apps/policy-engine/src/app/persistence/repository/mock_data.ts @@ -1,5 +1,5 @@ import { Action, EvaluationRequest, FIXTURE, Request, TransactionRequest } from '@narval/policy-engine-shared' -import { Alg, hashRequest } from '@narval/signature' +import { Alg, hash } from '@narval/signature' import { toHex } from 'viem' export const ONE_ETH = BigInt('1000000000000000000') @@ -22,7 +22,7 @@ export const generateInboundRequest = async (): Promise => { resourceId: FIXTURE.WALLET.Engineering.id } - const message = hashRequest(request) + const message = hash(request) const aliceSignature = await FIXTURE.ACCOUNT.Alice.signMessage({ message }) const bobSignature = await FIXTURE.ACCOUNT.Bob.signMessage({ message }) diff --git a/packages/signature/README.md b/packages/signature/README.md index 7537ccba5..eb97c7b42 100644 --- a/packages/signature/README.md +++ b/packages/signature/README.md @@ -1,6 +1,65 @@ -# Signature +# Signature Library -Lib to decode a signature +The Signature library is designed to decode and verify JWTs, providing robust support for various cryptographic operations, including signature verification and JWT decoding without signature validation. It's built with flexibility in mind, supporting Ethereum's EOA signatures and traditional JWT signing algorithms. + +## Features + +- Decode JWTs without verifying their signature. +- Verify JWT signatures with support for Ethereum EOA and traditional algorithms. +- Generate SHA256 hashes for arbitrary objects, with support for BigInt primitives. +- Sign requests using private keys and predefined algorithms. + +## Usage + +### Signing + +``` +import { Alg, sign } from '@narval/signature'; + +const signingInput = { + request: { + message: 'stuff' + }, // this can be anything + privateKey: 'your-private-key-pem-or-hex', + algorithm: Alg.ES256K, // Example algorithm + kid: 'your-key-id', + iat: new Date(), + exp: new Date(new Date().getTime() + 60 * 60 * 1000), // Expires in 1 hour +}; + +sign(signingInput) + .then(signedJwt => console.log(signedJwt)) + .catch(error => console.error(error)); +``` + +### Decoding + +``` +import { decode } from '@narval/signature'; + +const jwt = 'your.jwt.string'; +try { + const decodedJwt = decode(jwt); + console.log(decodedJwt); +} catch (error) { + console.error(error); +} +``` + +### Verifying + +``` +import { sign } from '@narval/signature'; + +const verificationInput = { + rawToken: 'your.jwt.string', + publicKey: 'your-public-key-in-pem-or-hex', // !! PubKey NOT eoa address +}; + +verify(verificationInput) + .then(decodedJwt => console.log(decodedJwt)) + .catch(error => console.error(error)); +``` ## Testing diff --git a/packages/signature/src/index.ts b/packages/signature/src/index.ts index d1fd60719..252e50bb7 100644 --- a/packages/signature/src/index.ts +++ b/packages/signature/src/index.ts @@ -1,5 +1,5 @@ export { decode } from './lib/decode' -export { hashRequest } from './lib/hash-request' +export { hash } from './lib/hash-request' export { sign } from './lib/sign' export * from './lib/types' export { verify } from './lib/verify' diff --git a/packages/signature/src/lib/__test__/unit/eoa-keys.spec.ts b/packages/signature/src/lib/__test__/unit/eoa-keys.spec.ts index d14fff555..d3c1a29f0 100644 --- a/packages/signature/src/lib/__test__/unit/eoa-keys.spec.ts +++ b/packages/signature/src/lib/__test__/unit/eoa-keys.spec.ts @@ -27,10 +27,8 @@ describe('flow with viem keypairs', () => { } const jwt = await sign(signingInput) const verificationInput: VerificationInput = { - request: REQUEST, - rawToken: jwt, - publicKey: viemPk, - algorithm: viemPkAlg + jwt, + publicKey: viemPk } const verifiedJwt = await verify(verificationInput) expect(verifiedJwt).toEqual(expected) diff --git a/packages/signature/src/lib/__test__/unit/hash-request.util.spec.ts b/packages/signature/src/lib/__test__/unit/hash-request.util.spec.ts index 444730ac9..8ab64e755 100644 --- a/packages/signature/src/lib/__test__/unit/hash-request.util.spec.ts +++ b/packages/signature/src/lib/__test__/unit/hash-request.util.spec.ts @@ -1,9 +1,9 @@ -import { hashRequest } from '../../hash-request' +import { hash } from '../../hash-request' describe('hashRequest', () => { it('hashes the given object', () => { expect( - hashRequest({ + hash({ a: 'a', b: 1, c: false @@ -12,7 +12,7 @@ describe('hashRequest', () => { }) it('hashes the given array', () => { - expect(hashRequest(['a', 1, false])).toEqual('cdd23dea0598c5ffc66b6a53f9dc7448a87b47454f209caa310e21da91754173') + expect(hash(['a', 1, false])).toEqual('cdd23dea0598c5ffc66b6a53f9dc7448a87b47454f209caa310e21da91754173') }) it('hashes two objects deterministically', () => { @@ -35,6 +35,6 @@ describe('hashRequest', () => { a: 'a' } - expect(hashRequest(a)).toEqual(hashRequest(b)) + expect(hash(a)).toEqual(hash(b)) }) }) diff --git a/packages/signature/src/lib/__test__/unit/mock.ts b/packages/signature/src/lib/__test__/unit/mock.ts index 58ffce5fa..49d2056c1 100644 --- a/packages/signature/src/lib/__test__/unit/mock.ts +++ b/packages/signature/src/lib/__test__/unit/mock.ts @@ -1,4 +1,4 @@ -import { hashRequest } from '../../hash-request' +import { hash } from '../../hash-request' import { Alg } from '../../types' export const ALGORITHM = Alg.ES256 @@ -27,7 +27,7 @@ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgUDs3iCP93sknEZ+c DcdsS+UdaUgKK0XVajrBWZbQYWehRANCAAT4HK9PrjSP2z5sFVzU+NOU6Jlr2jHK 56woxq1wNvSAh4JZtYO4SLJWaSB8YUD3caXqgH3gSIHYQWm+EY2h4nAc -----END PRIVATE KEY-----` -export const HASH = hashRequest(REQUEST) +export const HASH = hash(REQUEST) export const SIGNED_TOKEN = 'eyJhbGciOiJFUzI1NiIsImtpZCI6InRlc3Qta2lkIn0.eyJyZXF1ZXN0SGFzaCI6IjY4NjMxYmIyMmIxNzFkMjk2YTUyMmJiNmMzMjQ4MDU1NTk3YmY2M2VhYzJiYTk1ZjFmZDAyYTQ4YWUxZWRmOGMiLCJpYXQiOjE3MzM4NzUyMDAsImV4cCI6MTczMzk2MTYwMH0.0D_W3jtdBCAIht39FvPTtC6o9TywEQ_i1TL4BTDQIdndL1X2eoFawoczhWqhEQeP3MUs3XnLeBhtfbf25U3EsQ' export const HEADER_PART = 'eyJhbGciOiJFUzI1NiIsImtpZCI6InRlc3Qta2lkIn0' diff --git a/packages/signature/src/lib/__test__/unit/verify.spec.ts b/packages/signature/src/lib/__test__/unit/verify.spec.ts index 0ce0da8df..fddef9ab4 100644 --- a/packages/signature/src/lib/__test__/unit/verify.spec.ts +++ b/packages/signature/src/lib/__test__/unit/verify.spec.ts @@ -1,14 +1,12 @@ import { VerificationInput } from '../../types' import { verify } from '../../verify' -import { ALGORITHM, DECODED_TOKEN, PUBLIC_KEY_PEM, REQUEST, SIGNED_TOKEN } from './mock' +import { DECODED_TOKEN, PUBLIC_KEY_PEM, SIGNED_TOKEN } from './mock' describe('verify', () => { it('verifies a request successfully', async () => { const verificationInput: VerificationInput = { - request: REQUEST, - rawToken: SIGNED_TOKEN, - publicKey: PUBLIC_KEY_PEM, - algorithm: ALGORITHM + jwt: SIGNED_TOKEN, + publicKey: PUBLIC_KEY_PEM } const jwt = await verify(verificationInput) expect(jwt).toEqual(DECODED_TOKEN) diff --git a/packages/signature/src/lib/hash-request.ts b/packages/signature/src/lib/hash-request.ts index b3f30ccf7..f4839df89 100644 --- a/packages/signature/src/lib/hash-request.ts +++ b/packages/signature/src/lib/hash-request.ts @@ -28,7 +28,7 @@ const sort = (value: unknown): unknown => { * @param value an object * @returns object's hash */ -export const hashRequest = (value: unknown): string => { +export const hash = (value: unknown): string => { return createHash('sha256') .update(stringify(sort(value))) .digest('hex') diff --git a/packages/signature/src/lib/sign.ts b/packages/signature/src/lib/sign.ts index eebda40c0..a27ca3bf1 100644 --- a/packages/signature/src/lib/sign.ts +++ b/packages/signature/src/lib/sign.ts @@ -2,7 +2,7 @@ import { SignJWT, base64url, importPKCS8 } from 'jose' import { isHex, toBytes } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { JwtError } from './error' -import { hashRequest } from './hash-request' +import { hash } from './hash-request' import { Alg, SignatureInput } from './types' const DEF_EXP_TIME = '2h' @@ -20,7 +20,7 @@ const eoaKeys = async (signingInput: SignatureInput): Promise => { const expNumeric = exp ? Math.floor(exp.getTime() / 1000) : now + 2 * 60 * 60 const header = { alg: algorithm, kid } const payload = { - requestHash: hashRequest(request), + requestHash: hash(request), iat: iatNumeric, exp: expNumeric } @@ -49,7 +49,7 @@ export async function sign(signingInput: SignatureInput): Promise { return eoaKeys(signingInput) } const privateKey = await importPKCS8(pk, algorithm) - const requestHash = hashRequest(request) + const requestHash = hash(request) const jwt = await new SignJWT({ requestHash }) .setProtectedHeader({ alg: algorithm, kid }) diff --git a/packages/signature/src/lib/typeguards.ts b/packages/signature/src/lib/typeguards.ts index cba89ae81..edf482665 100644 --- a/packages/signature/src/lib/typeguards.ts +++ b/packages/signature/src/lib/typeguards.ts @@ -25,15 +25,12 @@ function isDate(date: unknown): date is Date { } if (typeof date === 'string') { const parsed = Date.parse(date) - console.log('### parsed', parsed) return !isNaN(parsed) } if (typeof date === 'number') { const parsed = new Date(date) - console.log('### parsed', parsed) return !isNaN(parsed.getTime()) } - console.log('### date', date) return false } diff --git a/packages/signature/src/lib/types.ts b/packages/signature/src/lib/types.ts index 3507fbde6..6320e43bb 100644 --- a/packages/signature/src/lib/types.ts +++ b/packages/signature/src/lib/types.ts @@ -78,12 +78,9 @@ export type SignatureInput = { * Defines the input required to verify a JWT. * * @param {string} jwt - The JWT to be verified. - * @param {Request} request - The content of the request to be verified. * @param {string} publicKey - The public key that corresponds to the private key used for signing. */ export type VerificationInput = { - rawToken: string - request: unknown + jwt: string publicKey: string - algorithm: Alg } diff --git a/packages/signature/src/lib/verify.ts b/packages/signature/src/lib/verify.ts index 826f428ce..7f8353b52 100644 --- a/packages/signature/src/lib/verify.ts +++ b/packages/signature/src/lib/verify.ts @@ -3,8 +3,7 @@ import { Hex, recoverMessageAddress } from 'viem' import { isHex, publicKeyToAddress, toHex } from 'viem/utils' import { decode } from './decode' import { JwtError } from './error' -import { hashRequest } from './hash-request' -import { Alg, Jwt, Payload, VerificationInput } from './types' +import { Jwt, Payload, VerificationInput } from './types' const checkTokenValidity = (token: string): boolean => { const parts = token.split('.') @@ -12,14 +11,14 @@ const checkTokenValidity = (token: string): boolean => { } const eoaKeys = async (verificationInput: VerificationInput): Promise => { - const { rawToken, publicKey } = verificationInput + const { jwt, publicKey } = verificationInput - if (!checkTokenValidity(rawToken)) { - throw new JwtError({ message: 'Invalid token', context: { rawToken } }) + if (!checkTokenValidity(jwt)) { + throw new JwtError({ message: 'Invalid token', context: { rawToken: jwt } }) } try { - const parts = rawToken.split('.') + const parts = jwt.split('.') const recoveredAddress = await recoverMessageAddress({ message: `${parts[0]}.${parts[1]}`, @@ -28,17 +27,17 @@ const eoaKeys = async (verificationInput: VerificationInput): Promise => { const pubKeyAddress = publicKeyToAddress(publicKey as Hex) if (pubKeyAddress !== recoveredAddress) { - throw new JwtError({ message: 'Invalid signature', context: { rawToken } }) + throw new JwtError({ message: 'Invalid signature', context: { rawToken: jwt } }) } - const token = decode(rawToken) + const token = decode(jwt) const now = new Date() if (token.payload.exp && token.payload.exp < now) { - throw new JwtError({ message: 'Token has expired', context: { rawToken } }) + throw new JwtError({ message: 'Token has expired', context: { rawToken: jwt } }) } - return decode(rawToken) + return decode(jwt) } catch (e) { throw new JwtError({ message: 'error verifying eoa signature', context: { e } }) } @@ -52,30 +51,16 @@ const eoaKeys = async (verificationInput: VerificationInput): Promise => { * @throws {Error} If the JWT is invalid or the request hash does not match the request. */ export async function verify(input: VerificationInput): Promise { - const { rawToken, request, algorithm, publicKey } = input + const { jwt, publicKey } = input try { - if (isHex(publicKey) && algorithm === Alg.ES256K) { + if (isHex(publicKey)) { return eoaKeys(input) } - const publicKeyObj = await importSPKI(publicKey, algorithm) + const decodedJwt = decode(jwt) + const publicKeyObj = await importSPKI(publicKey, decodedJwt.header.alg) + await jwtVerify(jwt, publicKeyObj) - const { payload } = await jwtVerify(rawToken, publicKeyObj, { - algorithms: [algorithm] - }) - - const requestHash = hashRequest(request) - if (payload.requestHash !== requestHash) { - throw new JwtError({ - message: 'Request hash does not match the request', - context: { - payload, - requestHash, - input - } - }) - } - const jwt = decode(rawToken) - return jwt + return decodedJwt } catch (error) { throw new JwtError({ message: 'Malformed token', context: { input, error } }) }