Skip to content

Commit

Permalink
Jwk schema definition and credential refactor (#176)
Browse files Browse the repository at this point in the history
* precise sig fns by alg, change credential shape, add jwk schemas

* Type change in legacy script

* remove unecessary logging

* add validation for public and private key

* handle exposed interface change in test

* fixed fixture data

* reverting unecessary changes to test file
  • Loading branch information
Ptroger authored Mar 22, 2024
1 parent 286ce66 commit 3f67d59
Show file tree
Hide file tree
Showing 21 changed files with 330 additions and 140 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
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 @@ -71,7 +71,7 @@ export class TenantService {
this.tenantRepository.savePolicyStore(clientId, stores.policy)
])

this.logger.log('Tenant data stores synced', { clientId, stores })
this.logger.log('Tenant data stores synced', { clientId })

return true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
Then,
toHex
} from '@narval/policy-engine-shared'
import { SigningAlg, buildSignerEip191, hash, privateKeyToJwk, signJwt } from '@narval/signature'
import { SigningAlg, buildSignerEip191, hash, secp256k1PrivateKeyToJwk, signJwt } from '@narval/signature'
import { ConfigModule, ConfigService, Path, PathValue } from '@nestjs/config'
import { Test, TestingModule } from '@nestjs/testing'
import { Config, load } from '../../../../policy-engine.config'
Expand All @@ -25,7 +25,7 @@ const ONE_ETH = toHex(BigInt('1000000000000000000'))
const UNSAFE_ENGINE_PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'

const getJwt = (option: { privateKey: Hex; request: Request; sub: string }): Promise<JwtString> => {
const jwk = privateKeyToJwk(option.privateKey)
const jwk = secp256k1PrivateKeyToJwk(option.privateKey)
const signer = buildSignerEip191(option.privateKey)

return signJwt(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,19 @@ import {
Entities,
EvaluationRequest,
EvaluationResponse,
JsonWebKey,
JwtString,
Policy
} from '@narval/policy-engine-shared'
import {
Hex,
Jwk,
Payload,
SigningAlg,
base64UrlToHex,
buildSignerEip191,
decode,
hash,
privateKeyToJwk,
publicKeyToJwk,
secp256k1PrivateKeyToJwk,
signJwt,
verifyJwt
} from '@narval/signature'
Expand Down Expand Up @@ -187,9 +186,10 @@ export class OpenPolicyAgentEngine implements Engine<OpenPolicyAgentEngine> {
})
}

const jwk = publicKeyToJwk(credential.pubKey as Hex)
const { key } = credential

const validJwt = await verifyJwt(signature, jwk)
console.log('### credential', credential)
const validJwt = await verifyJwt(signature, key)

