Skip to content

Commit

Permalink
Adding raw signign
Browse files Browse the repository at this point in the history
  • Loading branch information
mattschoch committed Mar 28, 2024
1 parent 8acfb3c commit e23fba1
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,6 +25,7 @@ describe('SigningService', () => {
address: '0x2c4895215973CbBd778C32c456C074b99daF8Bf1',
privateKey: '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'
}
const privateKey: Jwk = secp256k1PrivateKeyToJwk(wallet.privateKey)

beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
Expand Down Expand Up @@ -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)
})
})
})
26 changes: 25 additions & 1 deletion apps/vault/src/vault/core/service/signing.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@ 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,
checksumAddress,
createWalletClient,
extractChain,
hexToBigInt,
hexToBytes,
http,
signatureToHex,
transactionType
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
Expand All @@ -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({
Expand All @@ -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.Chain[], number>({
chains: Object.values(chains),
Expand Down Expand Up @@ -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<Hex> {
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
}
}
3 changes: 2 additions & 1 deletion apps/vault/src/vault/http/rest/controller/sign.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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) {}
Expand Down
10 changes: 10 additions & 0 deletions packages/policy-engine-shared/src/lib/type/action.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,16 @@ export const SignTypedDataAction = z.intersection(
)
export type SignTypedDataAction = z.infer<typeof SignTypedDataAction>

export const SignRawAction = z.intersection(
BaseActionSchema,
z.object({
action: z.literal(Action.SIGN_RAW),
resourceId: z.string(),
rawMessage: hexSchema
})
)
export type SignRawAction = z.infer<typeof SignRawAction>

export type CreateOrganizationAction = BaseAction & {
action: typeof Action.CREATE_ORGANIZATION
organization: {
Expand Down
15 changes: 13 additions & 2 deletions packages/policy-engine-shared/src/lib/type/domain.type.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -92,7 +98,12 @@ export type HistoricalTransfer = {
*/
export type Prices = Record<AssetId, Record<string, number>>

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
Expand Down
14 changes: 13 additions & 1 deletion packages/signature/src/lib/__test__/unit/verify.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
})
})
23 changes: 10 additions & 13 deletions packages/signature/src/lib/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -46,17 +46,10 @@ const verifyEip191WithPublicKey = async (sig: Hex, hash: Uint8Array, jwk: Public
})
}

const verifySepc256k1 = async (
sig: Hex,
hash: Uint8Array,
jwk: Secp256k1PublicKey | EoaPublicKey
): Promise<boolean> => {
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<boolean> => {
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<boolean> => {
Expand All @@ -66,7 +59,11 @@ export const verifyEip191 = async (jwt: string, jwk: PublicKey): Promise<boolean
const sig = base64UrlToHex(jwtSig)

if (jwk.alg === Alg.ES256K) {
await verifySepc256k1(sig, msg, jwk)
if (isSepc256k1PublicKeyJwk(jwk)) {
await verifyEip191WithPublicKey(sig, msg, jwk)
} else {
await verifyEip191WithRecovery(sig, msg, jwk.addr)
}
} else {
throw new JwtError({
message: 'Validation error: unsupported algorithm',
Expand Down

0 comments on commit e23fba1

Please sign in to comment.