Skip to content

Commit

Permalink
Merge pull request #165 from narval-xyz/chore/signature-to-jwt
Browse files Browse the repository at this point in the history
Converting EvaluationRequest signature type to JWTs
  • Loading branch information
mattschoch authored Mar 14, 2024
2 parents 1c57b8e + 294dc75 commit 02e7946
Show file tree
Hide file tree
Showing 42 changed files with 315 additions and 369 deletions.
6 changes: 6 additions & 0 deletions apps/armory/.lintstagedrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
'*.{ts,tsx}': (filenames) => [
`eslint --no-error-on-unmatched-pattern ${filenames.join(' ')}; echo "ESLint completed with exit code $?"`,
`prettier --write ${filenames.join(' ')}`
]
}
24 changes: 4 additions & 20 deletions apps/armory/src/__test__/fixture/authorization-request.fixture.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { Decision, Signature, TransactionRequest } from '@narval/policy-engine-shared'
import { Decision, TransactionRequest } from '@narval/policy-engine-shared'
import { AuthorizationRequestStatus } from '@prisma/client/armory'
import { z } from 'zod'
import { Fixture } from 'zod-fixture'
import { Approval, AuthorizationRequest, SignTransaction } from '../../orchestration/core/type/domain.type'
import { AuthorizationRequest, SignTransaction } from '../../orchestration/core/type/domain.type'
import { readRequestSchema } from '../../orchestration/persistence/schema/request.schema'
import { readSignTransactionSchema } from '../../orchestration/persistence/schema/sign-transaction.schema'
import { signatureSchema } from '../../orchestration/persistence/schema/signature.schema'
import { readTransactionRequestSchema } from '../../orchestration/persistence/schema/transaction-request.schema'
import { addressGenerator, chainIdGenerator, hexGenerator } from './shared.fixture'

const approvalSchema = signatureSchema.extend({
id: z.string().uuid(),
createdAt: z.date()
})

