Skip to content

Commit

Permalink
precise sig fns by alg, change credential shape, add jwk schemas
Browse files Browse the repository at this point in the history
  • Loading branch information
Ptroger committed Mar 20, 2024
1 parent e28efa1 commit 242e8b9
Show file tree
Hide file tree
Showing 19 changed files with 313 additions and 137 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Feed, HistoricalTransfer, JwtString } from '@narval/policy-engine-shared'
import { Payload, SigningAlg, hash, hexToBase64Url, privateKeyToJwk, signJwt } from '@narval/signature'
import { Payload, SigningAlg, hash, hexToBase64Url, secp256k1PrivateKeyToJwk, signJwt } from '@narval/signature'
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { mapValues, omit } from 'lodash/fp'
Expand Down Expand Up @@ -37,7 +37,7 @@ export class HistoricalTransferFeedService implements DataFeed<HistoricalTransfe
}

const now = Math.floor(Date.now() / 1000)
const jwk = privateKeyToJwk(this.getPrivateKey())
const jwk = secp256k1PrivateKeyToJwk(this.getPrivateKey())
const payload: Payload = {
data: hash(data),
sub: account.address,
Expand Down
4 changes: 2 additions & 2 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, JwtString } from '@narval/policy-engine-shared'
import { Payload, SigningAlg, hash, hexToBase64Url, privateKeyToJwk, signJwt } from '@narval/signature'
import { Payload, SigningAlg, hash, hexToBase64Url, secp256k1PrivateKeyToJwk, signJwt } from '@narval/signature'
import { InputType, Intents, safeDecode } from '@narval/transaction-request-intent'
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
Expand Down Expand Up @@ -36,7 +36,7 @@ export class PriceFeedService implements DataFeed<Prices> {
}

