diff --git a/.github/workflows/signature.yml b/.github/workflows/signature.yml
new file mode 100644
index 000000000..d57747d6c
--- /dev/null
+++ b/.github/workflows/signature.yml
@@ -0,0 +1,64 @@
+name: '@narval/signature CI'
+
+on:
+ push:
+ paths:
+ - packages/signature/**
+ - .github/workflows/signature.yml
+ - jest.config.ts
+ - jest.preset.js
+ - .eslintrc.json
+ - .prettierrc
+ - package.json
+ - package-lock.json
+
+jobs:
+ build-and-test:
+ name: Build and test
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@master
+
+ - name: Install Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: '20.4.0'
+
+ - name: Install dependencies
+ run: |
+ make install/ci
+
+ - name: Code format
+ shell: bash
+ run: |
+ make signature/format/check
+ make signature/lint/check
+
+ - name: Test types
+ shell: bash
+ run: |
+ make signature/test/type
+
+ - name: Test upstream application types
+ shell: bash
+ run: |
+ make policy-engine/test/type
+
+ - name: Test unit
+ shell: bash
+ run: |
+ make signature/test/unit
+
+ - name: Send Slack notification on failure
+ if: failure() && github.ref == 'refs/heads/main'
+ uses: 8398a7/action-slack@v3
+ with:
+ username: GitHub
+ author_name: '@narval/signature CI failed'
+ status: ${{ job.status }}
+ fields: message,commit,author
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
diff --git a/README.md b/README.md
index 279fb27c6..05e008f26 100644
--- a/README.md
+++ b/README.md
@@ -9,9 +9,11 @@
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [@app/armory](./apps/armory/README.md) | |
| [@app/policy-engine](./apps/policy-engine/README.md) | |
-| [@narval/encryption-module](./packages/encryption-module/README.md) | N/A |
+| [@narval/encryption](./packages/encryption/README.md) | |
+ |
| [@narval/policy-engine-shared](./packages/policy-engine-shared/README.md) | |
-| [@narval/signature](./packages/signature/README.md) | N/A |
+| [@narval/signature](./packages/signature/README.md) | |
+ |
| [@narval/transaction-request-intent](./packages/transaction-request-intent/README.md) | |
## Getting started
diff --git a/apps/armory/src/data-feed/core/service/historical-transfer-feed.service.ts b/apps/armory/src/data-feed/core/service/historical-transfer-feed.service.ts
index 3d3813e1b..56e05a17d 100644
--- a/apps/armory/src/data-feed/core/service/historical-transfer-feed.service.ts
+++ b/apps/armory/src/data-feed/core/service/historical-transfer-feed.service.ts
@@ -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'
@@ -37,7 +37,7 @@ export class HistoricalTransferFeedService implements DataFeed {
}
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,
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/engine/app.controller.ts b/apps/policy-engine/src/engine/app.controller.ts
index 449a55923..a338cb530 100644
--- a/apps/policy-engine/src/engine/app.controller.ts
+++ b/apps/policy-engine/src/engine/app.controller.ts
@@ -30,7 +30,12 @@ export class AppController {
body
})
- return this.evaluationService.evaluate(FIXTURE.ORGANIZATION.id, body)
+ const result = await this.evaluationService.evaluate(FIXTURE.ORGANIZATION.id, body)
+
+ this.logger.log({
+ message: 'Evaluation result',
+ body: result
+ })
}
@Post('/evaluation-demo')
diff --git a/apps/policy-engine/src/engine/core/service/signing.service.ts b/apps/policy-engine/src/engine/core/service/signing.service.ts
index 8728ac5c5..518de268a 100644
--- a/apps/policy-engine/src/engine/core/service/signing.service.ts
+++ b/apps/policy-engine/src/engine/core/service/signing.service.ts
@@ -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'
@@ -17,8 +19,8 @@ type KeyGenerationOptions = {
}
type KeyGenerationResponse = {
- publicKey: JsonWebKey
- privateKey?: JsonWebKey
+ publicKey: PublicKey
+ privateKey?: PrivateKey
}
type SignOptions = {
@@ -32,7 +34,7 @@ export class SigningService {
async generateSigningKey(alg: Alg, options?: KeyGenerationOptions): Promise {
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 = {
@@ -49,21 +51,14 @@ export class SigningService {
throw new Error('Unsupported algorithm')
}
- async sign(payload: Payload, jwk: JsonWebKey, opts: SignOptions = {}): Promise {
+ async sign(payload: Payload, jwk: PrivateKey, opts: SignOptions = {}): Promise {
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))
diff --git a/apps/policy-engine/src/engine/core/service/tenant.service.ts b/apps/policy-engine/src/engine/core/service/tenant.service.ts
index 7dcf38027..15a3bdf57 100644
--- a/apps/policy-engine/src/engine/core/service/tenant.service.ts
+++ b/apps/policy-engine/src/engine/core/service/tenant.service.ts
@@ -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
}
diff --git a/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts b/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts
index 50e758932..c12a12b9f 100644
--- a/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts
+++ b/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts
@@ -13,7 +13,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 { Path, PathValue } from '@nestjs/config'
import { Test, TestingModule } from '@nestjs/testing'
import { Config, load } from '../../../../policy-engine.config'
@@ -26,7 +26,7 @@ const ONE_ETH = toHex(BigInt('1000000000000000000'))
const UNSAFE_ENGINE_PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'
const getJwt = (option: { privateKey: Hex; request: Request; sub: string }): Promise => {
- const jwk = privateKeyToJwk(option.privateKey)
+ const jwk = secp256k1PrivateKeyToJwk(option.privateKey)
const signer = buildSignerEip191(option.privateKey)
return signJwt(
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 8a985b33f..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
@@ -7,20 +7,20 @@ import {
Entities,
EvaluationRequest,
EvaluationResponse,
- JsonWebKey,
JwtString,
Policy
} from '@narval/policy-engine-shared'
import {
Hex,
Payload,
+ PrivateKey,
+ PublicKey,
SigningAlg,
base64UrlToHex,
buildSignerEip191,
decode,
hash,
- privateKeyToJwk,
- publicKeyToJwk,
+ secp256k1PrivateKeyToJwk,
signJwt,
verifyJwt
} from '@narval/signature'
@@ -187,9 +187,10 @@ export class OpenPolicyAgentEngine implements Engine {
})
}
- 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({
@@ -304,8 +305,8 @@ export class OpenPolicyAgentEngine implements Engine {
}
private async sign(params: { principalCredential: CredentialEntity; message: string }): Promise {
- const engineJwk: JsonWebKey = privateKeyToJwk(this.privateKey)
- const principalJwk = publicKeyToJwk(params.principalCredential.pubKey as Hex)
+ 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/resource/open-policy-agent/rego/__test__/criteria/principal_test.rego b/apps/policy-engine/src/resource/open-policy-agent/rego/__test__/criteria/principal_test.rego
index d25bc974d..816adda3b 100644
--- a/apps/policy-engine/src/resource/open-policy-agent/rego/__test__/criteria/principal_test.rego
+++ b/apps/policy-engine/src/resource/open-policy-agent/rego/__test__/criteria/principal_test.rego
@@ -4,7 +4,7 @@ test_principal {
user = principal with input as request
with data.entities as entities
- user == {"uid": "test-bob-uid", "role": "root"}
+ user == {"id": "test-bob-uid", "role": "root"}
groups = principalGroups with input as request
with data.entities as entities
diff --git a/apps/policy-engine/src/resource/open-policy-agent/rego/__test__/main_test.rego b/apps/policy-engine/src/resource/open-policy-agent/rego/__test__/main_test.rego
index 8b5240749..8d6760677 100644
--- a/apps/policy-engine/src/resource/open-policy-agent/rego/__test__/main_test.rego
+++ b/apps/policy-engine/src/resource/open-policy-agent/rego/__test__/main_test.rego
@@ -115,23 +115,23 @@ request = {
entities = {
"users": {
"test-bob-uid": {
- "uid": "test-bob-uid",
+ "id": "test-bob-uid",
"role": "root",
},
"test-alice-uid": {
- "uid": "test-alice-uid",
+ "id": "test-alice-uid",
"role": "member",
},
"test-bar-uid": {
- "uid": "test-bar-uid",
+ "id": "test-bar-uid",
"role": "admin",
},
"test-foo-uid": {
- "uid": "test-foo-uid",
+ "id": "test-foo-uid",
"role": "admin",
},
"0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43": {
- "uid": "0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43",
+ "id": "0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43",
"role": "admin",
},
},
diff --git a/apps/policy-engine/src/resource/open-policy-agent/rego/__test__/policies/e2e_test.rego b/apps/policy-engine/src/resource/open-policy-agent/rego/__test__/policies/e2e_test.rego
index 28d2e315b..771c15485 100644
--- a/apps/policy-engine/src/resource/open-policy-agent/rego/__test__/policies/e2e_test.rego
+++ b/apps/policy-engine/src/resource/open-policy-agent/rego/__test__/policies/e2e_test.rego
@@ -120,10 +120,10 @@ e2e_req = {
e2e_entities = {
"users": {
- "u:root_user": {"uid": "u:root_user", "role": "root"},
- "matt@narval.xyz": {"uid": "matt@narval.xyz", "role": "admin"},
- "aa@narval.xyz": {"uid": "aa@narval.xyz", "role": "admin"},
- "bb@narval.xyz": {"uid": "bb@narval.xyz", "role": "admin"},
+ "u:root_user": {"id": "u:root_user", "role": "root"},
+ "matt@narval.xyz": {"id": "matt@narval.xyz", "role": "admin"},
+ "aa@narval.xyz": {"id": "aa@narval.xyz", "role": "admin"},
+ "bb@narval.xyz": {"id": "bb@narval.xyz", "role": "admin"},
},
"userGroups": {
"ug:dev-group": {"uid": "ug:dev-group", "name": "Dev", "users": ["matt@narval.xyz"]},
diff --git a/apps/policy-engine/src/resource/open-policy-agent/rego/criteria/approval.rego b/apps/policy-engine/src/resource/open-policy-agent/rego/criteria/approval.rego
index 110e1a5a7..edf9fe031 100644
--- a/apps/policy-engine/src/resource/open-policy-agent/rego/criteria/approval.rego
+++ b/apps/policy-engine/src/resource/open-policy-agent/rego/criteria/approval.rego
@@ -30,7 +30,7 @@ getApprovalsCount(possibleApprovers) = result {
checkApproval(approval) = result {
approval.countPrincipal == true
approval.approvalEntityType == "Narval::User"
- possibleApprovers = {entity | entity = approval.entityIds[_]} | {principal.uid}
+ possibleApprovers = {entity | entity = approval.entityIds[_]} | {principal.id}
result = getApprovalsCount(possibleApprovers)
}
@@ -39,7 +39,7 @@ checkApproval(approval) = result {
approval.approvalEntityType == "Narval::User"
possibleApprovers = {entity |
entity = approval.entityIds[_]
- entity != principal.uid
+ entity != principal.id
}
result = getApprovalsCount(possibleApprovers)
}
@@ -53,7 +53,7 @@ checkApproval(approval) = result {
entity = approval.entityIds[_]
users = userGroupsEntities[entity].users
user = users[_]
- } | {principal.uid}
+ } | {principal.id}
result = getApprovalsCount(possibleApprovers)
}
@@ -65,7 +65,7 @@ checkApproval(approval) = result {
entity = approval.entityIds[_]
users = userGroupsEntities[entity].users
user = users[_]
- user != principal.uid
+ user != principal.id
}
result = getApprovalsCount(possibleApprovers)
@@ -76,10 +76,10 @@ checkApproval(approval) = result {
checkApproval(approval) = result {
approval.countPrincipal == true
approval.approvalEntityType == "Narval::UserRole"
- possibleApprovers = {user.uid |
+ possibleApprovers = {user.id |
user = usersEntities[_]
user.role in approval.entityIds
- } | {principal.uid}
+ } | {principal.id}
result = getApprovalsCount(possibleApprovers)
}
@@ -87,10 +87,10 @@ checkApproval(approval) = result {
checkApproval(approval) = result {
approval.countPrincipal == false
approval.approvalEntityType == "Narval::UserRole"
- possibleApprovers = {user.uid |
+ possibleApprovers = {user.id |
user = usersEntities[_]
user.role in approval.entityIds
- user.uid != principal.uid
+ user.id != principal.id
}
result = getApprovalsCount(possibleApprovers)
diff --git a/apps/policy-engine/src/resource/open-policy-agent/rego/criteria/principal.rego b/apps/policy-engine/src/resource/open-policy-agent/rego/criteria/principal.rego
index 8deba84d2..401d463c2 100644
--- a/apps/policy-engine/src/resource/open-policy-agent/rego/criteria/principal.rego
+++ b/apps/policy-engine/src/resource/open-policy-agent/rego/criteria/principal.rego
@@ -11,14 +11,14 @@ principalGroups = {group.uid |
isPrincipalRootUser = principal.role == "root"
-isPrincipalAssignedToWallet = principal.uid in resource.assignees
+isPrincipalAssignedToWallet = principal.id in resource.assignees
checkPrincipal {
not isPrincipalRootUser
isPrincipalAssignedToWallet
}
-checkPrincipalId(values) = principal.uid in values
+checkPrincipalId(values) = principal.id in values
checkPrincipalRole(values) = principal.role in values
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 5ab241ab3..069b34d03 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,5 +1,5 @@
import { ConfigService } from '@narval/config-module'
-import { ArgumentsHost, HttpStatus } from '@nestjs/common'
+import { ArgumentsHost, HttpStatus, Logger } from '@nestjs/common'
import { HttpArgumentsHost } from '@nestjs/common/interfaces'
import { Response } from 'express'
import { mock } from 'jest-mock-extended'
@@ -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/policy-engine/src/shared/testing/evaluation.testing.ts b/apps/policy-engine/src/shared/testing/evaluation.testing.ts
index ffbd599e2..a0a268621 100644
--- a/apps/policy-engine/src/shared/testing/evaluation.testing.ts
+++ b/apps/policy-engine/src/shared/testing/evaluation.testing.ts
@@ -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')
@@ -30,19 +31,19 @@ export const generateInboundEvaluationRequest = async (): Promise {
+ const exception = new ApplicationException({
+ message: 'Test application exception filter',
+ suggestedHttpStatusCode: HttpStatus.INTERNAL_SERVER_ERROR,
+ context: {
+ additional: 'information',
+ to: 'debug'
+ }
+ })
+
+ const buildArgumentsHostMock = (): [ArgumentsHost, jest.Mock, jest.Mock] => {
+ const jsonMock = jest.fn()
+ const statusMock = jest.fn().mockReturnValue(
+ mock({
+ json: jsonMock
+ })
+ )
+
+ const host = mock({
+ switchToHttp: jest.fn().mockReturnValue(
+ mock({
+ getResponse: jest.fn().mockReturnValue(
+ mock({
+ status: statusMock
+ })
+ )
+ })
+ )
+ })
+
+ return [host, statusMock, jsonMock]
+ }
+
+ const buildConfigServiceMock = (env: Env) =>
+ mock>({
+ get: jest.fn().mockReturnValue(env)
+ })
+
+ describe('catch', () => {
+ describe('when environment is production', () => {
+ it('responds with exception status and short message', () => {
+ const filter = new ApplicationExceptionFilter(buildConfigServiceMock(Env.PRODUCTION))
+ const [host, statusMock, jsonMock] = buildArgumentsHostMock()
+
+ filter.catch(exception, host)
+
+ expect(statusMock).toHaveBeenCalledWith(exception.getStatus())
+ expect(jsonMock).toHaveBeenCalledWith({
+ statusCode: exception.getStatus(),
+ message: exception.message,
+ context: exception.context
+ })
+ })
+ })
+
+ describe('when environment is not production', () => {
+ it('responds with exception status and complete message', () => {
+ const filter = new ApplicationExceptionFilter(buildConfigServiceMock(Env.DEVELOPMENT))
+ const [host, statusMock, jsonMock] = buildArgumentsHostMock()
+
+ filter.catch(exception, host)
+
+ expect(statusMock).toHaveBeenCalledWith(exception.getStatus())
+ expect(jsonMock).toHaveBeenCalledWith({
+ statusCode: exception.getStatus(),
+ message: exception.message,
+ context: exception.context,
+ stack: exception.stack
+ })
+ })
+ })
+ })
+})
diff --git a/apps/vault/src/shared/filter/application-exception.filter.ts b/apps/vault/src/shared/filter/application-exception.filter.ts
new file mode 100644
index 000000000..4fc0ea45b
--- /dev/null
+++ b/apps/vault/src/shared/filter/application-exception.filter.ts
@@ -0,0 +1,52 @@
+import { ArgumentsHost, Catch, ExceptionFilter, LogLevel, Logger } from '@nestjs/common'
+import { ConfigService } from '@nestjs/config'
+import { Response } from 'express'
+import { Config, Env } from '../../main.config'
+import { ApplicationException } from '../../shared/exception/application.exception'
+
+@Catch(ApplicationException)
+export class ApplicationExceptionFilter implements ExceptionFilter {
+ private logger = new Logger(ApplicationExceptionFilter.name)
+
+ constructor(private configService: ConfigService) {}
+
+ catch(exception: ApplicationException, host: ArgumentsHost) {
+ const ctx = host.switchToHttp()
+ const response = ctx.getResponse()
+ const status = exception.getStatus()
+ const isProduction = this.configService.get('env') === Env.PRODUCTION
+
+ this.log(exception)
+
+ response.status(status).json(
+ isProduction
+ ? {
+ statusCode: status,
+ message: exception.message,
+ context: exception.context
+ }
+ : {
+ statusCode: status,
+ message: exception.message,
+ context: exception.context,
+ stack: exception.stack,
+ ...(exception.origin && { origin: exception.origin })
+ }
+ )
+ }
+
+ // TODO (@wcalderipe, 16/01/24): Unit test the logging logic. For that, we
+ // must inject the logger in the constructor via dependency injection.
+ private log(exception: ApplicationException) {
+ const level: LogLevel = exception.getStatus() >= 500 ? 'error' : 'warn'
+
+ if (this.logger[level]) {
+ this.logger[level](exception.message, {
+ status: exception.getStatus(),
+ context: exception.context,
+ stacktrace: exception.stack,
+ origin: exception.origin
+ })
+ }
+ }
+}
diff --git a/apps/vault/src/shared/filter/zod-exception.filter.ts b/apps/vault/src/shared/filter/zod-exception.filter.ts
new file mode 100644
index 000000000..aa9aff7f4
--- /dev/null
+++ b/apps/vault/src/shared/filter/zod-exception.filter.ts
@@ -0,0 +1,39 @@
+import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, Logger } from '@nestjs/common'
+import { ConfigService } from '@nestjs/config'
+import { Response } from 'express'
+import { ZodError } from 'zod'
+import { Config, Env } from '../../main.config'
+
+@Catch(ZodError)
+export class ZodExceptionFilter implements ExceptionFilter {
+ private logger = new Logger(ZodExceptionFilter.name)
+
+ constructor(private configService: ConfigService) {}
+
+ catch(exception: ZodError, host: ArgumentsHost) {
+ const ctx = host.switchToHttp()
+ const response = ctx.getResponse()
+ const status = HttpStatus.UNPROCESSABLE_ENTITY
+ const isProduction = this.configService.get('env') === Env.PRODUCTION
+
+ // Log as error level because Zod issues should be handled by the caller.
+ this.logger.error('Uncaught ZodError', {
+ exception
+ })
+
+ response.status(status).json(
+ isProduction
+ ? {
+ statusCode: status,
+ message: 'Internal validation error',
+ context: exception.errors
+ }
+ : {
+ statusCode: status,
+ message: 'Internal validation error',
+ context: exception.errors,
+ stacktrace: exception.stack
+ }
+ )
+ }
+}
diff --git a/apps/vault/src/shared/guard/authorization.guard.ts b/apps/vault/src/shared/guard/authorization.guard.ts
new file mode 100644
index 000000000..0af22d185
--- /dev/null
+++ b/apps/vault/src/shared/guard/authorization.guard.ts
@@ -0,0 +1,117 @@
+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'
+import { TenantService } from '../../tenant/core/service/tenant.service'
+import { ApplicationException } from '../exception/application.exception'
+
+const AuthorizationHeaderSchema = z.object({
+ authorization: z.string()
+})
+
+@Injectable()
+export class AuthorizationGuard implements CanActivate {
+ constructor(private tenantService: TenantService) {}
+
+ async canActivate(context: ExecutionContext): Promise {
+ const req = context.switchToHttp().getRequest()
+ const clientId = req.headers[REQUEST_HEADER_CLIENT_ID]
+ const headers = AuthorizationHeaderSchema.parse(req.headers)
+ // Expect the header in the format "GNAP "
+ const accessToken: string | undefined = headers.authorization.split('GNAP ')[1]
+
+ if (!accessToken) {
+ throw new ApplicationException({
+ message: `Missing or invalid Access Token in Authorization header`,
+ suggestedHttpStatusCode: HttpStatus.UNAUTHORIZED
+ })
+ }
+
+ if (!clientId) {
+ throw new ApplicationException({
+ message: `Missing or invalid ${REQUEST_HEADER_CLIENT_ID} header`,
+ suggestedHttpStatusCode: HttpStatus.UNAUTHORIZED
+ })
+ }
+
+ 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: 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 })
+
+ // 2. Validate the TX Request sent is the same as the one in the JWT
+ const req = context.switchToHttp().getRequest()
+ const request = req.body.request
+ const verificationMsg = hash(request)
+ const requestMatches = payload.requestHash === verificationMsg
+ if (!requestMatches) {
+ throw new ApplicationException({
+ message: `Request payload does not match the authorized request`,
+ suggestedHttpStatusCode: HttpStatus.FORBIDDEN
+ })
+ }
+
+ // 3. Validate that the JWT has all the corerct properties, claims, etc.
+ // This belongs in the signature lib, but needs to accept a options obj like jose does.
+ // We probs have to roll our own simply so we can support EIP191
+ // const v = await jwtVerify(token, await importJWK(tenantJwk))
+ // console.log('JWT Verified', v)
+
+ // We want to also check the client key in cnf so we can optionally do bound requests
+ if (payload.cnf) {
+ const boundKey = payload.cnf
+ const jwsdHeader = req.headers['detached-jws']
+ if (!jwsdHeader) {
+ throw new ApplicationException({
+ message: `Missing detached-jws header`,
+ suggestedHttpStatusCode: HttpStatus.FORBIDDEN
+ })
+ }
+
+ const parts = jwsdHeader.split('.')
+ const deepCopyBody = JSON.parse(JSON.stringify(req.body))
+ // This is the GNAP spec; base64URL the sha256 of the whole request body (not just our tx body.request part)
+ const jwsdPayload = hexToBase64Url(`0x${hash(deepCopyBody)}`)
+ // Replace the payload part; this lets the JWT be compacted with `header..signature` to be shorter.
+ parts[1] = jwsdPayload
+ const jwsdToVerify = parts.join('.')
+ // Will throw if not valid
+ try {
+ const decodedJwsd = await verifyJwsd(jwsdToVerify, boundKey)
+ // Verify the ATH matches our accessToken
+ const tokenHash = hexToBase64Url(`0x${hash(token)}`)
+ if (decodedJwsd.header.ath !== tokenHash) {
+ throw new ApplicationException({
+ message: `Request ath does not match the access token`,
+ suggestedHttpStatusCode: HttpStatus.FORBIDDEN
+ })
+ }
+ } catch (err) {
+ throw new ApplicationException({
+ message: err.message,
+ suggestedHttpStatusCode: HttpStatus.FORBIDDEN
+ })
+ }
+ // TODO: verify the request URI & such in the jwsd header
+ }
+
+ // Then we sign.
+
+ return true
+ }
+}
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 d455d40b0..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)
@@ -103,10 +135,8 @@ describe('Tenant', () => {
.set(REQUEST_HEADER_API_KEY, adminApiKey)
.send(payload)
- expect(body).toEqual({
- message: 'Tenant already exist',
- statusCode: HttpStatus.BAD_REQUEST
- })
+ expect(body.statusCode).toEqual(HttpStatus.BAD_REQUEST)
+ expect(body.message).toEqual('Tenant already exist')
expect(status).toEqual(HttpStatus.BAD_REQUEST)
})
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/tenant/tenant.module.ts b/apps/vault/src/tenant/tenant.module.ts
index 92e1002d5..b532b6372 100644
--- a/apps/vault/src/tenant/tenant.module.ts
+++ b/apps/vault/src/tenant/tenant.module.ts
@@ -1,5 +1,5 @@
import { HttpModule } from '@nestjs/axios'
-import { Module, OnApplicationBootstrap, ValidationPipe } from '@nestjs/common'
+import { Module, OnApplicationBootstrap, ValidationPipe, forwardRef } from '@nestjs/common'
import { APP_PIPE } from '@nestjs/core'
import { AdminApiKeyGuard } from '../shared/guard/admin-api-key.guard'
import { KeyValueModule } from '../shared/module/key-value/key-value.module'
@@ -11,7 +11,7 @@ import { TenantRepository } from './persistence/repository/tenant.repository'
@Module({
// NOTE: The AdminApiKeyGuard is the only reason we need the VaultModule.
- imports: [HttpModule, KeyValueModule, VaultModule],
+ imports: [HttpModule, KeyValueModule, forwardRef(() => VaultModule)],
controllers: [TenantController],
providers: [
AdminApiKeyGuard,
diff --git a/apps/vault/src/vault/__test__/e2e/sign.spec.ts b/apps/vault/src/vault/__test__/e2e/sign.spec.ts
index 983d125af..4df25fffa 100644
--- a/apps/vault/src/vault/__test__/e2e/sign.spec.ts
+++ b/apps/vault/src/vault/__test__/e2e/sign.spec.ts
@@ -1,11 +1,24 @@
import { EncryptionModuleOptionProvider } from '@narval/encryption-module'
+import {
+ JwsdHeader,
+ Payload,
+ SigningAlg,
+ buildSignerEip191,
+ hash,
+ hexToBase64Url,
+ secp256k1PrivateKeyToJwk,
+ secp256k1PublicKeyToJwk,
+ signJwsd,
+ signJwt
+} from '@narval/signature'
import { HttpStatus, INestApplication } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { Test, TestingModule } from '@nestjs/testing'
+import { ACCOUNT, UNSAFE_PRIVATE_KEY } from 'packages/policy-engine-shared/src/lib/dev.fixture'
import request from 'supertest'
import { v4 as uuid } from 'uuid'
import { load } from '../../../main.config'
-import { REQUEST_HEADER_API_KEY, REQUEST_HEADER_CLIENT_ID } from '../../../main.constant'
+import { REQUEST_HEADER_CLIENT_ID } from '../../../main.constant'
import { KeyValueRepository } from '../../../shared/module/key-value/core/repository/key-value.repository'
import { InMemoryKeyValueRepository } from '../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository'
import { getTestRawAesKeyring } from '../../../shared/testing/encryption.testing'
@@ -20,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()
}
@@ -33,6 +55,39 @@ describe('Sign', () => {
privateKey: '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'
}
+ const defaultRequest = {
+ action: 'signTransaction',
+ nonce: 'random-nonce-111',
+ transactionRequest: {
+ from: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157',
+ to: '0x04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B',
+ chainId: 137,
+ value: '0x5af3107a4000',
+ data: '0x',
+ nonce: 317,
+ type: '2',
+ gas: '21004',
+ maxFeePerGas: '291175227375',
+ maxPriorityFeePerGas: '81000000000'
+ },
+ resourceId: 'eip155:eoa:0xc3bdcdb4f593aa5a5d81cd425f6fc3265d962157'
+ }
+
+ const getAccessToken = async (request?: unknown, opts: object = {}) => {
+ const payload: Payload = {
+ requestHash: hash(request || defaultRequest),
+ sub: 'test-root-user-uid',
+ iss: 'https://armory.narval.xyz',
+ iat: Math.floor(Date.now() / 1000),
+ ...opts
+ }
+
+ const signer = buildSignerEip191(PRIVATE_KEY)
+ const jwt = await signJwt(payload, enginePrivateJwk, { alg: SigningAlg.EIP191 }, signer)
+
+ return jwt
+ }
+
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
@@ -60,7 +115,7 @@ describe('Sign', () => {
})
.compile()
- app = module.createNestApplication()
+ app = module.createNestApplication({ logger: false })
await app.init()
})
@@ -75,11 +130,10 @@ describe('Sign', () => {
it('has client secret guard', async () => {
const { status } = await request(app.getHttpServer())
.post('/sign')
- .set(REQUEST_HEADER_API_KEY, adminApiKey)
// .set(REQUEST_HEADER_CLIENT_ID, clientId) NO CLIENT SECRET
.send({})
- expect(status).toEqual(HttpStatus.UNAUTHORIZED)
+ expect(status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY)
})
it('validates nested txn data', async () => {
@@ -105,10 +159,12 @@ describe('Sign', () => {
}
}
+ const accessToken = await getAccessToken(payload.request)
+
const { status, body } = await request(app.getHttpServer())
.post('/sign')
- .set(REQUEST_HEADER_API_KEY, adminApiKey)
.set(REQUEST_HEADER_CLIENT_ID, clientId)
+ .set('authorization', `GNAP ${accessToken}`)
.send(payload)
expect(status).toEqual(HttpStatus.BAD_REQUEST)
@@ -121,31 +177,15 @@ describe('Sign', () => {
})
it('signs', async () => {
- const payload = {
- request: {
- action: 'signTransaction',
- nonce: 'random-nonce-111',
- transactionRequest: {
- from: '0xc3bdcdb4F593AA5A5D81cD425f6Fc3265D962157',
- to: '0x04B12F0863b83c7162429f0Ebb0DfdA20E1aA97B',
- chainId: 137,
- value: '0x5af3107a4000',
- data: '0x',
- nonce: 317,
- type: '2',
- gas: '21004',
- maxFeePerGas: '291175227375',
- maxPriorityFeePerGas: '81000000000'
- },
- resourceId: 'eip155:eoa:0xc3bdcdb4f593aa5a5d81cd425f6fc3265d962157'
- }
- }
+ const bodyPayload = { request: defaultRequest }
+
+ const accessToken = await getAccessToken()
const { status, body } = await request(app.getHttpServer())
.post('/sign')
- .set(REQUEST_HEADER_API_KEY, adminApiKey)
.set(REQUEST_HEADER_CLIENT_ID, clientId)
- .send(payload)
+ .set('authorization', `GNAP ${accessToken}`)
+ .send(bodyPayload)
expect(status).toEqual(HttpStatus.CREATED)
@@ -155,4 +195,121 @@ describe('Sign', () => {
})
})
})
+
+ describe('AuthorizationGuard', () => {
+ it('returns error when request does not match authorized request', async () => {
+ const bodyPayload = {
+ request: {
+ ...defaultRequest,
+ nonce: defaultRequest.nonce + 'x' // CHANGE THE NONCE SO IT DOES NOT MATCH ACCESS TOKEN
+ }
+ }
+
+ const accessToken = await getAccessToken(defaultRequest)
+
+ const { status, body } = await request(app.getHttpServer())
+ .post('/sign')
+ .set(REQUEST_HEADER_CLIENT_ID, clientId)
+ .set('authorization', `GNAP ${accessToken}`)
+ .send(bodyPayload)
+
+ expect(status).toEqual(HttpStatus.FORBIDDEN)
+ expect(body.statusCode).toEqual(HttpStatus.FORBIDDEN)
+ expect(body.message).toEqual('Request payload does not match the authorized request')
+ })
+
+ describe('jwsd', () => {
+ it('returns error when auth is client-bound but no jwsd header', async () => {
+ const bodyPayload = { request: defaultRequest }
+
+ const clientJwk = secp256k1PublicKeyToJwk(ACCOUNT.Alice.publicKey)
+ const accessToken = await getAccessToken(defaultRequest, { cnf: clientJwk })
+
+ const { status, body } = await request(app.getHttpServer())
+ .post('/sign')
+ .set(REQUEST_HEADER_CLIENT_ID, clientId)
+ .set('authorization', `GNAP ${accessToken}`)
+ .send(bodyPayload)
+
+ expect(status).toEqual(HttpStatus.FORBIDDEN)
+ expect(body.statusCode).toEqual(HttpStatus.FORBIDDEN)
+ expect(body.message).toEqual(`Missing detached-jws header`)
+ })
+
+ it('verifies jwsd header in a client-bound request', async () => {
+ const now = Math.floor(Date.now() / 1000)
+ const bodyPayload = { request: defaultRequest }
+
+ const clientJwk = secp256k1PublicKeyToJwk(ACCOUNT.Alice.publicKey)
+ const accessToken = await getAccessToken(defaultRequest, { cnf: clientJwk })
+
+ const jwsdSigner = buildSignerEip191(UNSAFE_PRIVATE_KEY.Alice)
+ const jwsdHeader: JwsdHeader = {
+ alg: SigningAlg.EIP191,
+ kid: clientJwk.kid,
+ typ: 'gnap-binding-jwsd',
+ htm: 'POST',
+ uri: 'https://armory.narval.xyz/sign',
+ created: now,
+ ath: hexToBase64Url(`0x${hash(accessToken)}`)
+ }
+ const jwsd = await signJwsd(bodyPayload, jwsdHeader, jwsdSigner).then((jws) => {
+ // Strip out the middle part for size
+ const parts = jws.split('.')
+ parts[1] = ''
+ return parts.join('.')
+ })
+
+ const { status, body } = await request(app.getHttpServer())
+ .post('/sign')
+ .set(REQUEST_HEADER_CLIENT_ID, clientId)
+ .set('authorization', `GNAP ${accessToken}`)
+ .set('detached-jws', jwsd)
+ .send(bodyPayload)
+
+ expect(body.message).toBeUndefined() // no message on this response; we're asserting it so we get a nice message on why this failed if it does fail.
+ expect(status).toEqual(HttpStatus.CREATED)
+ })
+
+ it('returns error when auth is client-bound to a different key', async () => {
+ const now = Math.floor(Date.now() / 1000)
+ const bodyPayload = { request: defaultRequest }
+
+ 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 })
+
+ const jwsdSigner = buildSignerEip191(UNSAFE_PRIVATE_KEY.Alice)
+ const jwsdHeader: JwsdHeader = {
+ alg: SigningAlg.EIP191,
+ kid: clientJwk.kid,
+ typ: 'gnap-binding-jwsd',
+ htm: 'POST',
+ uri: 'https://armory.narval.xyz/sign',
+ created: now,
+ ath: ''
+ }
+
+ const jwsd = await signJwsd(bodyPayload, jwsdHeader, jwsdSigner).then((jws) => {
+ // Strip out the middle part for size
+ const parts = jws.split('.')
+ parts[1] = ''
+ return parts.join('.')
+ })
+
+ const { status, body } = await request(app.getHttpServer())
+ .post('/sign')
+ .set(REQUEST_HEADER_CLIENT_ID, clientId)
+ .set('authorization', `GNAP ${accessToken}`)
+ .set('detached-jws', jwsd)
+ .send(bodyPayload)
+
+ expect(status).toEqual(HttpStatus.FORBIDDEN)
+ expect(body.statusCode).toEqual(HttpStatus.FORBIDDEN)
+ expect(body.message).toEqual('Invalid JWT signature')
+ })
+ })
+ })
})
diff --git a/apps/vault/src/vault/http/rest/controller/sign.controller.ts b/apps/vault/src/vault/http/rest/controller/sign.controller.ts
index 28429622a..c6b462b94 100644
--- a/apps/vault/src/vault/http/rest/controller/sign.controller.ts
+++ b/apps/vault/src/vault/http/rest/controller/sign.controller.ts
@@ -1,12 +1,12 @@
import { Request } from '@narval/policy-engine-shared'
import { Body, Controller, Post, UseGuards } from '@nestjs/common'
import { ClientId } from '../../../../shared/decorator/client-id.decorator'
-import { ClientSecretGuard } from '../../../../shared/guard/client-secret.guard'
+import { AuthorizationGuard } from '../../../../shared/guard/authorization.guard'
import { SigningService } from '../../../core/service/signing.service'
import { SignRequestDto } from '../dto/sign-request.dto'
@Controller('/sign')
-@UseGuards(ClientSecretGuard)
+@UseGuards(AuthorizationGuard)
export class SignController {
constructor(private signingService: SigningService) {}
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/apps/vault/src/vault/vault.module.ts b/apps/vault/src/vault/vault.module.ts
index 7affa09f5..36b653950 100644
--- a/apps/vault/src/vault/vault.module.ts
+++ b/apps/vault/src/vault/vault.module.ts
@@ -2,9 +2,11 @@ import { EncryptionModule } from '@narval/encryption-module'
import { HttpModule } from '@nestjs/axios'
import { Module, ValidationPipe, forwardRef } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
-import { APP_PIPE } from '@nestjs/core'
+import { APP_FILTER, APP_PIPE } from '@nestjs/core'
import { load } from '../main.config'
import { EncryptionModuleOptionFactory } from '../shared/factory/encryption-module-option.factory'
+import { ApplicationExceptionFilter } from '../shared/filter/application-exception.filter'
+import { ZodExceptionFilter } from '../shared/filter/zod-exception.filter'
import { ClientSecretGuard } from '../shared/guard/client-secret.guard'
import { KeyValueModule } from '../shared/module/key-value/key-value.module'
import { TenantModule } from '../tenant/tenant.module'
@@ -50,6 +52,14 @@ import { VaultService } from './vault.service'
new ValidationPipe({
transform: true
})
+ },
+ {
+ provide: APP_FILTER,
+ useClass: ApplicationExceptionFilter
+ },
+ {
+ provide: APP_FILTER,
+ useClass: ZodExceptionFilter
}
],
exports: [AppService, ProvisionService]
diff --git a/package-lock.json b/package-lock.json
index 9fc399290..0f7bc7013 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -38,7 +38,6 @@
"class-validator": "^0.14.1",
"clsx": "^1.2.1",
"date-fns": "^3.6.0",
- "ethers": "^5.7.2",
"handlebars": "^4.7.8",
"jose": "^5.2.2",
"lodash": "^4.17.21",
@@ -6768,697 +6767,6 @@
"node": ">=14"
}
},
- "node_modules/@ethersproject/abi": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz",
- "integrity": "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/address": "^5.7.0",
- "@ethersproject/bignumber": "^5.7.0",
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/constants": "^5.7.0",
- "@ethersproject/hash": "^5.7.0",
- "@ethersproject/keccak256": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/properties": "^5.7.0",
- "@ethersproject/strings": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/abstract-provider": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz",
- "integrity": "sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bignumber": "^5.7.0",
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/networks": "^5.7.0",
- "@ethersproject/properties": "^5.7.0",
- "@ethersproject/transactions": "^5.7.0",
- "@ethersproject/web": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/abstract-signer": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz",
- "integrity": "sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/abstract-provider": "^5.7.0",
- "@ethersproject/bignumber": "^5.7.0",
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/properties": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/address": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz",
- "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bignumber": "^5.7.0",
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/keccak256": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/rlp": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/base64": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.7.0.tgz",
- "integrity": "sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bytes": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/basex": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.7.0.tgz",
- "integrity": "sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/properties": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/bignumber": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz",
- "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "bn.js": "^5.2.1"
- }
- },
- "node_modules/@ethersproject/bytes": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz",
- "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/logger": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/constants": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz",
- "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bignumber": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/contracts": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.7.0.tgz",
- "integrity": "sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/abi": "^5.7.0",
- "@ethersproject/abstract-provider": "^5.7.0",
- "@ethersproject/abstract-signer": "^5.7.0",
- "@ethersproject/address": "^5.7.0",
- "@ethersproject/bignumber": "^5.7.0",
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/constants": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/properties": "^5.7.0",
- "@ethersproject/transactions": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/hash": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz",
- "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/abstract-signer": "^5.7.0",
- "@ethersproject/address": "^5.7.0",
- "@ethersproject/base64": "^5.7.0",
- "@ethersproject/bignumber": "^5.7.0",
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/keccak256": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/properties": "^5.7.0",
- "@ethersproject/strings": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/hdnode": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.7.0.tgz",
- "integrity": "sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/abstract-signer": "^5.7.0",
- "@ethersproject/basex": "^5.7.0",
- "@ethersproject/bignumber": "^5.7.0",
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/pbkdf2": "^5.7.0",
- "@ethersproject/properties": "^5.7.0",
- "@ethersproject/sha2": "^5.7.0",
- "@ethersproject/signing-key": "^5.7.0",
- "@ethersproject/strings": "^5.7.0",
- "@ethersproject/transactions": "^5.7.0",
- "@ethersproject/wordlists": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/json-wallets": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz",
- "integrity": "sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/abstract-signer": "^5.7.0",
- "@ethersproject/address": "^5.7.0",
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/hdnode": "^5.7.0",
- "@ethersproject/keccak256": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/pbkdf2": "^5.7.0",
- "@ethersproject/properties": "^5.7.0",
- "@ethersproject/random": "^5.7.0",
- "@ethersproject/strings": "^5.7.0",
- "@ethersproject/transactions": "^5.7.0",
- "aes-js": "3.0.0",
- "scrypt-js": "3.0.1"
- }
- },
- "node_modules/@ethersproject/keccak256": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz",
- "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bytes": "^5.7.0",
- "js-sha3": "0.8.0"
- }
- },
- "node_modules/@ethersproject/logger": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz",
- "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ]
- },
- "node_modules/@ethersproject/networks": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz",
- "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/logger": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/pbkdf2": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz",
- "integrity": "sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/sha2": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/properties": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz",
- "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/logger": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/providers": {
- "version": "5.7.2",
- "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz",
- "integrity": "sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/abstract-provider": "^5.7.0",
- "@ethersproject/abstract-signer": "^5.7.0",
- "@ethersproject/address": "^5.7.0",
- "@ethersproject/base64": "^5.7.0",
- "@ethersproject/basex": "^5.7.0",
- "@ethersproject/bignumber": "^5.7.0",
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/constants": "^5.7.0",
- "@ethersproject/hash": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/networks": "^5.7.0",
- "@ethersproject/properties": "^5.7.0",
- "@ethersproject/random": "^5.7.0",
- "@ethersproject/rlp": "^5.7.0",
- "@ethersproject/sha2": "^5.7.0",
- "@ethersproject/strings": "^5.7.0",
- "@ethersproject/transactions": "^5.7.0",
- "@ethersproject/web": "^5.7.0",
- "bech32": "1.1.4",
- "ws": "7.4.6"
- }
- },
- "node_modules/@ethersproject/providers/node_modules/ws": {
- "version": "7.4.6",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
- "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
- "engines": {
- "node": ">=8.3.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": "^5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
- "node_modules/@ethersproject/random": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.7.0.tgz",
- "integrity": "sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/logger": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/rlp": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz",
- "integrity": "sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/logger": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/sha2": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.7.0.tgz",
- "integrity": "sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "hash.js": "1.1.7"
- }
- },
- "node_modules/@ethersproject/signing-key": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.7.0.tgz",
- "integrity": "sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/properties": "^5.7.0",
- "bn.js": "^5.2.1",
- "elliptic": "6.5.4",
- "hash.js": "1.1.7"
- }
- },
- "node_modules/@ethersproject/solidity": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.7.0.tgz",
- "integrity": "sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bignumber": "^5.7.0",
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/keccak256": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/sha2": "^5.7.0",
- "@ethersproject/strings": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/strings": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz",
- "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/constants": "^5.7.0",
- "@ethersproject/logger": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/transactions": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz",
- "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/address": "^5.7.0",
- "@ethersproject/bignumber": "^5.7.0",
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/constants": "^5.7.0",
- "@ethersproject/keccak256": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/properties": "^5.7.0",
- "@ethersproject/rlp": "^5.7.0",
- "@ethersproject/signing-key": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/units": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.7.0.tgz",
- "integrity": "sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bignumber": "^5.7.0",
- "@ethersproject/constants": "^5.7.0",
- "@ethersproject/logger": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/wallet": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.7.0.tgz",
- "integrity": "sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/abstract-provider": "^5.7.0",
- "@ethersproject/abstract-signer": "^5.7.0",
- "@ethersproject/address": "^5.7.0",
- "@ethersproject/bignumber": "^5.7.0",
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/hash": "^5.7.0",
- "@ethersproject/hdnode": "^5.7.0",
- "@ethersproject/json-wallets": "^5.7.0",
- "@ethersproject/keccak256": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/properties": "^5.7.0",
- "@ethersproject/random": "^5.7.0",
- "@ethersproject/signing-key": "^5.7.0",
- "@ethersproject/transactions": "^5.7.0",
- "@ethersproject/wordlists": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/web": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz",
- "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/base64": "^5.7.0",
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/properties": "^5.7.0",
- "@ethersproject/strings": "^5.7.0"
- }
- },
- "node_modules/@ethersproject/wordlists": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.7.0.tgz",
- "integrity": "sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/bytes": "^5.7.0",
- "@ethersproject/hash": "^5.7.0",
- "@ethersproject/logger": "^5.7.0",
- "@ethersproject/properties": "^5.7.0",
- "@ethersproject/strings": "^5.7.0"
- }
- },
"node_modules/@faker-js/faker": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.0.tgz",
@@ -17910,11 +17218,6 @@
"node": ">= 10.0.0"
}
},
- "node_modules/aes-js": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz",
- "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw=="
- },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -18900,11 +18203,6 @@
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
"integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw=="
},
- "node_modules/bech32": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz",
- "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ=="
- },
"node_modules/big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -24015,53 +23313,6 @@
"url": "https://paulmillr.com/funding/"
}
},
- "node_modules/ethers": {
- "version": "5.7.2",
- "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz",
- "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==",
- "funding": [
- {
- "type": "individual",
- "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "dependencies": {
- "@ethersproject/abi": "5.7.0",
- "@ethersproject/abstract-provider": "5.7.0",
- "@ethersproject/abstract-signer": "5.7.0",
- "@ethersproject/address": "5.7.0",
- "@ethersproject/base64": "5.7.0",
- "@ethersproject/basex": "5.7.0",
- "@ethersproject/bignumber": "5.7.0",
- "@ethersproject/bytes": "5.7.0",
- "@ethersproject/constants": "5.7.0",
- "@ethersproject/contracts": "5.7.0",
- "@ethersproject/hash": "5.7.0",
- "@ethersproject/hdnode": "5.7.0",
- "@ethersproject/json-wallets": "5.7.0",
- "@ethersproject/keccak256": "5.7.0",
- "@ethersproject/logger": "5.7.0",
- "@ethersproject/networks": "5.7.1",
- "@ethersproject/pbkdf2": "5.7.0",
- "@ethersproject/properties": "5.7.0",
- "@ethersproject/providers": "5.7.2",
- "@ethersproject/random": "5.7.0",
- "@ethersproject/rlp": "5.7.0",
- "@ethersproject/sha2": "5.7.0",
- "@ethersproject/signing-key": "5.7.0",
- "@ethersproject/solidity": "5.7.0",
- "@ethersproject/strings": "5.7.0",
- "@ethersproject/transactions": "5.7.0",
- "@ethersproject/units": "5.7.0",
- "@ethersproject/wallet": "5.7.0",
- "@ethersproject/web": "5.7.1",
- "@ethersproject/wordlists": "5.7.0"
- }
- },
"node_modules/eval": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz",
@@ -28026,11 +27277,6 @@
"url": "https://github.com/sponsors/panva"
}
},
- "node_modules/js-sha3": {
- "version": "0.8.0",
- "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
- "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
- },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -36865,11 +36111,6 @@
"url": "https://opencollective.com/webpack"
}
},
- "node_modules/scrypt-js": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz",
- "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA=="
- },
"node_modules/search-insights": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.13.0.tgz",
diff --git a/package.json b/package.json
index 923e5e47a..dce5315b6 100644
--- a/package.json
+++ b/package.json
@@ -100,7 +100,6 @@
"class-validator": "^0.14.1",
"clsx": "^1.2.1",
"date-fns": "^3.6.0",
- "ethers": "^5.7.2",
"handlebars": "^4.7.8",
"jose": "^5.2.2",
"lodash": "^4.17.21",
diff --git a/packages/policy-engine-shared/src/lib/dev.fixture.ts b/packages/policy-engine-shared/src/lib/dev.fixture.ts
index 327122656..d0b42c5f6 100644
--- a/packages/policy-engine-shared/src/lib/dev.fixture.ts
+++ b/packages/policy-engine-shared/src/lib/dev.fixture.ts
@@ -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'
@@ -48,6 +48,14 @@ export const UNSAFE_PRIVATE_KEY: Record = {
Dave: '0x82a0cf4f0fdfd42d93ff328b73bfdbc9c8b4f95f5aedfae82059753fc08a180f'
}
+export const PUBLIC_KEYS_JWK: Record = {
+ 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 = {
Root: privateKeyToAccount(UNSAFE_PRIVATE_KEY.Root),
Alice: privateKeyToAccount(UNSAFE_PRIVATE_KEY.Alice),
@@ -81,39 +89,29 @@ export const USER: Record = {
export const CREDENTIAL: Record = {
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
}
}
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/schema/entity.schema.ts b/packages/policy-engine-shared/src/lib/schema/entity.schema.ts
index c42967c60..ce1a20124 100644
--- a/packages/policy-engine-shared/src/lib/schema/entity.schema.ts
+++ b/packages/policy-engine-shared/src/lib/schema/entity.schema.ts
@@ -1,4 +1,4 @@
-import { Alg } from '@narval/signature'
+import { publicKeySchema } from '@narval/signature'
import { z } from 'zod'
import { addressSchema } from './address.schema'
@@ -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({
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/index.ts b/packages/signature/src/index.ts
index a8dcd0d7b..17ae55436 100644
--- a/packages/signature/src/index.ts
+++ b/packages/signature/src/index.ts
@@ -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'
diff --git a/packages/signature/src/lib/__test__/unit/sign.spec.ts b/packages/signature/src/lib/__test__/unit/sign.spec.ts
index aa5010cdb..9c9dd8037 100644
--- a/packages/signature/src/lib/__test__/unit/sign.spec.ts
+++ b/packages/signature/src/lib/__test__/unit/sign.spec.ts
@@ -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, 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'
@@ -38,14 +38,21 @@ 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,
+ crv: 'P-256',
+ kty: 'EC',
+ kid: 'somekid',
+ use: undefined
+ })
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)
@@ -146,7 +153,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',
@@ -157,15 +164,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))
})
})
diff --git a/packages/signature/src/lib/__test__/unit/util.spec.ts b/packages/signature/src/lib/__test__/unit/util.spec.ts
index dab91880e..e4ad7b2b8 100644
--- a/packages/signature/src/lib/__test__/unit/util.spec.ts
+++ b/packages/signature/src/lib/__test__/unit/util.spec.ts
@@ -1,4 +1,10 @@
+import { p256PrivateKeySchema, rsaPrivateKeySchema, secp256k1PrivateKeySchema } from '../../schemas'
+import { buildSignerEip191, signJwt } from '../../sign'
import { isHeader, isPayload } from '../../typeguards'
+import { Alg, Secp256k1PrivateKey, SigningAlg } from '../../types'
+import { generateJwk, secp256k1PrivateKeyToHex } from '../../utils'
+import { validate } from '../../validate'
+import { verifyJwt } from '../../verify'
describe('isHeader', () => {
it('returns true for a valid header object', () => {
@@ -59,3 +65,38 @@ describe('isPayload', () => {
expect(isPayload('string')).toBe(false)
})
})
+
+describe('generateKeys', () => {
+ it('generate a valid RSA key pair and return it as a JWK', async () => {
+ const key = await generateJwk(Alg.RS256)
+ expect(rsaPrivateKeySchema.safeParse(key).success).toBe(true)
+ })
+
+ it('generates a valid P-256 key pair and return it as a JWK', async () => {
+ const key = await generateJwk(Alg.ES256)
+ expect(p256PrivateKeySchema.safeParse(key).success).toBe(true)
+ })
+
+ it('generates a valid secp256k1 key pair and return it as a JWK', async () => {
+ const key = await generateJwk(Alg.ES256K)
+ expect(secp256k1PrivateKeySchema.safeParse(key).success).toBe(true)
+ })
+
+ it('can sign and verify with a generated secp256k1 key pair', async () => {
+ const key = await generateJwk(Alg.ES256K)
+ const message = 'test message'
+ const payload = {
+ requestHash: message
+ }
+ const validatedKey = validate(
+ secp256k1PrivateKeySchema,
+ key,
+ 'Invalid secp256k1 Private Key JWK'
+ )
+
+ const signer = buildSignerEip191(secp256k1PrivateKeyToHex(validatedKey))
+ const signature = await signJwt(payload, key, { alg: SigningAlg.EIP191 }, signer)
+ const isValid = await verifyJwt(signature, key)
+ expect(isValid).not.toEqual(false)
+ })
+})
diff --git a/packages/signature/src/lib/__test__/unit/verify.spec.ts b/packages/signature/src/lib/__test__/unit/verify.spec.ts
index b1502a345..b08f0e881 100644
--- a/packages/signature/src/lib/__test__/unit/verify.spec.ts
+++ b/packages/signature/src/lib/__test__/unit/verify.spec.ts
@@ -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',
diff --git a/packages/signature/src/lib/address.schema.ts b/packages/signature/src/lib/address.schema.ts
new file mode 100644
index 000000000..57c0d165d
--- /dev/null
+++ b/packages/signature/src/lib/address.schema.ts
@@ -0,0 +1,22 @@
+import { z } from 'zod'
+import { isAddress } from './evm.util'
+
+/**
+ * Schema backward compatible with viem's Address type.
+ *
+ * @see https://viem.sh/docs/glossary/types#address
+ */
+export const addressSchema = z.custom<`0x${string}`>(
+ (value) => {
+ const parse = z.string().safeParse(value)
+
+ if (parse.success) {
+ return isAddress(parse.data)
+ }
+
+ return false
+ },
+ {
+ message: 'value is an invalid Ethereum address'
+ }
+)
diff --git a/packages/signature/src/lib/decode.ts b/packages/signature/src/lib/decode.ts
index 8a74b68f9..6432a066f 100644
--- a/packages/signature/src/lib/decode.ts
+++ b/packages/signature/src/lib/decode.ts
@@ -1,6 +1,6 @@
import { JwtError } from './error'
import { isHeader } from './typeguards'
-import { Jwt } from './types'
+import { Jwsd, Jwt } from './types'
import { base64UrlToBytes } from './utils'
/**
@@ -19,16 +19,49 @@ export function decode(rawToken: string): Jwt {
}
const [headerStr, payloadStr, jwtSig] = parts
const header = JSON.parse(base64UrlToBytes(headerStr).toString('utf-8'))
+ if (!isHeader(header)) {
+ throw new JwtError({ message: 'Invalid header', context: { rawToken, header } })
+ }
+
const payload = JSON.parse(base64UrlToBytes(payloadStr).toString('utf-8'))
// TODO: Switch these to zod parsers
// if (!isPayload(payload)) {
// throw new JwtError({ message: 'Invalid payload', context: { rawToken, payload } })
// }
+
+ return {
+ header,
+ payload,
+ signature: jwtSig
+ }
+ } catch (error) {
+ throw new JwtError({ message: 'Malformed token', context: { rawToken, error } })
+ }
+}
+
+/**
+ * Decodes a JWT without verifying its signature.
+ *
+ * @param {string} rawToken - The JWT to decode.
+ * @returns {Jwsd} A promise that resolves with the decoded payload.
+ * @throws {Error} If the payload does not match the expected structure.
+ * @throws {Error} If the header does not match the expected structure.
+ */
+export function decodeJwsd(rawToken: string): Jwsd {
+ try {
+ const parts = rawToken.split('.')
+ if (parts.length !== 3) {
+ throw new Error('Invalid JWT: The token must have three parts')
+ }
+ const [headerStr, payloadStr, jwtSig] = parts
+ const header = JSON.parse(base64UrlToBytes(headerStr).toString('utf-8'))
if (!isHeader(header)) {
throw new JwtError({ message: 'Invalid header', context: { rawToken, header } })
}
+ const payload = base64UrlToBytes(payloadStr).toString('utf-8') // Should be a sha256hash
+
return {
header,
payload,
diff --git a/packages/signature/src/lib/evm.util.ts b/packages/signature/src/lib/evm.util.ts
new file mode 100644
index 000000000..58dad15ce
--- /dev/null
+++ b/packages/signature/src/lib/evm.util.ts
@@ -0,0 +1,47 @@
+// eslint-disable-next-line no-restricted-imports
+import { InvalidAddressError, getAddress as viemGetAddress, isAddress as viemIsAddress } from 'viem'
+
+type Address = `0x${string}`
+
+/**
+ * Checks if a string is a valid Ethereum address without regard of its format.
+ *
+ * @param address - The string to be checked.
+ * @returns Returns true if the string is a valid Ethereum address, otherwise
+ * returns false.
+ */
+export const isAddress = (address: string): boolean => {
+ if (!/^(0x)?[0-9a-fA-F]{40}$/.test(address)) {
+ return false
+ } else if (/^(0x)?[0-9a-f]{40}$/.test(address) || /^(0x)?[0-9A-F]{40}$/.test(address)) {
+ return true
+ } else {
+ return viemIsAddress(address)
+ }
+}
+
+/**
+ * Retrieves the Ethereum address from a given string representation without
+ * regard of its format.
+ *
+ * @param address - The string representation of the Ethereum address.
+ * @param options - Optional parameters for address retrieval.
+ * @param options.checksum - Specifies whether the retrieved address should be
+ * checksummed.
+ * @param options.chainId - The chain ID to be used for address retrieval.
+ * @returns The Ethereum address.
+ * @throws {InvalidAddressError} if the provided address is invalid.
+ */
+export const getAddress = (address: string, options?: { checksum?: boolean; chainId?: number }): Address => {
+ if (isAddress(address)) {
+ const validAddress = address as Address
+
+ if (options?.checksum || options?.chainId) {
+ return viemGetAddress(validAddress, options.chainId)
+ }
+
+ return validAddress
+ }
+
+ throw new InvalidAddressError({ address })
+}
diff --git a/packages/signature/src/lib/schemas.ts b/packages/signature/src/lib/schemas.ts
new file mode 100644
index 000000000..eb23e5dcf
--- /dev/null
+++ b/packages/signature/src/lib/schemas.ts
@@ -0,0 +1,89 @@
+import { z } from 'zod'
+import { addressSchema } from './address.schema'
+import { Alg, Curves, KeyTypes, Use } from './types'
+
+// Base JWK Schema
+export const jwkBaseSchema = z.object({
+ kty: z.nativeEnum(KeyTypes),
+ alg: z.nativeEnum(Alg),
+ use: z.nativeEnum(Use).optional(),
+ kid: z.string(),
+ addr: z.string().optional()
+})
+
+export const jwkEoaSchema = z.object({
+ kty: z.literal(KeyTypes.EC),
+ crv: z.enum([Curves.SECP256K1]),
+ alg: z.literal(Alg.ES256K),
+ use: z.nativeEnum(Use).optional(),
+ kid: z.string(),
+ addr: addressSchema
+})
+
+// EC Base Schema
+export const ecBaseSchema = jwkBaseSchema.extend({
+ kty: z.literal(KeyTypes.EC),
+ crv: z.enum([Curves.SECP256K1, Curves.P256]),
+ x: z.string(),
+ y: z.string()
+})
+
+// RSA Base Schema
+export const rsaBaseSchema = jwkBaseSchema.extend({
+ kty: z.literal(KeyTypes.RSA),
+ alg: z.literal(Alg.RS256),
+ n: z.string(),
+ e: z.string()
+})
+
+// Specific Schemas for Public Keys
+export const secp256k1PublicKeySchema = ecBaseSchema.extend({
+ crv: z.literal(Curves.SECP256K1),
+ alg: z.literal(Alg.ES256K)
+})
+
+export const p256PublicKeySchema = ecBaseSchema.extend({
+ crv: z.literal(Curves.P256),
+ alg: z.literal(Alg.ES256)
+})
+
+export const rsaPublicKeySchema = rsaBaseSchema
+
+// Specific Schemas for Private Keys
+export const secp256k1PrivateKeySchema = secp256k1PublicKeySchema.extend({
+ d: z.string()
+})
+
+export const p256PrivateKeySchema = p256PublicKeySchema.extend({
+ d: z.string()
+})
+
+export const rsaPrivateKeySchema = rsaPublicKeySchema.extend({
+ d: z.string()
+})
+
+export const publicKeySchema = z.union([
+ secp256k1PublicKeySchema,
+ p256PublicKeySchema,
+ rsaPublicKeySchema,
+ jwkEoaSchema
+])
+
+export const privateKeySchema = z.union([secp256k1PrivateKeySchema, p256PrivateKeySchema, rsaPrivateKeySchema])
+
+export const secp256k1KeySchema = z.union([secp256k1PublicKeySchema, secp256k1PrivateKeySchema])
+
+const dynamicKeySchema = z.object({}).catchall(z.unknown())
+
+export const jwkSchema = dynamicKeySchema.extend({
+ 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')
+})
diff --git a/packages/signature/src/lib/sign.ts b/packages/signature/src/lib/sign.ts
index d9bb8e149..6692a70de 100644
--- a/packages/signature/src/lib/sign.ts
+++ b/packages/signature/src/lib/sign.ts
@@ -3,19 +3,38 @@ import { sha256 as sha256Hash } from '@noble/hashes/sha256'
import { keccak_256 as keccak256 } from '@noble/hashes/sha3'
import { SignJWT, base64url, importJWK } from 'jose'
import { isHex, signatureToHex, toBytes, toHex } from 'viem'
-import { EcdsaSignature, Header, Hex, JWK, Payload, SigningAlg } from './types'
+import { hash } from './hash-request'
+import { privateKeySchema } from './schemas'
+import { EcdsaSignature, Header, Hex, Jwk, JwsdHeader, Payload, PrivateKey, SigningAlg } from './types'
import { hexToBase64Url } from './utils'
+import { validate } from './validate'
+
+export async function signJwsd(
+ rawBody: string | object,
+ header: JwsdHeader,
+ signer: (payload: string) => Promise
+): Promise {
+ const encodedHeader = base64url.encode(JSON.stringify(header))
+ const encodedPayload = hexToBase64Url(`0x${hash(rawBody)}`)
+
+ const messageToSign = `${encodedHeader}.${encodedPayload}`
+
+ const signature = await signer(messageToSign)
+
+ const completeJWT = `${messageToSign}.${signature}`
+ return completeJWT
+}
-// WIP to replace `sign`
export async function signJwt(
payload: Payload,
- jwk: JWK,
+ jwk: Jwk,
opts: { alg?: SigningAlg } = {},
signer?: (payload: string) => Promise
): Promise {
+ const pk = validate(privateKeySchema, jwk, 'Invalid Private Key JWK')
const header: Header = {
- kid: jwk.kid,
- alg: opts.alg || jwk.alg, // TODO: add separate type for `ES256k-KECCAK`
+ kid: pk.kid,
+ alg: opts.alg || pk.alg, // TODO: add separate type for `ES256k-KECCAK`
typ: 'JWT'
}
diff --git a/packages/signature/src/lib/typeguards.ts b/packages/signature/src/lib/typeguards.ts
index a64ba40a9..fe344902e 100644
--- a/packages/signature/src/lib/typeguards.ts
+++ b/packages/signature/src/lib/typeguards.ts
@@ -1,4 +1,5 @@
-import { Header, Payload, SigningAlg } from './types'
+import { jwkEoaSchema, jwkSchema, secp256k1PublicKeySchema } from './schemas'
+import { EoaPublicKey, Header, Jwk, Payload, Secp256k1PublicKey, SigningAlg } from './types'
function isAlg(alg: unknown): alg is SigningAlg {
return typeof alg === 'string' && Object.values(SigningAlg).includes(alg as SigningAlg)
@@ -8,6 +9,14 @@ function isStringNonNull(kid: unknown): kid is string {
return typeof kid === 'string' && kid.length > 0
}
+export function isJwk(jwk: unknown): jwk is Jwk {
+ return jwkSchema.safeParse(jwk).success
+}
+
+export const isSepc256k1PublicKeyJwk = (jwk: Jwk): jwk is Secp256k1PublicKey =>
+ secp256k1PublicKeySchema.safeParse(jwk).success
+export const isEoaPublicKeyJwk = (jwk: Jwk): jwk is EoaPublicKey => jwkEoaSchema.safeParse(jwk).success
+
export function isHeader(header: unknown): header is Header {
return (
typeof header === 'object' &&
diff --git a/packages/signature/src/lib/types.ts b/packages/signature/src/lib/types.ts
index 2e7c9fbc8..b86f47cbb 100644
--- a/packages/signature/src/lib/types.ts
+++ b/packages/signature/src/lib/types.ts
@@ -1,3 +1,17 @@
+import { z } from 'zod'
+import {
+ jwkEoaSchema,
+ jwkSchema,
+ p256PrivateKeySchema,
+ p256PublicKeySchema,
+ privateKeySchema,
+ publicKeySchema,
+ rsaPrivateKeySchema,
+ secp256k1KeySchema,
+ secp256k1PrivateKeySchema,
+ secp256k1PublicKeySchema
+} from './schemas'
+
export const KeyTypes = {
EC: 'EC',
RSA: 'RSA'
@@ -36,19 +50,17 @@ export const Use = {
export type Use = (typeof Use)[keyof typeof Use]
-export type JWK = {
- kty: 'EC' | 'RSA'
- kid: string
- alg: 'ES256K' | 'ES256' | 'RS256'
- crv?: 'P-256' | 'secp256k1' | undefined
- use?: 'sig' | 'enc' | undefined
- n?: string | undefined
- e?: string | undefined
- x?: string | undefined
- y?: string | undefined
- d?: string | undefined
- addr?: Hex | undefined
-}
+export type Secp256k1PrivateKey = z.infer
+export type P256PrivateKey = z.infer
+export type P256PublicKey = z.infer
+export type RsaPrivateKey = z.infer
+export type EoaPublicKey = z.infer
+export type Secp256k1PublicKey = z.infer
+export type Secp256k1KeySchema = z.infer
+export type PublicKey = z.infer
+export type PrivateKey = z.infer
+export type Jwk = z.infer
+
export type Hex = `0x${string}` // DOMAIN
/**
@@ -68,6 +80,19 @@ export type Header = {
ath?: string | undefined // The hash of the access token. The value MUST be the result of Base64url encoding (with no padding) the SHA-256 digest of the ASCII encoding of the associated access token's value.
}
+// https://www.ietf.org/archive/id/draft-ietf-gnap-core-protocol-19.html#name-detached-jws
+// For GNAP JWSD header, the fields are required.
+// `ath` is also required IF it's a bound-request, otherwise it's optional
+export type JwsdHeader = {
+ alg: SigningAlg
+ kid: string // Key ID to identify the signing key
+ typ: 'gnap-binding-jwsd' // see https://www.ietf.org/archive/id/draft-ietf-gnap-core-protocol-19.html#name-detached-jws
+ htm: string // HTTP Method
+ uri: string // The HTTP URI used for this request. This value MUST be an absolute URI, including all path and query components and no fragment component.
+ created: number // The time the request was created.
+ ath?: string | undefined // The hash of the access token. The value MUST be the result of Base64url encoding (with no padding) the SHA-256 digest of the ASCII encoding of the associated access token's value.
+}
+
/**
* Defines the payload of JWT.
*
@@ -78,7 +103,7 @@ export type Header = {
* @param {string} sub - The subject of the JWT.
* @param {string} [aud] - The audience of the JWT.
* @param {string} [jti] - The JWT ID.
- * @param {JWK} cnf - The client-bound key.
+ * @param {Jwk} cnf - The client-bound key.
*
*/
export type Payload = {
@@ -88,7 +113,7 @@ export type Payload = {
iss?: string
aud?: string
jti?: string
- cnf?: JWK // The client-bound key
+ cnf?: PublicKey // The client-bound key
requestHash?: string
data?: string // hash of any data
}
@@ -99,6 +124,12 @@ export type Jwt = {
signature: string
}
+export type Jwsd = {
+ header: Header
+ payload: string
+ signature: string
+}
+
/**
* Defines the input required to generate a JWT signature for a request.
*
diff --git a/packages/signature/src/lib/utils.ts b/packages/signature/src/lib/utils.ts
index b5955e023..a0a91be62 100644
--- a/packages/signature/src/lib/utils.ts
+++ b/packages/signature/src/lib/utils.ts
@@ -1,8 +1,26 @@
+import { p256 } from '@noble/curves/p256'
import { secp256k1 } from '@noble/curves/secp256k1'
import { sha256 as sha256Hash } from '@noble/hashes/sha256'
+import { exportJWK, generateKeyPair } from 'jose'
import { toHex } from 'viem'
-import { getAddress, publicKeyToAddress } from 'viem/utils'
-import { Alg, Curves, Hex, JWK, KeyTypes } from './types'
+import { publicKeyToAddress } from 'viem/utils'
+import { JwtError } from './error'
+import { rsaPrivateKeySchema } from './schemas'
+import {
+ Alg,
+ Curves,
+ Hex,
+ Jwk,
+ KeyTypes,
+ P256PrivateKey,
+ P256PublicKey,
+ RsaPrivateKey,
+ Secp256k1KeySchema,
+ Secp256k1PrivateKey,
+ Secp256k1PublicKey,
+ Use
+} from './types'
+import { validate } from './validate'
export const algToJwk = (
alg: Alg
@@ -39,7 +57,7 @@ export const addressToKid = (address: string): string => {
}
// ES256k
-export const publicKeyToJwk = (publicKey: Hex, keyId?: string): JWK => {
+export const secp256k1PublicKeyToJwk = (publicKey: Hex, keyId?: string): Secp256k1PublicKey => {
// remove the 0x04 prefix -- 04 means it's an uncompressed ECDSA key, 02 or 03 means compressed -- these need to be removed in a JWK!
const hexPubKey = publicKey.slice(4)
const x = hexPubKey.slice(0, 64)
@@ -55,40 +73,50 @@ export const publicKeyToJwk = (publicKey: Hex, keyId?: string): JWK => {
}
}
+export const p256PublicKeyToJwk = (publicKey: Hex, keyId?: string): P256PublicKey => {
+ const hexPubKey = publicKey.slice(4)
+ const x = hexPubKey.slice(0, 64)
+ const y = hexPubKey.slice(64)
+ return {
+ kty: KeyTypes.EC,
+ crv: Curves.P256,
+ alg: Alg.ES256,
+ kid: keyId || publicKeyToAddress(publicKey),
+ x: hexToBase64Url(`0x${x}`),
+ y: hexToBase64Url(`0x${y}`)
+ }
+}
+
// ES256k
-export const privateKeyToJwk = (privateKey: Hex, keyId?: string): JWK => {
+export const secp256k1PrivateKeyToJwk = (privateKey: Hex, keyId?: string): Secp256k1PrivateKey => {
const publicKey = toHex(secp256k1.getPublicKey(privateKey.slice(2), false))
- const publicJwk = publicKeyToJwk(publicKey, keyId)
+ const publicJwk = secp256k1PublicKeyToJwk(publicKey, keyId)
return {
...publicJwk,
d: hexToBase64Url(privateKey)
}
}
-// Eth EOA
-export const addressToJwk = (address: string, keyId?: string): JWK => {
+export const p256PrivateKeyToJwk = (privateKey: Hex, keyId?: string): P256PrivateKey => {
+ const publicKey = toHex(p256.getPublicKey(privateKey.slice(2), false))
+ const publicJwk = p256PublicKeyToJwk(publicKey, keyId)
return {
- kty: KeyTypes.EC,
- crv: Curves.SECP256K1,
- alg: Alg.ES256K,
- kid: keyId || addressToKid(address),
- addr: getAddress(address)
+ ...publicJwk,
+ d: hexToBase64Url(privateKey)
}
}
-export const jwkToPublicKey = (jwk: JWK): Hex => {
- if (!jwk.x || !jwk.y) {
- throw new Error('Invalid JWK; missing x or y')
- }
+export const p256PrivateKeyToHex = (jwk: P256PrivateKey): Hex => {
+ return base64UrlToHex(jwk.d)
+}
+
+export const secp256k1PublicKeyToHex = (jwk: Secp256k1KeySchema): Hex => {
const x = base64UrlToHex(jwk.x)
const y = base64UrlToHex(jwk.y)
return `0x04${x.slice(2)}${y.slice(2)}`
}
-export const jwkToPrivateKey = (jwk: JWK): Hex => {
- if (!jwk.d) {
- throw new Error('Invalid JWK; missing d')
- }
+export const secp256k1PrivateKeyToHex = (jwk: Secp256k1PrivateKey): Hex => {
return base64UrlToHex(jwk.d)
}
@@ -114,3 +142,69 @@ export const base64UrlToHex = (base64Url: string): Hex => {
export const base64UrlToBytes = (base64Url: string): Buffer => {
return Buffer.from(base64UrlToBase64(base64Url), 'base64')
}
+
+const rsaKeyToKid = (jwk: Jwk) => {
+ // Concatenate the 'n' and 'e' values, splitted by ':'
+ const dataToHash = `${jwk.n}:${jwk.e}`
+
+ const binaryData = base64UrlToBytes(dataToHash)
+ const hash = sha256Hash(binaryData)
+ return toHex(hash)
+}
+
+const generateRsaKeyPair = async (
+ opts: {
+ keyId?: string
+ modulusLength?: number
+ use?: Use
+ } = {
+ modulusLength: 2048
+ }
+): Promise => {
+ const { privateKey } = await generateKeyPair(Alg.RS256, {
+ modulusLength: opts.modulusLength,
+ extractable: true
+ })
+
+ const partialJwk = await exportJWK(privateKey)
+ if (!partialJwk.n) {
+ throw new JwtError({ message: 'Invalid JWK; missing n', context: { partialJwk } })
+ }
+ const jwk: Jwk = {
+ ...partialJwk,
+ alg: Alg.RS256,
+ kty: KeyTypes.RSA,
+ crv: undefined,
+ use: opts.use || undefined
+ }
+ jwk.kid = opts.keyId || rsaKeyToKid(jwk)
+
+ const pk = validate(rsaPrivateKeySchema, jwk, 'Invalid RSA Private Key JWK')
+ return pk
+}
+
+export const generateJwk = async (
+ alg: Alg,
+ opts?: {
+ keyId?: string
+ modulusLength?: number
+ use?: Use
+ }
+): Promise => {
+ switch (alg) {
+ case Alg.ES256K: {
+ const privateKeyK1 = toHex(secp256k1.utils.randomPrivateKey())
+ return secp256k1PrivateKeyToJwk(privateKeyK1, opts?.keyId)
+ }
+ case Alg.ES256: {
+ const privateKeyP256 = toHex(p256.utils.randomPrivateKey())
+ return p256PrivateKeyToJwk(privateKeyP256, opts?.keyId)
+ }
+ case Alg.RS256: {
+ const jwk = await generateRsaKeyPair(opts)
+ return jwk
+ }
+ default:
+ throw new Error(`Unsupported algorithm: ${alg}`)
+ }
+}
diff --git a/packages/signature/src/lib/validate.ts b/packages/signature/src/lib/validate.ts
new file mode 100644
index 000000000..fb0735aa0
--- /dev/null
+++ b/packages/signature/src/lib/validate.ts
@@ -0,0 +1,16 @@
+import { ZodSchema } from 'zod'
+import { JwtError } from './error'
+import { Jwk } from './types'
+
+export function validate(schema: ZodSchema, jwk: Jwk, errorMessage: string = 'Validation failed') {
+ return (function validate(input: Jwk): T {
+ const result = schema.safeParse(input)
+ if (!result.success) {
+ throw new JwtError({
+ message: errorMessage,
+ context: { errors: result.error.flatten().fieldErrors }
+ })
+ }
+ return result.data
+ })(jwk)
+}
diff --git a/packages/signature/src/lib/verify.ts b/packages/signature/src/lib/verify.ts
index c7a0a4abe..178d8ad10 100644
--- a/packages/signature/src/lib/verify.ts
+++ b/packages/signature/src/lib/verify.ts
@@ -1,11 +1,14 @@
import { secp256k1 } from '@noble/curves/secp256k1'
import { importJWK, jwtVerify } from 'jose'
import { isAddressEqual, recoverAddress } from 'viem'
-import { decode } from './decode'
+import { decode, decodeJwsd } from './decode'
import { JwtError } from './error'
+import { publicKeySchema } from './schemas'
import { eip191Hash } from './sign'
-import { Hex, JWK, Jwt, Payload, SigningAlg } from './types'
-import { base64UrlToHex, jwkToPublicKey } from './utils'
+import { isSepc256k1PublicKeyJwk } from './typeguards'
+import { Alg, EoaPublicKey, Hex, Jwk, Jwsd, Jwt, Payload, PublicKey, Secp256k1PublicKey, SigningAlg } from './types'
+import { base64UrlToHex, secp256k1PublicKeyToHex } from './utils'
+import { validate } from './validate'
const checkTokenExpiration = (payload: Payload): boolean => {
const now = Math.floor(Date.now() / 1000)
@@ -26,42 +29,60 @@ const verifyEip191WithRecovery = async (sig: Hex, hash: Uint8Array, address: Hex
return true
}
-const verifyEip191WithPublicKey = async (sig: Hex, hash: Uint8Array, jwk: JWK): Promise => {
- const pub = jwkToPublicKey(jwk)
+const verifyEip191WithPublicKey = async (sig: Hex, hash: Uint8Array, jwk: PublicKey): Promise => {
+ if (isSepc256k1PublicKeyJwk(jwk)) {
+ const pub = secp256k1PublicKeyToHex(jwk)
+ // A eth sig has a `v` value of 27 or 28, so we need to remove that to get the signature
+ // And we remove the 0x prefix. So that means we slice the first and last 2 bytes, leaving the 128 character signature
+ const isValid = secp256k1.verify(sig.slice(2, 130), hash, pub.slice(2)) === true
+ if (!isValid) {
+ throw new Error('Invalid JWT signature')
+ }
+ return isValid
+ }
+ throw new JwtError({
+ message: 'Validation error: unsupported algorithm',
+ context: { jwk }
+ })
+}
- // A eth sig has a `v` value of 27 or 28, so we need to remove that to get the signature
- // And we remove the 0x prefix. So that means we slice the first and last 2 bytes, leaving the 128 character signature
- const isValid = secp256k1.verify(sig.slice(2, 130), hash, pub.slice(2)) === true
- if (!isValid) {
- throw new Error('Invalid JWT signature')
+const verifySepc256k1 = async (
+ sig: Hex,
+ hash: Uint8Array,
+ jwk: Secp256k1PublicKey | EoaPublicKey
+): Promise => {
+ if (isSepc256k1PublicKeyJwk(jwk)) {
+ await verifyEip191WithPublicKey(sig, hash, jwk)
+ } else {
+ await verifyEip191WithRecovery(sig, hash, jwk.addr)
}
- return isValid
+ return true
}
-export const verifyEip191 = async (jwt: string, jwk: JWK): Promise => {
+export const verifyEip191 = async (jwt: string, jwk: PublicKey): Promise => {
const [headerStr, payloadStr, jwtSig] = jwt.split('.')
const verificationMsg = [headerStr, payloadStr].join('.')
const msg = eip191Hash(verificationMsg)
const sig = base64UrlToHex(jwtSig)
- // If we have an Address but no x & y, recover the address from the signature to verify
- // Otherwise, verify directly against the public key from the x&y.
- if (jwk.x && jwk.y) {
- await verifyEip191WithPublicKey(sig, msg, jwk)
- } else if (jwk.addr) {
- await verifyEip191WithRecovery(sig, msg, jwk.addr)
+ if (jwk.alg === Alg.ES256K) {
+ await verifySepc256k1(sig, msg, jwk)
} else {
- throw new Error('Invalid JWK, no x & y or address')
+ throw new JwtError({
+ message: 'Validation error: unsupported algorithm',
+ context: { jwk }
+ })
}
return true
}
-export async function verifyJwt(jwt: string, jwk: JWK): Promise {
+export async function verifyJwt(jwt: string, jwk: Jwk): Promise {
const { header, payload, signature } = decode(jwt)
+ const key = validate(publicKeySchema, jwk, 'Invalid Public Key JWK')
if (header.alg === SigningAlg.EIP191) {
- await verifyEip191(jwt, jwk)
+ await verifyEip191(jwt, key)
} else {
// TODO: Implement other algs individually without jose
const joseJwk = await importJWK(jwk)
@@ -78,3 +99,21 @@ export async function verifyJwt(jwt: string, jwk: JWK): Promise {
signature
}
}
+
+export async function verifyJwsd(jws: string, jwk: PublicKey): Promise {
+ const { header, payload, signature } = decodeJwsd(jws)
+
+ if (header.alg === SigningAlg.EIP191) {
+ await verifyEip191(jws, jwk)
+ } else {
+ // TODO: Implement other algs individually without jose
+ const joseJwk = await importJWK(jwk)
+ await jwtVerify(jws, joseJwk)
+ }
+
+ return {
+ header,
+ payload,
+ signature
+ }
+}