diff --git a/apps/vault/src/vault/core/service/__test__/unit/signing.service.spec.ts b/apps/vault/src/vault/core/service/__test__/unit/signing.service.spec.ts index 814e6a6a0..a8e5c908e 100644 --- a/apps/vault/src/vault/core/service/__test__/unit/signing.service.spec.ts +++ b/apps/vault/src/vault/core/service/__test__/unit/signing.service.spec.ts @@ -1,11 +1,14 @@ import { Action, Eip712TypedData, Request } from '@narval/policy-engine-shared' +import { Jwk, Secp256k1PublicKey, secp256k1PrivateKeyToJwk, verifySepc256k1 } from '@narval/signature' import { Test } from '@nestjs/testing' import { Hex, TransactionSerializable, + bytesToHex, hexToBigInt, parseTransaction, serializeTransaction, + stringToBytes, toHex, verifyMessage, verifyTypedData @@ -22,6 +25,7 @@ describe('SigningService', () => { address: '0x2c4895215973CbBd778C32c456C074b99daF8Bf1', privateKey: '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5' } + const privateKey: Jwk = secp256k1PrivateKeyToJwk(wallet.privateKey) beforeEach(async () => { const moduleRef = await Test.createTestingModule({ @@ -209,5 +213,24 @@ describe('SigningService', () => { expect(isVerified).toEqual(true) expect(result).toEqual(expectedSignature) }) + + it('signs raw payload', async () => { + const stringMessage = 'My ASCII message' + const byteMessage = stringToBytes(stringMessage) + const hexMessage = bytesToHex(byteMessage) + + const tenantId = 'tenantId' + const rawRequest: Request = { + action: Action.SIGN_RAW, + nonce: 'random-nonce-111', + rawMessage: hexMessage, + resourceId: 'eip155:eoa:0x2c4895215973CbBd778C32c456C074b99daF8Bf1' + } + + const result = await signingService.sign(tenantId, rawRequest) + + const isVerified = await verifySepc256k1(result, byteMessage, privateKey as Secp256k1PublicKey) + expect(isVerified).toEqual(true) + }) }) }) diff --git a/apps/vault/src/vault/core/service/signing.service.ts b/apps/vault/src/vault/core/service/signing.service.ts index 92d38bb49..6dbc55aaa 100644 --- a/apps/vault/src/vault/core/service/signing.service.ts +++ b/apps/vault/src/vault/core/service/signing.service.ts @@ -3,9 +3,11 @@ import { Hex, Request, SignMessageAction, + SignRawAction, SignTransactionAction, SignTypedDataAction } from '@narval/policy-engine-shared' +import { signSecp256k1 } from '@narval/signature' import { HttpStatus, Injectable } from '@nestjs/common' import { TransactionRequest, @@ -13,7 +15,9 @@ import { createWalletClient, extractChain, hexToBigInt, + hexToBytes, http, + signatureToHex, transactionType } from 'viem' import { privateKeyToAccount } from 'viem/accounts' @@ -32,12 +36,14 @@ export class SigningService { return this.signMessage(tenantId, request) } else if (request.action === Action.SIGN_TYPED_DATA) { return this.signTypedData(tenantId, request) + } else if (request.action === Action.SIGN_RAW) { + return this.signRaw(tenantId, request) } throw new Error('Action not supported') } - async #buildClient(tenantId: string, resourceId: string, chainId?: number) { + async #getWallet(tenantId: string, resourceId: string) { const wallet = await this.walletRepository.findById(tenantId, resourceId) if (!wallet) { throw new ApplicationException({ @@ -47,6 +53,12 @@ export class SigningService { }) } + return wallet + } + + async #buildClient(tenantId: string, resourceId: string, chainId?: number) { + const wallet = await this.#getWallet(tenantId, resourceId) + const account = privateKeyToAccount(wallet.privateKey) const chain = extractChain({ chains: Object.values(chains), @@ -111,4 +123,16 @@ export class SigningService { const signature = await client.signTypedData(typedData) return signature } + + // Sign a raw message; nothing ETH or chain-specific, simply performs an ecdsa signature on the byte representation of the hex-encoded raw message + async signRaw(tenantId: string, action: SignRawAction): Promise { + const { rawMessage, resourceId } = action + + const wallet = await this.#getWallet(tenantId, resourceId) + const message = hexToBytes(rawMessage) + const signature = await signSecp256k1(message, wallet.privateKey, true) + + const hexSignature = signatureToHex(signature) + return hexSignature + } } diff --git a/apps/vault/src/vault/http/rest/controller/sign.controller.ts b/apps/vault/src/vault/http/rest/controller/sign.controller.ts index 4f1f3a946..3d2959e29 100644 --- a/apps/vault/src/vault/http/rest/controller/sign.controller.ts +++ b/apps/vault/src/vault/http/rest/controller/sign.controller.ts @@ -3,6 +3,7 @@ import { Body, Controller, Post, UseGuards } from '@nestjs/common' import { createZodDto } from 'nestjs-zod' import { SignMessageAction, + SignRawAction, SignTransactionAction, SignTypedDataAction } from 'packages/policy-engine-shared/src/lib/type/action.type' @@ -12,7 +13,7 @@ import { AuthorizationGuard } from '../../../../shared/guard/authorization.guard import { SigningService } from '../../../core/service/signing.service' const SignRequest = z.object({ - request: z.union([SignTransactionAction, SignMessageAction, SignTypedDataAction]) + request: z.union([SignTransactionAction, SignMessageAction, SignTypedDataAction, SignRawAction]) }) class SignRequestDto extends createZodDto(SignRequest) {} diff --git a/packages/policy-engine-shared/src/lib/type/action.type.ts b/packages/policy-engine-shared/src/lib/type/action.type.ts index 635b4d760..f5c1e1a95 100644 --- a/packages/policy-engine-shared/src/lib/type/action.type.ts +++ b/packages/policy-engine-shared/src/lib/type/action.type.ts @@ -176,6 +176,16 @@ export const SignTypedDataAction = z.intersection( ) export type SignTypedDataAction = z.infer +export const SignRawAction = z.intersection( + BaseActionSchema, + z.object({ + action: z.literal(Action.SIGN_RAW), + resourceId: z.string(), + rawMessage: hexSchema + }) +) +export type SignRawAction = z.infer + export type CreateOrganizationAction = BaseAction & { action: typeof Action.CREATE_ORGANIZATION organization: { diff --git a/packages/policy-engine-shared/src/lib/type/domain.type.ts b/packages/policy-engine-shared/src/lib/type/domain.type.ts index 622333397..e273c22ee 100644 --- a/packages/policy-engine-shared/src/lib/type/domain.type.ts +++ b/packages/policy-engine-shared/src/lib/type/domain.type.ts @@ -1,7 +1,13 @@ import { z } from 'zod' import { approvalRequirementSchema } from '../schema/domain.schema' import { AssetId } from '../util/caip.util' -import { CreateOrganizationAction, SignMessageAction, SignTransactionAction, SignTypedDataAction } from './action.type' +import { + CreateOrganizationAction, + type SignMessageAction, + type SignRawAction, + type SignTransactionAction, + type SignTypedDataAction +} from './action.type' export enum Decision { PERMIT = 'Permit', @@ -92,7 +98,12 @@ export type HistoricalTransfer = { */ export type Prices = Record> -export type Request = SignTransactionAction | SignMessageAction | SignTypedDataAction | CreateOrganizationAction +export type Request = + | SignTransactionAction + | SignMessageAction + | SignTypedDataAction + | SignRawAction + | CreateOrganizationAction /** * The feeds represent arbitrary data collected by the Armory and diff --git a/packages/signature/src/lib/__test__/unit/verify.spec.ts b/packages/signature/src/lib/__test__/unit/verify.spec.ts index b08f0e881..e95ee46e3 100644 --- a/packages/signature/src/lib/__test__/unit/verify.spec.ts +++ b/packages/signature/src/lib/__test__/unit/verify.spec.ts @@ -1,7 +1,9 @@ +import { signatureToHex, toBytes } from 'viem' import { hash } from '../../hash-request' +import { signSecp256k1 } from '../../sign' import { Payload } from '../../types' import { secp256k1PrivateKeyToJwk } from '../../utils' -import { verifyJwt } from '../../verify' +import { verifyJwt, verifySepc256k1 } from '../../verify' describe('verify', () => { const ENGINE_PRIVATE_KEY = '7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5' @@ -88,4 +90,14 @@ describe('verify', () => { expect(res).toBeDefined() expect(res.payload.data).toEqual(policyHash) }) + + it('verifies raw secp256k1 signatures', async () => { + const msg = toBytes('My ASCII message') + const jwk = secp256k1PrivateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`) + + const signature = await signSecp256k1(msg, ENGINE_PRIVATE_KEY, true) + const hexSignature = signatureToHex(signature) + const isVerified = await verifySepc256k1(hexSignature, msg, jwk) + expect(isVerified).toEqual(true) + }) }) diff --git a/packages/signature/src/lib/verify.ts b/packages/signature/src/lib/verify.ts index 178d8ad10..9103a3cd1 100644 --- a/packages/signature/src/lib/verify.ts +++ b/packages/signature/src/lib/verify.ts @@ -6,7 +6,7 @@ import { JwtError } from './error' import { publicKeySchema } from './schemas' import { eip191Hash } from './sign' import { isSepc256k1PublicKeyJwk } from './typeguards' -import { Alg, EoaPublicKey, Hex, Jwk, Jwsd, Jwt, Payload, PublicKey, Secp256k1PublicKey, SigningAlg } from './types' +import { Alg, Hex, Jwk, Jwsd, Jwt, Payload, PublicKey, Secp256k1PublicKey, SigningAlg } from './types' import { base64UrlToHex, secp256k1PublicKeyToHex } from './utils' import { validate } from './validate' @@ -46,17 +46,10 @@ const verifyEip191WithPublicKey = async (sig: Hex, hash: Uint8Array, jwk: Public }) } -const verifySepc256k1 = async ( - sig: Hex, - hash: Uint8Array, - jwk: Secp256k1PublicKey | EoaPublicKey -): Promise => { - if (isSepc256k1PublicKeyJwk(jwk)) { - await verifyEip191WithPublicKey(sig, hash, jwk) - } else { - await verifyEip191WithRecovery(sig, hash, jwk.addr) - } - return true +export const verifySepc256k1 = async (sig: Hex, hash: Uint8Array, jwk: Secp256k1PublicKey): Promise => { + const pubKey = secp256k1PublicKeyToHex(jwk) + const isValid = secp256k1.verify(sig.slice(2, 130), hash, pubKey.slice(2)) === true + return isValid } export const verifyEip191 = async (jwt: string, jwk: PublicKey): Promise => { @@ -66,7 +59,11 @@ export const verifyEip191 = async (jwt: string, jwk: PublicKey): Promise