const now = Math.floor(Date.now() / 1000)
const jwk = privateKeyToJwk(this.getPrivateKey())
const jwk = secp256k1PrivateKeyToJwk(this.getPrivateKey())
const payload: Payload = {
data: hash(data),
sub: account.address,
Expand Down
12 changes: 4 additions & 8 deletions apps/policy-engine/src/engine/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import {
EvaluationRequest,
EvaluationResponse,
HistoricalTransfer,
JsonWebKey,
JwtString,
Request
} from '@narval/policy-engine-shared'
import { Payload, SigningAlg, decode, hash, privateKeyToJwk, publicKeyToJwk, verifyJwt } from '@narval/signature'
import { Payload, SigningAlg, decode, hash, secp256k1PrivateKeyToJwk, verifyJwt } from '@narval/signature'
import { safeDecode } from '@narval/transaction-request-intent'
import {
BadRequestException,
Expand All @@ -20,7 +19,6 @@ import {
} from '@nestjs/common'
import { InputType } from 'packages/transaction-request-intent/src/lib/domain'
import { Intent } from 'packages/transaction-request-intent/src/lib/intent.types'
import { Hex } from 'viem'
import { OpaResult, RegoInput } from '../shared/type/domain.type'
import { SigningService } from './core/service/signing.service'
import { OpaService } from './opa/opa.service'
Expand Down Expand Up @@ -83,9 +81,7 @@ export class AppService {
throw new NotFoundException('Credential not found')
}

const jwk = publicKeyToJwk(credential.pubKey as Hex)

const validJwt = await verifyJwt(requestSignature, jwk)
const validJwt = await verifyJwt(requestSignature, credential.key)
// Check the data is the same
if (validJwt.payload.requestHash !== verificationMessage) {
throw new BadRequestException('Invalid signature')
Expand Down Expand Up @@ -205,9 +201,9 @@ export class AppService {

// If we are allowing, then the ENGINE signs the verification too
if (finalDecision.decision === Decision.PERMIT) {
const tenantSigningKey: JsonWebKey = privateKeyToJwk(ENGINE_PRIVATE_KEY)
const tenantSigningKey = secp256k1PrivateKeyToJwk(ENGINE_PRIVATE_KEY)

const clientJwk = publicKeyToJwk(principalCredential.pubKey as Hex)
const { key: clientJwk } = principalCredential

const jwtPayload: Payload = {
requestHash: verificationMessage,
Expand Down
21 changes: 8 additions & 13 deletions apps/policy-engine/src/engine/core/service/signing.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { JsonWebKey, toHex } from '@narval/policy-engine-shared'
import { toHex } from '@narval/policy-engine-shared'
import {
Alg,
Payload,
PrivateKey,
PublicKey,
SigningAlg,
buildSignerEip191,
buildSignerEs256k,
privateKeyToJwk,
secp256k1PrivateKeyToJwk,
signJwt
} from '@narval/signature'
import { Injectable } from '@nestjs/common'
Expand All @@ -17,8 +19,8 @@ type KeyGenerationOptions = {
}

type KeyGenerationResponse = {
publicKey: JsonWebKey
privateKey?: JsonWebKey
publicKey: PublicKey
privateKey?: PrivateKey
}

type SignOptions = {
Expand All @@ -32,7 +34,7 @@ export class SigningService {
async generateSigningKey(alg: Alg, options?: KeyGenerationOptions): Promise<KeyGenerationResponse> {
if (alg === Alg.ES256K) {
const privateKey = toHex(secp256k1.utils.randomPrivateKey())
const privateJwk = privateKeyToJwk(privateKey, options?.keyId)
const privateJwk = secp256k1PrivateKeyToJwk(privateKey, options?.keyId)

// Remove the privateKey from the public jwk
const publicJwk = {
Expand All @@ -49,21 +51,14 @@ export class SigningService {
throw new Error('Unsupported algorithm')
}

async sign(payload: Payload, jwk: JsonWebKey, opts: SignOptions = {}): Promise<string> {
async sign(payload: Payload, jwk: PrivateKey, opts: SignOptions = {}): Promise<string> {
const alg: SigningAlg = opts.alg || jwk.alg
if (alg === SigningAlg.ES256K) {
if (!jwk.d) {
throw new Error('Missing private key')
}
const pk = jwk.d

const jwt = await signJwt(payload, jwk, opts, buildSignerEs256k(pk))

return jwt
} else if (alg === SigningAlg.EIP191) {
if (!jwk.d) {
throw new Error('Missing private key')
}
const pk = jwk.d

const jwt = await signJwt(payload, jwk, opts, buildSignerEip191(pk))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ export class EntityRepository {
return FIXTURE.ENTITIES
}

getCredentialForPubKey(pubKey: string): CredentialEntity | null {
return FIXTURE.ENTITIES.credentials.find((cred) => cred.pubKey === pubKey) || null
}
getCredential(id: string): CredentialEntity | null {
return FIXTURE.ENTITIES.credentials.find((cred) => cred.id === id) || null
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Action, EvaluationRequest, FIXTURE, Request, TransactionRequest } from '@narval/policy-engine-shared'
import { Payload, SigningAlg, buildSignerEip191, hash, privateKeyToJwk, signJwt } from '@narval/signature'
import { Payload, SigningAlg, buildSignerEip191, hash, secp256k1PrivateKeyToJwk, signJwt } from '@narval/signature'
import { UNSAFE_PRIVATE_KEY } from 'packages/policy-engine-shared/src/lib/dev.fixture'
import { toHex } from 'viem'

Expand Down Expand Up @@ -31,19 +31,19 @@ export const generateInboundRequest = async (): Promise<EvaluationRequest> => {
// const aliceSignature = await FIXTURE.ACCOUNT.Alice.signMessage({ message })
const aliceSignature = await signJwt(
payload,
privateKeyToJwk(UNSAFE_PRIVATE_KEY.Alice),
secp256k1PrivateKeyToJwk(UNSAFE_PRIVATE_KEY.Alice),
{ alg: SigningAlg.EIP191 },
buildSignerEip191(UNSAFE_PRIVATE_KEY.Alice)
)
const bobSignature = await signJwt(
payload,
privateKeyToJwk(UNSAFE_PRIVATE_KEY.Bob),
secp256k1PrivateKeyToJwk(UNSAFE_PRIVATE_KEY.Bob),
{ alg: SigningAlg.EIP191 },
buildSignerEip191(UNSAFE_PRIVATE_KEY.Bob)
)
const carolSignature = await signJwt(
payload,
privateKeyToJwk(UNSAFE_PRIVATE_KEY.Carol),
secp256k1PrivateKeyToJwk(UNSAFE_PRIVATE_KEY.Carol),
{ alg: SigningAlg.EIP191 },
buildSignerEip191(UNSAFE_PRIVATE_KEY.Carol)
)
Expand Down
77 changes: 52 additions & 25 deletions packages/policy-engine-shared/src/lib/dev.fixture.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Alg, addressToKid } from '@narval/signature'
import { Alg, Curves, KeyTypes, Use } from '@narval/signature'
import { PrivateKeyAccount } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { Action } from './type/action.type'
Expand Down Expand Up @@ -81,39 +81,66 @@ export const USER: Record<Personas, UserEntity> = {

export const CREDENTIAL: Record<Personas, CredentialEntity> = {
Root: {
id: addressToKid(ACCOUNT.Root.address),
pubKey: ACCOUNT.Root.publicKey,
address: ACCOUNT.Root.address,
alg: Alg.ES256K,
userId: USER.Root.id
id: '0x20FB9603DC2C011aBFdFbf270bD627e94065cBb9',
userId: USER.Root.id,
key: {
kty: KeyTypes.EC,
use: Use.ENC,
crv: Curves.SECP256K1,
alg: Alg.ES256K,
kid: '0x20FB9603DC2C011aBFdFbf270bD627e94065cBb9',
x: 'crqZ2XkCBgl1XwxjlQ02PKm_JJ4wJAkANJ6DidZRzTw',
y: 'GyAbgM5_HOaPmAHNatWanWmhLgaznyNHUIw5YUe_yyw'
}
},
Alice: {
id: addressToKid(ACCOUNT.Alice.address),
pubKey: ACCOUNT.Alice.publicKey,
address: ACCOUNT.Alice.address,
alg: Alg.ES256K,
userId: USER.Alice.id
id: '0xcdE93dc1C6D8AF279c33069233aEE5542F308594',
userId: USER.Alice.id,
key: {
kty: KeyTypes.EC,
use: Use.SIG,
crv: Curves.SECP256K1,
alg: Alg.ES256K,
kid: '0xcdE93dc1C6D8AF279c33069233aEE5542F308594',
x: 'vjNVzbnLxdazY0M-2BDnX54JexB8Pa9n_fucDJli6Bo',
y: 'jOAwUCXcLz7nhvW2mSwPBCZwv856ybAGK7LS6hvfdFQ'
}
},
Bob: {
id: addressToKid(ACCOUNT.Bob.address),
pubKey: ACCOUNT.Bob.publicKey,
address: ACCOUNT.Bob.address,
alg: Alg.ES256K,
id: '0x9A5Bd18C902887DCc2D881a352010C15eea229d',
key: {
kty: KeyTypes.EC,
crv: Curves.SECP256K1,
alg: Alg.ES256K,
kid: '0xc7916Ee805440bB386a88d09AED8688eFb99CB0F',
x: 'MjsuvdMuxs1AoQ12BuARzzTyilJNh2jQmErMZwR2M-E',
y: 'axLms3pGEX0Xujho5welzcn9mx_oV0Bs3uVeG9-eCqU'
},
userId: USER.Bob.id
},
Carol: {
id: addressToKid(ACCOUNT.Carol.address),
pubKey: ACCOUNT.Carol.publicKey,
address: ACCOUNT.Carol.address,
alg: Alg.ES256K,
userId: USER.Carol.id
id: '0xe99c6FBb2eE939682AB8A216a893cBD21CC2f982',
userId: USER.Carol.id,
key: {
kty: KeyTypes.EC,
crv: Curves.SECP256K1,
alg: Alg.ES256K,
kid: '0x9AA5Bd18C902887DCc2D881a352010C15eea229d',
x: '4n3yf5qUBU0sDH9yGjdfiVRFEnQndbd5yGEupSdG6R4',
y: 'FESQhctMSQOF2E79YbCE8q1JIQWltMbvoCVwSsO19ck'
}
},
Dave: {
id: addressToKid(ACCOUNT.Dave.address),
pubKey: ACCOUNT.Dave.publicKey,
address: ACCOUNT.Dave.address,
alg: Alg.ES256K,
userId: USER.Dave.id
id: '0xddd26a02e7c54e8dc373b9d2dcb309ecdeca815d',
userId: USER.Dave.id,
key: {
kty: KeyTypes.EC,
crv: Curves.SECP256K1,
alg: Alg.ES256K,
kid: '0xe99c6FBb2eE939682AB8A216a893cBD21CC2f982',
x: 'sdb8VZcfcI6t5i7BD3BTPoZPyYCxaVpw7H1BIUyPZ5M',
y: 'cIcYdzuWF7KqFKJrdQSmdjPpQzrk9_uzNycqtvtH1QI'
}
}
}

Expand Down
9 changes: 4 additions & 5 deletions packages/policy-engine-shared/src/lib/schema/entity.schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Alg } from '@narval/signature'
import { publicKeySchema } from '@narval/signature'
import { z } from 'zod'
import { addressSchema } from './address.schema'

Expand All @@ -23,10 +23,9 @@ export const accountClassificationSchema = z.nativeEnum({

export const credentialEntitySchema = z.object({
id: z.string(),
pubKey: z.string(),
address: z.string().optional(),
alg: z.nativeEnum(Alg),
userId: z.string()
userId: z.string(),
key: publicKeySchema
// TODO @ptroger: Should we be allowing a private key to be passed in entity data ?
})

export const organizationEntitySchema = z.object({
Expand Down
1 change: 1 addition & 0 deletions packages/signature/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './lib/decode'
export * from './lib/hash-request'
export * from './lib/schemas'
export * from './lib/sign'
export * from './lib/types'
export * from './lib/utils'
Expand Down
24 changes: 12 additions & 12 deletions packages/signature/src/lib/__test__/unit/sign.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { createPublicKey } from 'node:crypto'
import { toHex, verifyMessage } from 'viem'
import { privateKeyToAccount, signMessage } from 'viem/accounts'
import { buildSignerEip191, buildSignerEs256k, signJwt } from '../../sign'
import { Alg, JWK, Payload, SigningAlg } from '../../types'
import { Alg, Payload, PrivateKey, SigningAlg } from '../../types'
import {
base64UrlToBytes,
base64UrlToHex,
jwkToPrivateKey,
jwkToPublicKey,
privateKeyToJwk,
publicKeyToJwk
secp256k1PrivateKeyToHex,
secp256k1PrivateKeyToJwk,
secp256k1PublicKeyToHex,
secp256k1PublicKeyToJwk
} from '../../utils'
import { verifyJwt } from '../../verify'
import { HEADER_PART, PAYLOAD_PART, PRIVATE_KEY_PEM } from './mock'
Expand All @@ -38,14 +38,14 @@ describe('sign', () => {
it('should sign build & sign es256 JWT correctly with a PEM', async () => {
const key = await importPKCS8(PRIVATE_KEY_PEM, Alg.ES256)
const jwk = await exportJWK(key)
const jwt = await signJwt(payload, { ...jwk, alg: Alg.ES256 } as JWK)
const jwt = await signJwt(payload, { ...jwk, alg: Alg.ES256 } as PrivateKey)

const verified = await jwtVerify(jwt, key)
expect(verified.payload).toEqual(payload)
})

it('should build & sign a EIP191 JWT', async () => {
const jwk = privateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)
const jwk = secp256k1PrivateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)
const signer = buildSignerEip191(ENGINE_PRIVATE_KEY)

const jwt = await signJwt(payload, jwk, { alg: SigningAlg.EIP191 }, signer)
Expand Down Expand Up @@ -146,7 +146,7 @@ describe('sign', () => {
const viemPubKey = privateKeyToAccount(`0x${ENGINE_PRIVATE_KEY}`).publicKey
expect(toHex(publicKey)).toBe(viemPubKey) // Confirm that our key is in fact the same as what viem would give.

const jwk = privateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)
const jwk = secp256k1PrivateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)

const k = await createPublicKey({
format: 'jwk',
Expand All @@ -157,15 +157,15 @@ describe('sign', () => {
})

it('should convert to and from jwk', async () => {
const jwk = privateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)
const pk = jwkToPrivateKey(jwk)
const jwk = secp256k1PrivateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)
const pk = secp256k1PrivateKeyToHex(jwk)
expect(pk).toBe(`0x${ENGINE_PRIVATE_KEY}`)
})

it('should convert to and from public jwk', async () => {
const publicKey = secp256k1.getPublicKey(ENGINE_PRIVATE_KEY, false)
const jwk = publicKeyToJwk(toHex(publicKey))
const pk = jwkToPublicKey(jwk)
const jwk = secp256k1PublicKeyToJwk(toHex(publicKey))
const pk = secp256k1PublicKeyToHex(jwk)
expect(pk).toBe(toHex(publicKey))
})
})
4 changes: 2 additions & 2 deletions packages/signature/src/lib/__test__/unit/verify.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { hash } from '../../hash-request'
import { Payload } from '../../types'
import { privateKeyToJwk } from '../../utils'
import { secp256k1PrivateKeyToJwk } from '../../utils'
import { verifyJwt } from '../../verify'

describe('verify', () => {
const ENGINE_PRIVATE_KEY = '7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'

it('should verify a EIP191-signed JWT', async () => {
const jwk = privateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)
const jwk = secp256k1PrivateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)

const header = {
kid: '0x2c4895215973CbBd778C32c456C074b99daF8Bf1',
Expand Down
Loading

0 comments on commit 242e8b9

Please sign in to comment.