diff --git a/apps/devtool/src/app/components/EditorComponent.tsx b/apps/devtool/src/app/components/EditorComponent.tsx index 44b737d31..ea443e299 100644 --- a/apps/devtool/src/app/components/EditorComponent.tsx +++ b/apps/devtool/src/app/components/EditorComponent.tsx @@ -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' @@ -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, diff --git a/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts b/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts index 647cdc5be..97e0f1863 100644 --- a/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts +++ b/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts @@ -12,8 +12,9 @@ import { } from '@narval/policy-engine-shared' import { Hex, - Jwk, Payload, + PrivateKey, + PublicKey, SigningAlg, base64UrlToHex, buildSignerEip191, @@ -304,8 +305,8 @@ export class OpenPolicyAgentEngine implements Engine { } private async sign(params: { principalCredential: CredentialEntity; message: string }): Promise { - 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, diff --git a/apps/policy-engine/src/shared/filter/__test__/unit/application-exception.filter.spec.ts b/apps/policy-engine/src/shared/filter/__test__/unit/application-exception.filter.spec.ts index f440e4e79..e27884571 100644 --- a/apps/policy-engine/src/shared/filter/__test__/unit/application-exception.filter.spec.ts +++ b/apps/policy-engine/src/shared/filter/__test__/unit/application-exception.filter.spec.ts @@ -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' @@ -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)) diff --git a/apps/vault/src/shared/guard/authorization.guard.ts b/apps/vault/src/shared/guard/authorization.guard.ts index 39b17d45f..0af22d185 100644 --- a/apps/vault/src/shared/guard/authorization.guard.ts +++ b/apps/vault/src/shared/guard/authorization.guard.ts @@ -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' @@ -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) {} @@ -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 { + async validateToken(context: ExecutionContext, token: string, tenantJwk: PublicKey): Promise { // 1. Validate the JWT has a valid signature for the expected tenant key const { payload } = await verifyJwt(token, tenantJwk) // console.log('Validated', { header, payload }) diff --git a/apps/vault/src/shared/schema/tenant.schema.ts b/apps/vault/src/shared/schema/tenant.schema.ts index c9e1098a2..00c641913 100644 --- a/apps/vault/src/shared/schema/tenant.schema.ts +++ b/apps/vault/src/shared/schema/tenant.schema.ts @@ -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() }) diff --git a/apps/vault/src/tenant/__test__/e2e/tenant.spec.ts b/apps/vault/src/tenant/__test__/e2e/tenant.spec.ts index c5805a1c7..545ecf166 100644 --- a/apps/vault/src/tenant/__test__/e2e/tenant.spec.ts +++ b/apps/vault/src/tenant/__test__/e2e/tenant.spec.ts @@ -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) diff --git a/apps/vault/src/tenant/http/rest/controller/tenant.controller.ts b/apps/vault/src/tenant/http/rest/controller/tenant.controller.ts index a5e223884..203662771 100644 --- a/apps/vault/src/tenant/http/rest/controller/tenant.controller.ts +++ b/apps/vault/src/tenant/http/rest/controller/tenant.controller.ts @@ -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' @@ -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 }) diff --git a/apps/vault/src/tenant/http/rest/dto/create-tenant.dto.ts b/apps/vault/src/tenant/http/rest/dto/create-tenant.dto.ts index d545788f1..6a6448411 100644 --- a/apps/vault/src/tenant/http/rest/dto/create-tenant.dto.ts +++ b/apps/vault/src/tenant/http/rest/dto/create-tenant.dto.ts @@ -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 } diff --git a/apps/vault/src/vault/__test__/e2e/sign.spec.ts b/apps/vault/src/vault/__test__/e2e/sign.spec.ts index 1f86d3249..4df25fffa 100644 --- a/apps/vault/src/vault/__test__/e2e/sign.spec.ts +++ b/apps/vault/src/vault/__test__/e2e/sign.spec.ts @@ -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' @@ -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() } @@ -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', @@ -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()) @@ -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) @@ -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) @@ -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 }) diff --git a/apps/vault/src/vault/persistence/repository/mock_data.ts b/apps/vault/src/vault/persistence/repository/mock_data.ts index b8b2aecce..74a260d7e 100644 --- a/apps/vault/src/vault/persistence/repository/mock_data.ts +++ b/apps/vault/src/vault/persistence/repository/mock_data.ts @@ -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' @@ -31,19 +31,19 @@ export const generateInboundRequest = async (): Promise => { // 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) ) diff --git a/packages/policy-engine-shared/src/lib/schema/data-store.schema.ts b/packages/policy-engine-shared/src/lib/schema/data-store.schema.ts index 420d5d428..cb77a8dd9 100644 --- a/packages/policy-engine-shared/src/lib/schema/data-store.schema.ts +++ b/packages/policy-engine-shared/src/lib/schema/data-store.schema.ts @@ -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({ @@ -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) }) }) @@ -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) }) }) diff --git a/packages/policy-engine-shared/src/lib/type/data-store.type.ts b/packages/policy-engine-shared/src/lib/type/data-store.type.ts index 8d37fe2e3..e12b7daf0 100644 --- a/packages/policy-engine-shared/src/lib/type/data-store.type.ts +++ b/packages/policy-engine-shared/src/lib/type/data-store.type.ts @@ -5,15 +5,12 @@ import { entityJsonWebKeysSchema, entitySignatureSchema, entityStoreSchema, - jsonWebKeySchema, policyDataSchema, policyJsonWebKeysSchema, policySignatureSchema, policyStoreSchema } from '../schema/data-store.schema' -export type JsonWebKey = z.infer - export type DataStoreConfiguration = z.infer export type EntityData = z.infer diff --git a/packages/signature/src/lib/schemas.ts b/packages/signature/src/lib/schemas.ts index 6e0b5206d..eb23e5dcf 100644 --- a/packages/signature/src/lib/schemas.ts +++ b/packages/signature/src/lib/schemas.ts @@ -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') })