Skip to content

Commit

Permalink
Merge pull request #159 from narval-xyz/feature/nar-1546-add-signing-…
Browse files Browse the repository at this point in the history
…key-to-engine

Feature/nar 1546 add signing key to engine
  • Loading branch information
mattschoch authored Mar 10, 2024
2 parents 916c664 + 3c6c3cf commit af961c4
Show file tree
Hide file tree
Showing 21 changed files with 536 additions and 250 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ include ./apps/devtool/Makefile
include ./apps/policy-engine/Makefile
include ./packages/policy-engine-shared/Makefile
include ./packages/transaction-request-intent/Makefile
include ./packages/signature/Makefile

# For more terminal color codes, head over to https://opensource.com/article/19/9/linux-terminal-colors
TERM_NO_COLOR := \033[0m
Expand Down
2 changes: 2 additions & 0 deletions apps/policy-engine/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DataStoreRepositoryFactory } from './core/factory/data-store-repository
import { BootstrapService } from './core/service/bootstrap.service'
import { DataStoreService } from './core/service/data-store.service'
import { EngineService } from './core/service/engine.service'
import { SigningService } from './core/service/signing.service'
import { TenantService } from './core/service/tenant.service'
import { TenantController } from './http/rest/controller/tenant.controller'
import { OpaService } from './opa/opa.service'
Expand Down Expand Up @@ -38,6 +39,7 @@ import { TenantRepository } from './persistence/repository/tenant.repository'
DataStoreService,
EngineRepository,
EngineService,
SigningService,
EntityRepository,
FileSystemDataStoreRepository,
HttpDataStoreRepository,
Expand Down
38 changes: 26 additions & 12 deletions apps/policy-engine/src/app/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {
EvaluationRequest,
EvaluationResponse,
HistoricalTransfer,
JsonWebKey,
Request,
Signature
} from '@narval/policy-engine-shared'
import { Alg, hash } from '@narval/signature'
import { Alg, Payload, SigningAlg, hash, privateKeyToJwk, publicKeyToJwk } from '@narval/signature'
import { safeDecode } from '@narval/transaction-request-intent'
import {
BadRequestException,
Expand All @@ -20,8 +21,8 @@ import {
import { InputType } from 'packages/transaction-request-intent/src/lib/domain'
import { Intent } from 'packages/transaction-request-intent/src/lib/intent.types'
import { Hex, verifyMessage } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { OpaResult, RegoInput } from '../shared/type/domain.type'
import { SigningService } from './core/service/signing.service'
import { OpaService } from './opa/opa.service'
import { EntityRepository } from './persistence/repository/entity.repository'

Expand Down Expand Up @@ -69,7 +70,8 @@ export const finalizeDecision = (response: OpaResult[]) => {
export class AppService {
constructor(
private opaService: OpaService,
private entityRepository: EntityRepository
private entityRepository: EntityRepository,
private signingService: SigningService
) {}

async #verifySignature(requestSignature: Signature, verificationMessage: string): Promise<CredentialEntity> {
Expand Down Expand Up @@ -213,15 +215,27 @@ export class AppService {

// If we are allowing, then the ENGINE signs the verification too
if (finalDecision.decision === Decision.PERMIT) {
// TODO: store a global configuration on the response signature alg
const engineAccount = privateKeyToAccount(ENGINE_PRIVATE_KEY)
const permitSignature = await engineAccount.signMessage({
message: verificationMessage
})
authzResponse.attestation = {
sig: permitSignature,
alg: Alg.ES256K,
pubKey: engineAccount.address // TODO: should this be account.publicKey?
const tenantSigningKey: JsonWebKey = privateKeyToJwk(ENGINE_PRIVATE_KEY)

const clientJwk = publicKeyToJwk(principalCredential.pubKey as Hex)

const jwtPayload: Payload = {
requestHash: verificationMessage,
sub: principalCredential.userId,
// TODO: iat & exp values must be arguments, cannot generate timestamps because of cluster mis-match
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 60 * 10, // 10 minutes
iss: 'https://armory.narval.xyz', // TODO: allow client-specific; should come from config
// aud: TODO
// jti: TODO
cnf: clientJwk
}

// TODO: signing alg should come from the tenant config
const permitJwt = await this.signingService.sign(jwtPayload, tenantSigningKey, { alg: SigningAlg.EIP191 })

authzResponse.accessToken = {
value: permitJwt
}
}

Expand Down
55 changes: 37 additions & 18 deletions apps/policy-engine/src/app/core/service/signing.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { JsonWebKey, toHex } from '@narval/policy-engine-shared'
import { Alg, Curves, KeyTypes, Use } from '@narval/signature'
import { Alg, Payload, SigningAlg, privateKeyToJwk } from '@narval/signature'
import { Injectable } from '@nestjs/common'
import { secp256k1 } from '@noble/curves/secp256k1'
import { publicKeyToAddress } from 'viem/utils'
import { EncryptionService } from '../../../encryption/core/encryption.service'
import { buildSignerEip191, buildSignerEs256k, signJwt } from 'packages/signature/src/lib/sign'

// Optional additional configs, such as for MPC-based DKG.
type KeyGenerationOptions = {
Expand All @@ -15,28 +14,23 @@ type KeyGenerationResponse = {
privateKey?: JsonWebKey
}

type SignOptions = {
alg?: SigningAlg
}

@Injectable()
export class SigningService {
constructor(private encryptionService: EncryptionService) {}
constructor() {}

async generateSigningKey(alg: Alg, options?: KeyGenerationOptions): Promise<KeyGenerationResponse> {
if (alg === Alg.ES256K) {
const privateKey = toHex(secp256k1.utils.randomPrivateKey())
const publicKey = toHex(secp256k1.getPublicKey(privateKey.slice(2), false))

const publicJwk: JsonWebKey = {
kty: KeyTypes.EC,
crv: Curves.SECP256K1,
alg: Alg.ES256K,
use: Use.SIG,
kid: options?.keyId || publicKeyToAddress(publicKey), // add an opaque prefix that indicates the key type
x: publicKey.slice(2, 66),
y: publicKey.slice(66)
}
const privateJwk = privateKeyToJwk(privateKey, options?.keyId)

const privateJwk: JsonWebKey = {
...publicJwk,
d: privateKey.slice(2)
// Remove the privateKey from the public jwk
const publicJwk = {
...privateJwk,
d: undefined
}

return {
Expand All @@ -47,4 +41,29 @@ export class SigningService {

throw new Error('Unsupported algorithm')
}

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

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

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

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

return jwt
}

throw new Error('Unsupported algorithm')
}
}
10 changes: 5 additions & 5 deletions packages/policy-engine-shared/src/lib/dev.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,31 +82,31 @@ export const USER: Record<Personas, UserEntity> = {
export const CREDENTIAL: Record<Personas, CredentialEntity> = {
Root: {
id: sha256(ACCOUNT.Root.address).toLowerCase(),
pubKey: ACCOUNT.Root.address,
pubKey: ACCOUNT.Root.publicKey,
alg: Alg.ES256K,
userId: USER.Root.id
},
Alice: {
id: sha256(ACCOUNT.Alice.address).toLowerCase(),
pubKey: ACCOUNT.Alice.address,
pubKey: ACCOUNT.Alice.publicKey,
alg: Alg.ES256K,
userId: USER.Alice.id
},
Bob: {
id: sha256(ACCOUNT.Bob.address).toLowerCase(),
pubKey: ACCOUNT.Bob.address,
pubKey: ACCOUNT.Bob.publicKey,
alg: Alg.ES256K,
userId: USER.Bob.id
},
Carol: {
id: sha256(ACCOUNT.Carol.address).toLowerCase(),
pubKey: ACCOUNT.Carol.address,
pubKey: ACCOUNT.Carol.publicKey,
alg: Alg.ES256K,
userId: USER.Carol.id
},
Dave: {
id: sha256(ACCOUNT.Dave.address).toLowerCase(),
pubKey: ACCOUNT.Dave.address,
pubKey: ACCOUNT.Dave.publicKey,
alg: Alg.ES256K,
userId: USER.Dave.id
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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.string().describe('Algorithm'),
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'),
Expand Down
8 changes: 7 additions & 1 deletion packages/policy-engine-shared/src/lib/type/domain.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ export type ApprovalRequirement = {
countPrincipal: boolean
}

export type AccessToken = {
value: string // JWT
// could include a key-proof
}

export type EvaluationResponse = {
decision: Decision
request?: Request
Expand All @@ -168,7 +173,8 @@ export type EvaluationResponse = {
missing: ApprovalRequirement[]
satisfied: ApprovalRequirement[]
}
attestation?: Signature
attestation?: Signature // @deprecated, use AccessToken
accessToken?: AccessToken
transactionRequestIntent?: unknown
}
// DOMAIN
Expand Down
9 changes: 5 additions & 4 deletions packages/signature/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { decode } from './lib/decode'
export { hash } from './lib/hash-request'
export { sign } from './lib/sign'
export * from './lib/decode'
export * from './lib/hash-request'
export * from './lib/sign'
export * from './lib/types'
export { verify } from './lib/verify'
export * from './lib/verify'
export * from './lib/utils'
12 changes: 2 additions & 10 deletions packages/signature/src/lib/__test__/unit/decode.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { base64url } from 'jose'
import { decode } from '../../decode'
import { DECODED_TOKEN, HEADER_PART, SIGNATURE_PART, SIGNED_TOKEN } from './mock'
import { DECODED_TOKEN, SIGNATURE_PART, SIGNED_TOKEN } from './mock'

describe('decode', () => {
it('decodes a request successfully', async () => {
Expand All @@ -13,15 +13,7 @@ describe('decode', () => {
it('throws an error if token is formed well with unmeaningful data', async () => {
expect(() => decode('invalid.invalid.invalid')).toThrow()
})
it('throws an error if payload is invalid with a valid signature', async () => {
const token = `${HEADER_PART}.${'invalid'}.${SIGNATURE_PART}`
expect(() => decode(token)).toThrow()
const encodedPayload = base64url.encode(
JSON.stringify({ requestHash: 'hashedRequest', iat: '1728917', exp: '1728917' })
)
const token2 = `${HEADER_PART}.${encodedPayload}.${SIGNATURE_PART}`
expect(() => decode(token2)).toThrow()
})

it('throws an error if header is invalid', async () => {
const encodedHeader = base64url.encode(JSON.stringify({ alg: 'invalid', kid: 'invalid' }))
const token = `${encodedHeader}.${'invalid'}.${SIGNATURE_PART}`
Expand Down
36 changes: 0 additions & 36 deletions packages/signature/src/lib/__test__/unit/eoa-keys.spec.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/signature/src/lib/__test__/unit/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Alg } from '../../types'

export const ALGORITHM = Alg.ES256
export const KID = 'test-kid'
export const IAT = new Date('2024-12-11T00:00:00Z')
export const EXP = new Date('2024-12-12T00:00:00Z')
export const IAT = new Date('2024-12-11T00:00:00Z').getTime() / 1000
export const EXP = new Date('2024-12-12T00:00:00Z').getTime() / 1000
export const REQUEST = {
action: 'CREATE_ORGANIZATION',
nonce: 'random-nonce-111',
Expand Down
Loading

0 comments on commit af961c4

Please sign in to comment.