if (validJwt.payload.requestHash !== message) {
throw new OpenPolicyAgentException({
Expand Down Expand Up @@ -304,8 +304,8 @@ export class OpenPolicyAgentEngine implements Engine<OpenPolicyAgentEngine> {
}

private async sign(params: { principalCredential: CredentialEntity; message: string }): Promise<JwtString> {
const engineJwk: JsonWebKey = privateKeyToJwk(this.privateKey)
const principalJwk = publicKeyToJwk(params.principalCredential.pubKey as Hex)
const engineJwk: Jwk = secp256k1PrivateKeyToJwk(this.privateKey)
const principalJwk: Jwk = params.principalCredential.key

const payload: Payload = {
requestHash: params.message,
Expand Down
9 changes: 5 additions & 4 deletions apps/policy-engine/src/shared/testing/evaluation.testing.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
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'

export const ONE_ETH = BigInt('1000000000000000000')
Expand Down Expand Up @@ -30,19 +31,19 @@ export const generateInboundEvaluationRequest = async (): Promise<EvaluationRequ
// const aliceSignature = await FIXTURE.ACCOUNT.Alice.signMessage({ message })
const aliceSignature = await signJwt(
payload,
privateKeyToJwk(FIXTURE.UNSAFE_PRIVATE_KEY.Alice),
secp256k1PrivateKeyToJwk(UNSAFE_PRIVATE_KEY.Alice),
{ alg: SigningAlg.EIP191 },
buildSignerEip191(FIXTURE.UNSAFE_PRIVATE_KEY.Alice)
)
const bobSignature = await signJwt(
payload,
privateKeyToJwk(FIXTURE.UNSAFE_PRIVATE_KEY.Bob),
secp256k1PrivateKeyToJwk(UNSAFE_PRIVATE_KEY.Bob),
{ alg: SigningAlg.EIP191 },
buildSignerEip191(FIXTURE.UNSAFE_PRIVATE_KEY.Bob)
)
const carolSignature = await signJwt(
payload,
privateKeyToJwk(FIXTURE.UNSAFE_PRIVATE_KEY.Carol),
secp256k1PrivateKeyToJwk(UNSAFE_PRIVATE_KEY.Carol),
{ alg: SigningAlg.EIP191 },
buildSignerEip191(FIXTURE.UNSAFE_PRIVATE_KEY.Carol)
)
Expand Down
50 changes: 24 additions & 26 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 { Secp256k1PublicKey, secp256k1PrivateKeyToJwk, secp256k1PublicKeySchema } from '@narval/signature'
import { PrivateKeyAccount } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { Action } from './type/action.type'
Expand Down Expand Up @@ -48,6 +48,14 @@ export const UNSAFE_PRIVATE_KEY: Record<Personas, `0x${string}`> = {
Dave: '0x82a0cf4f0fdfd42d93ff328b73bfdbc9c8b4f95f5aedfae82059753fc08a180f'
}

export const PUBLIC_KEYS_JWK: Record<Personas, Secp256k1PublicKey> = {
Root: secp256k1PublicKeySchema.parse(secp256k1PrivateKeyToJwk(UNSAFE_PRIVATE_KEY.Root)),
Alice: secp256k1PublicKeySchema.parse(secp256k1PrivateKeyToJwk(UNSAFE_PRIVATE_KEY.Alice)),
Bob: secp256k1PublicKeySchema.parse(secp256k1PrivateKeyToJwk(UNSAFE_PRIVATE_KEY.Bob)),
Carol: secp256k1PublicKeySchema.parse(secp256k1PrivateKeyToJwk(UNSAFE_PRIVATE_KEY.Carol)),
Dave: secp256k1PublicKeySchema.parse(secp256k1PrivateKeyToJwk(UNSAFE_PRIVATE_KEY.Dave))
}

export const ACCOUNT: Record<Personas, PrivateKeyAccount> = {
Root: privateKeyToAccount(UNSAFE_PRIVATE_KEY.Root),
Alice: privateKeyToAccount(UNSAFE_PRIVATE_KEY.Alice),
Expand Down Expand Up @@ -81,39 +89,29 @@ 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: PUBLIC_KEYS_JWK.Root.kid,
userId: USER.Root.id,
key: PUBLIC_KEYS_JWK.Root
},
Alice: {
id: addressToKid(ACCOUNT.Alice.address),
pubKey: ACCOUNT.Alice.publicKey,
address: ACCOUNT.Alice.address,
alg: Alg.ES256K,
userId: USER.Alice.id
userId: USER.Alice.id,
id: PUBLIC_KEYS_JWK.Alice.kid,
key: PUBLIC_KEYS_JWK.Alice
},
Bob: {
id: addressToKid(ACCOUNT.Bob.address),
pubKey: ACCOUNT.Bob.publicKey,
address: ACCOUNT.Bob.address,
alg: Alg.ES256K,
userId: USER.Bob.id
userId: USER.Bob.id,
id: PUBLIC_KEYS_JWK.Bob.kid,
key: PUBLIC_KEYS_JWK.Bob
},
Carol: {
id: addressToKid(ACCOUNT.Carol.address),
pubKey: ACCOUNT.Carol.publicKey,
address: ACCOUNT.Carol.address,
alg: Alg.ES256K,
userId: USER.Carol.id
userId: USER.Carol.id,
id: PUBLIC_KEYS_JWK.Carol.kid,
key: PUBLIC_KEYS_JWK.Carol
},
Dave: {
id: addressToKid(ACCOUNT.Dave.address),
pubKey: ACCOUNT.Dave.publicKey,
address: ACCOUNT.Dave.address,
alg: Alg.ES256K,
userId: USER.Dave.id
userId: USER.Dave.id,
id: PUBLIC_KEYS_JWK.Dave.kid,
key: PUBLIC_KEYS_JWK.Dave
}
}

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 3f67d59

Please sign in to comment.