const evaluationSchema = z.object({
id: z.string().uuid(),
decision: z.nativeEnum(Decision),
Expand All @@ -26,8 +20,8 @@ const authorizationRequestSchema = z.object({
orgId: z.string().uuid(),
status: z.nativeEnum(AuthorizationRequestStatus),
request: readRequestSchema,
authentication: signatureSchema,
approvals: z.array(approvalSchema),
authentication: z.string(),
approvals: z.array(z.string()),
evaluations: z.array(evaluationSchema),
idempotencyKey: z.string().nullish(),
createdAt: z.date(),
Expand Down Expand Up @@ -66,13 +60,3 @@ export const generateAuthorizationRequest = (partial?: Partial<AuthorizationRequ
...partial
}
}

export const generateApproval = (partial?: Partial<Approval>): Approval => ({
...new Fixture().fromSchema(approvalSchema),
...partial
})

export const generateSignature = (partial?: Partial<Signature>): Signature => ({
...new Fixture().fromSchema(approvalSchema),
...partial
})
3 changes: 1 addition & 2 deletions apps/armory/src/__test__/fixture/feed.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import { times } from 'lodash/fp'
import { z } from 'zod'
import { Fixture } from 'zod-fixture'
import { HistoricalTransferFeedService } from '../../data-feed/core/service/historical-transfer-feed.service'
import { signatureSchema } from '../../orchestration/persistence/schema/signature.schema'
import { hexGenerator } from './shared.fixture'
import { generateTransfer } from './transfer-tracking.fixture'

const feedSchema = z.object({
source: z.string().min(1).max(42),
sig: signatureSchema.nullable()
sig: z.string().nullable()
})

export const generateHistoricalTransfers = (): HistoricalTransfer[] =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { MockProxy, mock, mockDeep } from 'jest-mock-extended'
import {
generateAuthorizationRequest,
generateSignTransactionRequest,
generateSignature,
generateTransactionRequest
} from '../../../../../__test__/fixture/authorization-request.fixture'
import { generateHistoricalTransfers } from '../../../../../__test__/fixture/feed.fixture'
Expand All @@ -17,6 +16,9 @@ import { HistoricalTransferFeedService } from '../../historical-transfer-feed.se
import { PriceFeedService } from '../../price-feed.service'

describe(FeedService.name, () => {
const jwt =
'eyJraWQiOiIweDJjNDg5NTIxNTk3M0NiQmQ3NzhDMzJjNDU2QzA3NGI5OWRhRjhCZjEiLCJhbGciOiJFSVAxOTEiLCJ0eXAiOiJKV1QifQ.eyJyZXF1ZXN0SGFzaCI6IjYwOGFiZTkwOGNmZmVhYjFmYzMzZWRkZTZiNDQ1ODZmOWRhY2JjOWM2ZmU2ZjBhMTNmYTMwNzIzNzI5MGNlNWEiLCJzdWIiOiJ0ZXN0LXJvb3QtdXNlci11aWQiLCJpc3MiOiJodHRwczovL2FybW9yeS5uYXJ2YWwueHl6IiwiY25mIjp7Imt0eSI6IkVDIiwiY3J2Ijoic2VjcDI1NmsxIiwiYWxnIjoiRVMyNTZLIiwidXNlIjoic2lnIiwia2lkIjoiMHgwMDBjMGQxOTEzMDhBMzM2MzU2QkVlMzgxM0NDMTdGNjg2ODk3MkM0IiwieCI6IjA0YTlmM2JjZjY1MDUwNTk1OTdmNmYyN2FkOGMwZjAzYTNiZDdhMTc2MzUyMGIwYmZlYzIwNDQ4OGI4ZTU4NDAiLCJ5IjoiN2VlOTI4NDVhYjFjMzVhNzg0YjA1ZmRmYTU2NzcxNWM1M2JiMmYyOTk0OWIyNzcxNGUzYzE3NjBlMzcwOTAwOWE2In19.gFDywYsxY2-uT6H6hyxk51CtJhAZpI8WtcvoXHltiWsoBVOot1zMo3nHAhkWlYRmD3RuLtmOYzi6TwTUM8mFyBs'

let module: TestingModule
let service: FeedService
let prismaServiceMock: MockProxy<PrismaService>
Expand All @@ -33,13 +35,13 @@ describe(FeedService.name, () => {

const historicalTransferFeed: Feed<HistoricalTransfer[]> = {
source: HistoricalTransferFeedService.SOURCE_ID,
sig: generateSignature(),
sig: jwt,
data: generateHistoricalTransfers()
}

const priceFeed: Feed<Prices> = {
source: PriceFeedService.SOURCE_ID,
sig: generateSignature(),
sig: jwt,
data: generatePrices()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Alg } from '@narval/signature'
import { ConfigModule } from '@nestjs/config'
import { Test, TestingModule } from '@nestjs/testing'
import { MockProxy, mock } from 'jest-mock-extended'
Expand Down Expand Up @@ -70,12 +69,7 @@ describe(HistoricalTransferFeedService.name, () => {
expect(feed).toMatchObject({
data: HistoricalTransferFeedService.build(transfers),
source: HistoricalTransferFeedService.SOURCE_ID,
sig: {
alg: Alg.ES256K,
pubKey:
'0x041a2a9746efacc23443530a75092ad75c6cd5dd10d2ccc1d9c866acf9545974bcacc6b755c3c241d6a35b9b27b00cd2df8f46525a751d6872360c3be3015bb563',
sig: expect.any(String)
}
sig: expect.any(String)
})
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Prices } from '@narval/policy-engine-shared'
import { Alg } from '@narval/signature'
import { ConfigModule } from '@nestjs/config'
import { Test, TestingModule } from '@nestjs/testing'
import { MockProxy, mock } from 'jest-mock-extended'
Expand Down Expand Up @@ -73,12 +72,7 @@ describe(PriceFeedService.name, () => {
expect(feed).toMatchObject({
data: prices,
source: PriceFeedService.SOURCE_ID,
sig: {
alg: Alg.ES256K,
pubKey:
'0x04583c9cf37f209ca3afbdb43f83dfaed0e758b89545bfa8e297d3d727fc3ed9c6f16607bc859f2304704329bf19c843acce511d0639bbd8abb1fe70e7dd05f8f5',
sig: expect.any(String)
}
sig: expect.any(String)
})
})

Expand Down
4 changes: 1 addition & 3 deletions apps/armory/src/data-feed/core/service/feed.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ export class FeedService {
orgId,
requestId,
source: feed.source,
sig: feed.sig?.sig,
alg: feed.sig?.alg,
pubKey: feed.sig?.pubKey,
sig: feed.sig,
data: this.getPersistableJson(feed.data),
createdAt: new Date()
}))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Feed, HistoricalTransfer, Signature } from '@narval/policy-engine-shared'
import { Alg, hash } from '@narval/signature'
import { Feed, HistoricalTransfer, JwtString } from '@narval/policy-engine-shared'
import { Payload, SigningAlg, hash, hexToBase64Url, privateKeyToJwk, signJwt } from '@narval/signature'
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { mapValues, omit } from 'lodash/fp'
Expand Down Expand Up @@ -27,17 +27,26 @@ export class HistoricalTransferFeedService implements DataFeed<HistoricalTransfe
return privateKeyToAccount(this.getPrivateKey()).publicKey
}

async sign(data: HistoricalTransfer[]): Promise<Signature> {
async sign(data: HistoricalTransfer[]): Promise<JwtString> {
const account = privateKeyToAccount(this.getPrivateKey())
const sig = await account.signMessage({
message: hash(data)
})

return {
alg: Alg.ES256K,
pubKey: account.publicKey,
sig
const jwtSigner = async (msg: string) => {
const jwtSig = await account.signMessage({ message: msg })

return hexToBase64Url(jwtSig)
}

const now = Math.floor(Date.now() / 1000)
const jwk = privateKeyToJwk(this.getPrivateKey())
const payload: Payload = {
data: hash(data),
sub: account.address,
iss: 'https://armory.narval.xyz',
iat: now
}
const jwt = await signJwt(payload, jwk, { alg: SigningAlg.EIP191 }, jwtSigner)

return jwt
}

private getPrivateKey(): `0x${string}` {
Expand Down
29 changes: 19 additions & 10 deletions apps/armory/src/data-feed/core/service/price-feed.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Action, AssetId, Feed, Signature } from '@narval/policy-engine-shared'
import { Alg, hash } from '@narval/signature'
import { Action, AssetId, Feed, JwtString } from '@narval/policy-engine-shared'
import { Payload, SigningAlg, hash, hexToBase64Url, privateKeyToJwk, signJwt } from '@narval/signature'
import { InputType, Intents, safeDecode } from '@narval/transaction-request-intent'
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
Expand All @@ -26,17 +26,26 @@ export class PriceFeedService implements DataFeed<Prices> {
return PriceFeedService.SOURCE_ID
}

async sign(data: Prices): Promise<Signature> {
async sign(data: Prices): Promise<JwtString> {
const account = privateKeyToAccount(this.getPrivateKey())
const sig = await account.signMessage({
message: hash(data)
})

return {
alg: Alg.ES256K,
pubKey: account.publicKey,
sig
const jwtSigner = async (msg: string) => {
const jwtSig = await account.signMessage({ message: msg })

return hexToBase64Url(jwtSig)
}

const now = Math.floor(Date.now() / 1000)
const jwk = privateKeyToJwk(this.getPrivateKey())
const payload: Payload = {
data: hash(data),
sub: account.address,
iss: 'https://armory.narval.xyz',
iat: now
}
const jwt = await signJwt(payload, jwk, { alg: SigningAlg.EIP191 }, jwtSigner)

return jwt
}

getPubKey(): string {
Expand Down
4 changes: 2 additions & 2 deletions apps/armory/src/data-feed/core/type/data-feed.type.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Feed, Signature } from '@narval/policy-engine-shared'
import { Feed } from '@narval/policy-engine-shared'
// TODO (@wcalderipe, 06/02/24): Move the AuthorizationRequest type to shared
import { AuthorizationRequest } from '../../../orchestration/core/type/domain.type'

export interface DataFeed<Data> {
getId(): string
getPubKey(): string
getFeed(input: AuthorizationRequest): Promise<Feed<Data>>
sign(data: Data): Promise<Signature>
sign(data: Data): Promise<string>
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Action } from '@narval/policy-engine-shared'
import { Alg } from '@narval/signature'
import { getQueueToken } from '@nestjs/bull'
import { HttpStatus, INestApplication } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
Expand All @@ -11,7 +10,6 @@ import { stringToHex } from 'viem'
import { load } from '../../../armory.config'
import { AUTHORIZATION_REQUEST_PROCESSING_QUEUE, REQUEST_HEADER_ORG_ID } from '../../../armory.constant'
import { AuthorizationRequest } from '../../../orchestration/core/type/domain.type'
import { SignatureDto } from '../../../orchestration/http/rest/dto/signature.dto'
import { AuthorizationRequestRepository } from '../../../orchestration/persistence/repository/authorization-request.repository'
import { PersistenceModule } from '../../../shared/module/persistence/persistence.module'
import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service'
Expand All @@ -27,23 +25,12 @@ describe('Authorization Request', () => {
let authzRequestRepository: AuthorizationRequestRepository
let authzRequestProcessingQueue: Queue

const authentication: SignatureDto = {
alg: Alg.ES256K,
pubKey: '0xd75D626a116D4a1959fE3bB938B2e7c116A05890',
sig: '0xe24d097cea880a40f8be2cf42f497b9fbda5f9e4a31b596827e051d78dce75c032fa7e5ee3046f7c6f116e5b98cb8d268fa9b9d222ff44719e2ec2a0d9159d0d1c'
}
const authentication =
'0xe24d097cea880a40f8be2cf42f497b9fbda5f9e4a31b596827e051d78dce75c032fa7e5ee3046f7c6f116e5b98cb8d268fa9b9d222ff44719e2ec2a0d9159d0d1c'

const approvals: SignatureDto[] = [
{
alg: Alg.ES256K,
pubKey: '0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06',
sig: '0x48510e3b74799b8e8f4e01aba0d196e18f66d86a62ae91abf5b89be9391c15661c7d29ee4654a300ed6db977da512475ed5a39f70f677e23d1b2f53c1554d0dd1b'
},
{
alg: Alg.ES256K,
pubKey: '0xab88c8785D0C00082dE75D801Fcb1d5066a6311e',
sig: '0xcc645f43d8df80c4deeb2e60a8c0c15d58586d2c29ea7c85208cea81d1c47cbd787b1c8473dde70c3a7d49f573e491223107933257b2b99ecc4806b7cc16848d1c'
}
const approvals: string[] = [
'0x48510e3b74799b8e8f4e01aba0d196e18f66d86a62ae91abf5b89be9391c15661c7d29ee4654a300ed6db977da512475ed5a39f70f677e23d1b2f53c1554d0dd1b',
'0xcc645f43d8df80c4deeb2e60a8c0c15d58586d2c29ea7c85208cea81d1c47cbd787b1c8473dde70c3a7d49f573e491223107933257b2b99ecc4806b7cc16848d1c'
]

// TODO: Create domain type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import { Test, TestingModule } from '@nestjs/testing'
import { MockProxy, mock } from 'jest-mock-extended'
import { times } from 'lodash/fp'
import {
generateApproval,
generateAuthorizationRequest,
generateSignTransactionRequest,
generateSignature,
generateTransactionRequest
} from '../../../../../__test__/fixture/authorization-request.fixture'
import { generateTransfer } from '../../../../../__test__/fixture/transfer-tracking.fixture'
Expand All @@ -20,16 +18,14 @@ import { TransferTrackingService } from '../../../../../transfer-tracking/core/s
import { AuthorizationRequestAlreadyProcessingException } from '../../../../core/exception/authorization-request-already-processing.exception'
import { AuthorizationRequestService } from '../../../../core/service/authorization-request.service'
import { ClusterService } from '../../../../core/service/cluster.service'
import {
Approval,
AuthorizationRequest,
AuthorizationRequestStatus,
SignTransaction
} from '../../../../core/type/domain.type'
import { AuthorizationRequest, AuthorizationRequestStatus, SignTransaction } from '../../../../core/type/domain.type'
import { AuthorizationRequestRepository } from '../../../../persistence/repository/authorization-request.repository'
import { AuthorizationRequestProcessingProducer } from '../../../../queue/producer/authorization-request-processing.producer'

describe(AuthorizationRequestService.name, () => {
const jwt =
'eyJraWQiOiIweDJjNDg5NTIxNTk3M0NiQmQ3NzhDMzJjNDU2QzA3NGI5OWRhRjhCZjEiLCJhbGciOiJFSVAxOTEiLCJ0eXAiOiJKV1QifQ.eyJyZXF1ZXN0SGFzaCI6IjYwOGFiZTkwOGNmZmVhYjFmYzMzZWRkZTZiNDQ1ODZmOWRhY2JjOWM2ZmU2ZjBhMTNmYTMwNzIzNzI5MGNlNWEiLCJzdWIiOiJ0ZXN0LXJvb3QtdXNlci11aWQiLCJpc3MiOiJodHRwczovL2FybW9yeS5uYXJ2YWwueHl6IiwiY25mIjp7Imt0eSI6IkVDIiwiY3J2Ijoic2VjcDI1NmsxIiwiYWxnIjoiRVMyNTZLIiwidXNlIjoic2lnIiwia2lkIjoiMHgwMDBjMGQxOTEzMDhBMzM2MzU2QkVlMzgxM0NDMTdGNjg2ODk3MkM0IiwieCI6IjA0YTlmM2JjZjY1MDUwNTk1OTdmNmYyN2FkOGMwZjAzYTNiZDdhMTc2MzUyMGIwYmZlYzIwNDQ4OGI4ZTU4NDAiLCJ5IjoiN2VlOTI4NDVhYjFjMzVhNzg0YjA1ZmRmYTU2NzcxNWM1M2JiMmYyOTk0OWIyNzcxNGUzYzE3NjBlMzcwOTAwOWE2In19.gFDywYsxY2-uT6H6hyxk51CtJhAZpI8WtcvoXHltiWsoBVOot1zMo3nHAhkWlYRmD3RuLtmOYzi6TwTUM8mFyBs'

let module: TestingModule
let authzRequestRepositoryMock: MockProxy<AuthorizationRequestRepository>
let authzRequestProcessingProducerMock: MockProxy<AuthorizationRequestProcessingProducer>
Expand Down Expand Up @@ -90,11 +86,9 @@ describe(AuthorizationRequestService.name, () => {
})

describe('approve', () => {
const approval: Approval = generateApproval()

const updatedAuthzRequest: AuthorizationRequest = {
...authzRequest,
approvals: [approval]
approvals: [jwt]
}

beforeEach(() => {
Expand All @@ -106,11 +100,11 @@ describe(AuthorizationRequestService.name, () => {
it('creates a new approval and evaluates the authorization request', async () => {
authzRequestRepositoryMock.update.mockResolvedValue(updatedAuthzRequest)

await service.approve(authzRequest.id, approval)
await service.approve(authzRequest.id, jwt)

expect(authzRequestRepositoryMock.update).toHaveBeenCalledWith({
id: authzRequest.id,
approvals: [approval]
approvals: [jwt]
})
expect(service.evaluate).toHaveBeenCalledWith(updatedAuthzRequest)
})
Expand All @@ -120,7 +114,9 @@ describe(AuthorizationRequestService.name, () => {
const evaluationResponse: EvaluationResponse = {
decision: Decision.PERMIT,
request: authzRequest.request,
attestation: generateSignature(),
accessToken: {
value: jwt
},
transactionRequestIntent: {
type: Intents.TRANSFER_NATIVE,
amount: '1000000000000000000',
Expand Down Expand Up @@ -176,7 +172,7 @@ describe(AuthorizationRequestService.name, () => {
expect.objectContaining({
id: expect.any(String),
decision: evaluationResponse.decision,
signature: evaluationResponse.attestation?.sig,
signature: evaluationResponse.accessToken?.value,
createdAt: expect.any(Date)
})
]
Expand Down Expand Up @@ -207,7 +203,7 @@ describe(AuthorizationRequestService.name, () => {
chainId: request.transactionRequest.chainId,
orgId: authzRequest.orgId,
requestId: authzRequest.id,
initiatedBy: authzRequest.authentication.pubKey,
initiatedBy: authzRequest.authentication, // TODO: this will change when the underlying data is corrected.
rates: {
'fiat:usd': 0.99
},
Expand Down
Loading

0 comments on commit 02e7946

Please sign in to comment.