Skip to content

Commit

Permalink
Adding SIGN_MESSAGE action, signing EIP191 messages (#192)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattschoch authored Mar 26, 2024
1 parent 9498abc commit b39b2c7
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 21 deletions.
45 changes: 39 additions & 6 deletions apps/vault/src/vault/__test__/e2e/sign.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EncryptionModuleOptionProvider } from '@narval/encryption-module'
import { Action } from '@narval/policy-engine-shared'
import {
JwsdHeader,
Payload,
Expand All @@ -17,6 +18,7 @@ import { Test, TestingModule } from '@nestjs/testing'
import { ACCOUNT, UNSAFE_PRIVATE_KEY } from 'packages/policy-engine-shared/src/lib/dev.fixture'
import request from 'supertest'
import { v4 as uuid } from 'uuid'
import { verifyMessage } from 'viem'
import { load } from '../../../main.config'
import { REQUEST_HEADER_CLIENT_ID } from '../../../main.constant'
import { KeyValueRepository } from '../../../shared/module/key-value/core/repository/key-value.repository'
Expand Down Expand Up @@ -50,16 +52,16 @@ describe('Sign', () => {
}

const wallet: Wallet = {
id: 'eip155:eoa:0xc3bdcdb4f593aa5a5d81cd425f6fc3265d962157',
address: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157',
id: 'eip155:eoa:0x2c4895215973CbBd778C32c456C074b99daF8Bf1',
address: '0x2c4895215973CbBd778C32c456C074b99daF8Bf1',
privateKey: '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'
}

const defaultRequest = {
action: 'signTransaction',
nonce: 'random-nonce-111',
transactionRequest: {
from: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157',
from: '0x2c4895215973CbBd778C32c456C074b99daF8Bf1',
to: '0x04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B',
chainId: 137,
value: '0x5af3107a4000',
Expand All @@ -70,7 +72,7 @@ describe('Sign', () => {
maxFeePerGas: '291175227375',
maxPriorityFeePerGas: '81000000000'
},
resourceId: 'eip155:eoa:0xc3bdcdb4f593aa5a5d81cd425f6fc3265d962157'
resourceId: 'eip155:eoa:0x2c4895215973CbBd778C32c456C074b99daF8Bf1'
}

const getAccessToken = async (request?: unknown, opts: object = {}) => {
Expand Down Expand Up @@ -144,7 +146,7 @@ describe('Sign', () => {
action: 'signTransaction',
nonce: 'random-nonce-111',
transactionRequest: {
from: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157',
from: '0x2c4895215973CbBd778C32c456C074b99daF8Bf1',
to: '04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B', // INVALID
chainId: 137,
value: '0x5af3107a4000',
Expand All @@ -155,7 +157,7 @@ describe('Sign', () => {
maxFeePerGas: '291175227375',
maxPriorityFeePerGas: '81000000000'
},
resourceId: 'eip155:eoa:0xc3bdcdb4f593aa5a5d81cd425f6fc3265d962157'
resourceId: 'eip155:eoa:0x2c4895215973CbBd778C32c456C074b99daF8Bf1'
}
}

Expand Down Expand Up @@ -194,6 +196,37 @@ describe('Sign', () => {
'0x02f875818982013d8512dbf9ea008543cb655fef82520c9404b12f0863b83c7162429f0ebb0dfda20e1aa97b865af3107a400080c080a00de78cbb96f83ef1b8d6be4d55b4046b2706c7d63ce0a815bae2b1ea4f891e6ba06f7648a9c9710b171d55e056c4abca268857f607a8a4a257d945fc44ace9f076'
})
})

it('signs Message', async () => {
const messageRequest = {
action: Action.SIGN_MESSAGE,
nonce: 'random-nonce-111',
message: 'My ASCII message',
resourceId: 'eip155:eoa:0x2c4895215973CbBd778C32c456C074b99daF8Bf1'
}

const accessToken = await getAccessToken(messageRequest)
const { status, body } = await request(app.getHttpServer())
.post('/sign')
.set(REQUEST_HEADER_CLIENT_ID, clientId)
.set('authorization', `GNAP ${accessToken}`)
.send({ request: messageRequest })

const isVerified = await verifyMessage({
address: wallet.address,
message: messageRequest.message,
signature: body.signature
})

expect(body).toEqual({
signature:
'0x65071b7126abd24fe6b8fa396529e21d22448d23ff1a6c5a0e043a4f641cd11b2a21958127d1b91db4d991f8b33ad6b201637799a95eadbe3a7cf5cee26bd9521b'
})

expect(status).toEqual(HttpStatus.CREATED)

expect(isVerified).toEqual(true)
})
})

describe('AuthorizationGuard', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { Request } from '@narval/policy-engine-shared'
import { Action, Request } from '@narval/policy-engine-shared'
import { Test } from '@nestjs/testing'
import { Hex, TransactionSerializable, hexToBigInt, parseTransaction, serializeTransaction } from 'viem'
import {
Hex,
TransactionSerializable,
hexToBigInt,
parseTransaction,
serializeTransaction,
toHex,
verifyMessage
} from 'viem'
import { Wallet } from '../../../../../shared/type/domain.type'
import { WalletRepository } from '../../../../persistence/repository/wallet.repository'
import { SigningService } from '../../signing.service'
Expand All @@ -9,8 +17,8 @@ describe('SigningService', () => {
let signingService: SigningService

const wallet: Wallet = {
id: 'eip155:eoa:0xc3bdcdb4f593aa5a5d81cd425f6fc3265d962157',
address: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157',
id: 'eip155:eoa:0x2c4895215973CbBd778C32c456C074b99daF8Bf1',
address: '0x2c4895215973CbBd778C32c456C074b99daF8Bf1',
privateKey: '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'
}

Expand All @@ -37,9 +45,9 @@ describe('SigningService', () => {
const request: Request = {
action: 'signTransaction',
nonce: 'random-nonce-111',
resourceId: 'eip155:eoa:0xc3bdcdb4f593aa5a5d81cd425f6fc3265d962157',
resourceId: 'eip155:eoa:0x2c4895215973CbBd778C32c456C074b99daF8Bf1',
transactionRequest: {
from: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157',
from: '0x2c4895215973CbBd778C32c456C074b99daF8Bf1',
to: '0x04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B',
chainId: 137,
value: '0x5af3107a4000',
Expand All @@ -65,7 +73,7 @@ describe('SigningService', () => {
// Just for testing formatting & stuff
it('should serialize/deserialize', async () => {
const txRequest: TransactionSerializable = {
// from: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157',
// from: '0x2c4895215973CbBd778C32c456C074b99daF8Bf1',
to: '0x04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B'.toLowerCase() as Hex,
chainId: 137,
value: hexToBigInt('0x5af3107a4000'),
Expand All @@ -77,5 +85,59 @@ describe('SigningService', () => {

expect(deserialized).toEqual(txRequest)
})

it('signs EIP191 Message string', async () => {
const tenantId = 'tenantId'
const messageRequest: Request = {
action: Action.SIGN_MESSAGE,
nonce: 'random-nonce-111',
message: 'My ASCII message',
resourceId: 'eip155:eoa:0x2c4895215973CbBd778C32c456C074b99daF8Bf1'
}

const expectedSignature =
'0x65071b7126abd24fe6b8fa396529e21d22448d23ff1a6c5a0e043a4f641cd11b2a21958127d1b91db4d991f8b33ad6b201637799a95eadbe3a7cf5cee26bd9521b'

// Call the sign method
const result = await signingService.sign(tenantId, messageRequest)

const isVerified = await verifyMessage({
address: wallet.address,
message: messageRequest.message,
signature: result
})

// Assert the result
expect(result).toEqual(expectedSignature)
expect(isVerified).toEqual(true)
})

it('signs EIP191 Message Hex', async () => {
const tenantId = 'tenantId'
const messageRequest: Request = {
action: Action.SIGN_MESSAGE,
nonce: 'random-nonce-111',
message: {
raw: toHex('My ASCII message')
},
resourceId: 'eip155:eoa:0x2c4895215973CbBd778C32c456C074b99daF8Bf1'
}

const expectedSignature =
'0x65071b7126abd24fe6b8fa396529e21d22448d23ff1a6c5a0e043a4f641cd11b2a21958127d1b91db4d991f8b33ad6b201637799a95eadbe3a7cf5cee26bd9521b'

// Call the sign method
const result = await signingService.sign(tenantId, messageRequest)

const isVerified = await verifyMessage({
address: wallet.address,
message: messageRequest.message,
signature: result
})

// Assert the result
expect(result).toEqual(expectedSignature)
expect(isVerified).toEqual(true)
})
})
})
29 changes: 27 additions & 2 deletions apps/vault/src/vault/core/service/signing.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Action, Hex, Request, SignTransactionAction } from '@narval/policy-engine-shared'
import { Action, Hex, Request, SignMessageAction, SignTransactionAction } from '@narval/policy-engine-shared'
import { HttpStatus, Injectable } from '@nestjs/common'
import {
TransactionRequest,
Expand All @@ -18,9 +18,11 @@ import { WalletRepository } from '../../persistence/repository/wallet.repository
export class SigningService {
constructor(private walletRepository: WalletRepository) {}

async sign(tenantId: string, request: Request): Promise<string> {
async sign(tenantId: string, request: Request): Promise<Hex> {
if (request.action === Action.SIGN_TRANSACTION) {
return this.signTransaction(tenantId, request)
} else if (request.action === Action.SIGN_MESSAGE) {
return this.signMessage(tenantId, request)
}

throw new Error('Action not supported')
Expand Down Expand Up @@ -78,4 +80,27 @@ export class SigningService {

return signature
}

async signMessage(tenantId: string, action: SignMessageAction): Promise<Hex> {
const { message, resourceId } = action
const wallet = await this.walletRepository.findById(tenantId, resourceId)
if (!wallet) {
throw new ApplicationException({
message: 'Wallet not found',
suggestedHttpStatusCode: HttpStatus.BAD_REQUEST,
context: { clientId: tenantId, resourceId }
})
}

const account = privateKeyToAccount(wallet.privateKey)

const client = createWalletClient({
account,
chain: chains.mainnet,
transport: http('') // clear the RPC so we don't call any chain stuff here.
})

const signature = await client.signMessage({ message })
return signature
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Action, toHex } from '@narval/policy-engine-shared'
import { Hex } from '@narval/signature'
import { validateSync } from 'class-validator'
import { SignMessageRequestDataDto } from '../../sign-message-request-data-dto'

describe('SignMessageRequestDataDto', () => {
it('should validate a valid SignMessageRequestDataDto object', () => {
const dto = new SignMessageRequestDataDto()
dto.action = Action.SIGN_MESSAGE
dto.nonce = 'xxx'
dto.resourceId = 'eip155:eoa:0x2c4895215973CbBd778C32c456C074b99daF8Bf1'
dto.message = 'My ASCII message'

const errors = validateSync(dto)

expect(errors).toEqual([])
expect(errors.length).toBe(0)
})

it('should not validate an invalid SignMessageRequestDataDto object', () => {
const dto = new SignMessageRequestDataDto()
dto.nonce = 'xxx'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
dto.action = 'invalid-action' as any // Invalid action value
dto.resourceId = 'invalid-resource-id' // Invalid resourceId value
// eslint-disable-next-line @typescript-eslint/no-explicit-any
dto.message = 123 as any // Invalid message value

const errors = validateSync(dto)

expect(errors.length).toBeGreaterThan(0)
})

it('validates nested raw message', () => {
const dto = new SignMessageRequestDataDto()
dto.nonce = 'xxx'
dto.action = Action.SIGN_MESSAGE
dto.resourceId = 'eip155:eoa:0x2c4895215973CbBd778C32c456C074b99daF8Bf1'
dto.message = { raw: toHex('My ASCII message') }

const errors = validateSync(dto)

expect(errors.length).toBe(0)
})

it('validates nested raw message must be hex', () => {
const dto = new SignMessageRequestDataDto()
dto.nonce = 'xxx'
dto.action = Action.SIGN_MESSAGE
dto.resourceId = 'eip155:eoa:0x2c4895215973CbBd778C32c456C074b99daF8Bf1'
dto.message = { raw: 'My ASCII message' as Hex }

const errors = validateSync(dto)

expect(errors.length).toBe(1)
})
})
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
import { Action } from '@narval/policy-engine-shared'
import { Action, isHexString } from '@narval/policy-engine-shared'
import { Hex } from '@narval/signature'
import { ApiProperty } from '@nestjs/swagger'
import { IsDefined, IsIn, IsString } from 'class-validator'
import { IsDefined, IsIn, IsString, ValidationOptions, registerDecorator } from 'class-validator'
import { BaseActionDto } from './'

// This is needed because class-validator cannot handle `string | RawMessage` when using @ValidateNested()
function IsStringOrHasRawProperty(validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isStringOrHasRawProperty',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate: (value: unknown) => {
if (value === null || value === undefined) return false
if (typeof value === 'string') return true
if (typeof value === 'object' && 'raw' in value) {
return isHexString(value.raw)
}
return false
},
defaultMessage: () => 'The message must be a string or an object with a raw property as a hex string'
}
})
}
}

class RawMessage {
raw: Hex
}
export class SignMessageRequestDataDto extends BaseActionDto {
@IsIn(Object.values(Action))
@IsDefined()
Expand All @@ -17,8 +44,7 @@ export class SignMessageRequestDataDto extends BaseActionDto {
@ApiProperty()
resourceId: string

@IsString()
@IsDefined()
@ApiProperty()
message: string // TODO: Is this string hex or raw?
@IsStringOrHasRawProperty()
message: string | RawMessage
}
6 changes: 5 additions & 1 deletion packages/policy-engine-shared/src/lib/type/action.type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Hex } from 'viem'
import { Address, JwtString, TransactionRequest } from './domain.type'
import {
AccountClassification,
Expand Down Expand Up @@ -72,10 +73,13 @@ export type SignTransactionAction = BaseAction & {
transactionRequest: TransactionRequest
}

// Matching viem's SignableMessage options https://viem.sh/docs/actions/wallet/signMessage#message
export type SignableMessage = string | { raw: Hex }

export type SignMessageAction = BaseAction & {
action: typeof Action.SIGN_MESSAGE
resourceId: string
message: string
message: SignableMessage
}

export type SignTypedDataAction = BaseAction & {
Expand Down

0 comments on commit b39b2c7

Please sign in to comment.