Skip to content

Commit

Permalink
Fixing JWK/PrivateKey/PublicKey type schemas; adding tenant-specific …
Browse files Browse the repository at this point in the history
…engineJwk
  • Loading branch information
mattschoch committed Mar 22, 2024
1 parent 444f790 commit fc950e7
Show file tree
Hide file tree
Showing 13 changed files with 99 additions and 61 deletions.
6 changes: 3 additions & 3 deletions apps/devtool/src/app/components/EditorComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import Editor from '@monaco-editor/react'
import { JWK, Payload, SigningAlg, hash, hexToBase64Url, signJwt } from '@narval/signature'
import { Jwk, Payload, SigningAlg, hash, hexToBase64Url, signJwt } from '@narval/signature'
import { getAccount, signMessage } from '@wagmi/core'
import axios from 'axios'
import Image from 'next/image'
Expand Down Expand Up @@ -46,8 +46,8 @@ const EditorComponent = () => {
const address = getAccount(config).address
if (!address) throw new Error('No address connected')

// Need real JWK
const jwk: JWK = {
// Need real Jwk
const jwk: Jwk = {
kty: 'EC',
crv: 'secp256k1',
alg: SigningAlg.ES256K,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import {
} from '@narval/policy-engine-shared'
import {
Hex,
Jwk,
Payload,
PrivateKey,
PublicKey,
SigningAlg,
base64UrlToHex,
buildSignerEip191,
Expand Down Expand Up @@ -304,8 +305,8 @@ export class OpenPolicyAgentEngine implements Engine<OpenPolicyAgentEngine> {
}

private async sign(params: { principalCredential: CredentialEntity; message: string }): Promise<JwtString> {
const engineJwk: Jwk = secp256k1PrivateKeyToJwk(this.privateKey)
const principalJwk: Jwk = params.principalCredential.key
const engineJwk: PrivateKey = secp256k1PrivateKeyToJwk(this.privateKey)
const principalJwk: PublicKey = params.principalCredential.key

const payload: Payload = {
requestHash: params.message,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ArgumentsHost, HttpStatus } from '@nestjs/common'
import { ArgumentsHost, HttpStatus, Logger } from '@nestjs/common'
import { HttpArgumentsHost } from '@nestjs/common/interfaces'
import { ConfigService } from '@nestjs/config'
import { Response } from 'express'
Expand Down Expand Up @@ -46,6 +46,8 @@ describe(ApplicationExceptionFilter.name, () => {
})

describe('catch', () => {
Logger.overrideLogger([])

describe('when environment is production', () => {
it('responds with exception status and short message', () => {
const filter = new ApplicationExceptionFilter(buildConfigServiceMock(Env.PRODUCTION))
Expand Down
20 changes: 13 additions & 7 deletions apps/vault/src/shared/guard/authorization.guard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { JWK, hash, hexToBase64Url, privateKeyToJwk, verifyJwsd, verifyJwt } from '@narval/signature'
import { PublicKey, hash, hexToBase64Url, verifyJwsd, verifyJwt } from '@narval/signature'
import { CanActivate, ExecutionContext, HttpStatus, Injectable } from '@nestjs/common'
import { z } from 'zod'
import { REQUEST_HEADER_CLIENT_ID } from '../../main.constant'
Expand All @@ -9,9 +9,6 @@ const AuthorizationHeaderSchema = z.object({
authorization: z.string()
})

const tenantPublicJWK: JWK = privateKeyToJwk('0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5')
delete tenantPublicJWK.d

@Injectable()
export class AuthorizationGuard implements CanActivate {
constructor(private tenantService: TenantService) {}
Expand All @@ -37,13 +34,22 @@ export class AuthorizationGuard implements CanActivate {
})
}

// const tenant = await this.tenantService.findByClientId(clientId)
const isAuthorized = await this.validateToken(context, accessToken, tenantPublicJWK)
const tenant = await this.tenantService.findByClientId(clientId)
if (!tenant?.engineJwk) {
throw new ApplicationException({
message: 'No engine key configured',
suggestedHttpStatusCode: HttpStatus.UNAUTHORIZED,
context: {
clientId
}
})
}
const isAuthorized = await this.validateToken(context, accessToken, tenant?.engineJwk)

return isAuthorized
}

async validateToken(context: ExecutionContext, token: string, tenantJwk: JWK): Promise<boolean> {
async validateToken(context: ExecutionContext, token: string, tenantJwk: PublicKey): Promise<boolean> {
// 1. Validate the JWT has a valid signature for the expected tenant key
const { payload } = await verifyJwt(token, tenantJwk)
// console.log('Validated', { header, payload })
Expand Down
2 changes: 2 additions & 0 deletions apps/vault/src/shared/schema/tenant.schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { publicKeySchema } from '@narval/signature'
import { z } from 'zod'

export const tenantSchema = z.object({
clientId: z.string(),
clientSecret: z.string(),
engineJwk: publicKeySchema.optional(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date()
})
Expand Down
32 changes: 32 additions & 0 deletions apps/vault/src/tenant/__test__/e2e/tenant.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,38 @@ describe('Tenant', () => {
expect(status).toEqual(HttpStatus.CREATED)
})

it('creates a new tenant with Engine JWK', async () => {
const newPayload: CreateTenantDto = {
clientId: 'tenant-2',
engineJwk: {
kty: 'EC',
crv: 'secp256k1',
alg: 'ES256K',
kid: '0x73d3ed0e92ac09a45d9538980214abb1a36c4943d64ffa53a407683ddf567fc9',
x: 'sxT67JN5KJVnWYyy7xhFNUOk4buvPLrbElHBinuFwmY',
y: 'CzC7IHlsDg9wz-Gqhtc78eC0IEX75upMgrvmS3U6Ad4'
}
}
const { status, body } = await request(app.getHttpServer())
.post('/tenants')
.set(REQUEST_HEADER_API_KEY, adminApiKey)
.send(newPayload)
const actualTenant = await tenantRepository.findByClientId('tenant-2')

expect(body).toMatchObject({
clientId: newPayload.clientId,
clientSecret: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String)
})
expect(body).toEqual({
...actualTenant,
createdAt: actualTenant?.createdAt.toISOString(),
updatedAt: actualTenant?.updatedAt.toISOString()
})
expect(status).toEqual(HttpStatus.CREATED)
})

it('responds with an error when clientId already exist', async () => {
await request(app.getHttpServer()).post('/tenants').set(REQUEST_HEADER_API_KEY, adminApiKey).send(payload)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { publicKeySchema } from '@narval/signature'
import { Body, Controller, Post, UseGuards } from '@nestjs/common'
import { randomBytes } from 'crypto'
import { v4 as uuid } from 'uuid'
Expand All @@ -14,9 +15,11 @@ export class TenantController {
async create(@Body() body: CreateTenantDto) {
const now = new Date()

const engineJwk = body.engineJwk ? publicKeySchema.parse(body.engineJwk) : undefined // Validate the JWK, instead of in DTO
const tenant = await this.tenantService.onboard({
clientId: body.clientId || uuid(),
clientSecret: randomBytes(42).toString('hex'),
engineJwk,
createdAt: now,
updatedAt: now
})
Expand Down
6 changes: 5 additions & 1 deletion apps/vault/src/tenant/http/rest/dto/create-tenant.dto.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Jwk } from '@narval/signature'
import { ApiPropertyOptional } from '@nestjs/swagger'
import { IsString } from 'class-validator'
import { IsOptional, IsString } from 'class-validator'

export class CreateTenantDto {
@IsString()
@ApiPropertyOptional()
clientId?: string

@IsOptional()
engineJwk?: Jwk
}
29 changes: 16 additions & 13 deletions apps/vault/src/vault/__test__/e2e/sign.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { EncryptionModuleOptionProvider } from '@narval/encryption-module'
import {
JWK,
JwsdHeader,
Payload,
SigningAlg,
buildSignerEip191,
hash,
hexToBase64Url,
privateKeyToJwk,
publicKeyToJwk,
secp256k1PrivateKeyToJwk,
secp256k1PublicKeyToJwk,
signJwsd,
signJwt
} from '@narval/signature'
Expand All @@ -34,9 +33,18 @@ describe('Sign', () => {

const adminApiKey = 'test-admin-api-key'
const clientId = uuid()

const PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'
// Engine key used to sign the approval request
const enginePrivateJwk = secp256k1PrivateKeyToJwk(PRIVATE_KEY)
// Engine public key registered w/ the Vault Tenant
// eslint-disable-next-line
const { d, ...tenantPublicJWK } = enginePrivateJwk

const tenant: Tenant = {
clientId,
clientSecret: adminApiKey,
engineJwk: tenantPublicJWK,
createdAt: new Date(),
updatedAt: new Date()
}
Expand All @@ -46,12 +54,6 @@ describe('Sign', () => {
address: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157',
privateKey: '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'
}
const PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'
// Engine key used to sign the approval request
const enginePrivateJwk: JWK = privateKeyToJwk(PRIVATE_KEY)
// Engine public key registered w/ the Vault Tenant
const tenantPublicJWK: JWK = { ...enginePrivateJwk }
delete tenantPublicJWK.d

const defaultRequest = {
action: 'signTransaction',
Expand Down Expand Up @@ -220,7 +222,7 @@ describe('Sign', () => {
it('returns error when auth is client-bound but no jwsd header', async () => {
const bodyPayload = { request: defaultRequest }

const clientJwk = publicKeyToJwk(ACCOUNT.Alice.publicKey)
const clientJwk = secp256k1PublicKeyToJwk(ACCOUNT.Alice.publicKey)
const accessToken = await getAccessToken(defaultRequest, { cnf: clientJwk })

const { status, body } = await request(app.getHttpServer())
Expand All @@ -238,7 +240,7 @@ describe('Sign', () => {
const now = Math.floor(Date.now() / 1000)
const bodyPayload = { request: defaultRequest }

const clientJwk = publicKeyToJwk(ACCOUNT.Alice.publicKey)
const clientJwk = secp256k1PublicKeyToJwk(ACCOUNT.Alice.publicKey)
const accessToken = await getAccessToken(defaultRequest, { cnf: clientJwk })

const jwsdSigner = buildSignerEip191(UNSAFE_PRIVATE_KEY.Alice)
Expand All @@ -257,6 +259,7 @@ describe('Sign', () => {
parts[1] = ''
return parts.join('.')
})

const { status, body } = await request(app.getHttpServer())
.post('/sign')
.set(REQUEST_HEADER_CLIENT_ID, clientId)
Expand All @@ -272,8 +275,8 @@ describe('Sign', () => {
const now = Math.floor(Date.now() / 1000)
const bodyPayload = { request: defaultRequest }

const clientJwk = publicKeyToJwk(ACCOUNT.Alice.publicKey)
const boundClientJwk = publicKeyToJwk(ACCOUNT.Bob.publicKey)
const clientJwk = secp256k1PublicKeyToJwk(ACCOUNT.Alice.publicKey)
const boundClientJwk = secp256k1PublicKeyToJwk(ACCOUNT.Bob.publicKey)
// We bind BOB to the access token, but ALICe is the one signing the request, so she has
// a valid access token but it's not bound to her.
const accessToken = await getAccessToken(defaultRequest, { cnf: boundClientJwk })
Expand Down
8 changes: 4 additions & 4 deletions apps/vault/src/vault/persistence/repository/mock_data.ts
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
20 changes: 4 additions & 16 deletions packages/policy-engine-shared/src/lib/schema/data-store.schema.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
import { jwkSchema } from '@narval/signature'
import { z } from 'zod'
import { entitiesSchema } from './entity.schema'
import { policySchema } from './policy.schema'

export const jsonWebKeySchema = z.object({
kty: z.enum(['EC', 'RSA']).describe('Key Type (e.g. RSA or EC'),
crv: z.enum(['P-256', 'secp256k1']).optional().describe('Curve name'),
kid: z.string().describe('Unique key ID'),
alg: z.enum(['ES256K', 'ES256', 'RS256']).describe('Algorithm'),
use: z.enum(['sig', 'enc']).optional().describe('Public Key Use'),
n: z.string().optional().describe('(RSA) Key modulus'),
e: z.string().optional().describe('(RSA) Key exponent'),
x: z.string().optional().describe('(EC) X Coordinate'),
y: z.string().optional().describe('(EC) Y Coordinate'),
d: z.string().optional().describe('(EC) Private Key')
})

export const dataStoreConfigurationSchema = z.object({
dataUrl: z.string().min(1),
signatureUrl: z.string().min(1),
keys: z.array(jsonWebKeySchema)
keys: z.array(jwkSchema)
})

export const dataStoreSchema = z.object({
Expand All @@ -40,7 +28,7 @@ export const entitySignatureSchema = z.object({

export const entityJsonWebKeysSchema = z.object({
entity: z.object({
keys: z.array(jsonWebKeySchema)
keys: z.array(jwkSchema)
})
})

Expand All @@ -63,7 +51,7 @@ export const policySignatureSchema = z.object({

export const policyJsonWebKeysSchema = z.object({
policy: z.object({
keys: z.array(jsonWebKeySchema)
keys: z.array(jwkSchema)
})
})

Expand Down
3 changes: 0 additions & 3 deletions packages/policy-engine-shared/src/lib/type/data-store.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,12 @@ import {
entityJsonWebKeysSchema,
entitySignatureSchema,
entityStoreSchema,
jsonWebKeySchema,
policyDataSchema,
policyJsonWebKeysSchema,
policySignatureSchema,
policyStoreSchema
} from '../schema/data-store.schema'

export type JsonWebKey = z.infer<typeof jsonWebKeySchema>

export type DataStoreConfiguration = z.infer<typeof dataStoreConfigurationSchema>

export type EntityData = z.infer<typeof entityDataSchema>
Expand Down
20 changes: 10 additions & 10 deletions packages/signature/src/lib/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,14 @@ export const secp256k1KeySchema = z.union([secp256k1PublicKeySchema, secp256k1Pr
const dynamicKeySchema = z.object({}).catchall(z.unknown())

export const jwkSchema = dynamicKeySchema.extend({
kty: z.nativeEnum(KeyTypes).optional(),
crv: z.nativeEnum(Curves).optional(),
alg: z.nativeEnum(Alg).optional(),
use: z.nativeEnum(Use).optional(),
kid: z.string().optional(),
x: z.string().optional(),
y: z.string().optional(),
n: z.string().optional(),
e: z.string().optional(),
d: z.string().optional()
kty: z.nativeEnum(KeyTypes).optional().describe('Key Type (e.g. RSA or EC'),
crv: z.nativeEnum(Curves).optional().describe('Curve name'),
alg: z.nativeEnum(Alg).optional().describe('Algorithm'),
use: z.nativeEnum(Use).optional().describe('Public Key Use'),
kid: z.string().optional().describe('Unique key ID'),
n: z.string().optional().describe('(RSA) Key modulus'),
e: z.string().optional().describe('(RSA) Key exponent'),
x: z.string().optional().describe('(EC) X Coordinate'),
y: z.string().optional().describe('(EC) Y Coordinate'),
d: z.string().optional().describe('(EC) Private Key')
})

0 comments on commit fc950e7

Please sign in to comment.