Skip to content

Commit

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

Generating signing key
  • Loading branch information
mattschoch authored Mar 4, 2024
2 parents 5cd9c9d + e3d083c commit d098e00
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 28 deletions.
50 changes: 50 additions & 0 deletions apps/policy-engine/src/app/core/signing.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { JsonWebKey, toHex } from '@narval/policy-engine-shared'
import { Alg, Curves, KeyTypes, Use } 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'

// Optional additional configs, such as for MPC-based DKG.
type KeyGenerationOptions = {
keyId: string
}

type KeyGenerationResponse = {
publicKey: JsonWebKey
privateKey?: JsonWebKey
}

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

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: JsonWebKey = {
...publicJwk,
d: privateKey.slice(2)
}

return {
publicKey: publicJwk,
privateKey: privateJwk
}
}

throw new Error('Unsupported algorithm')
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { toBytes, toHex } from '@narval/policy-engine-shared'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { Test } from '@nestjs/testing'
import { mock } from 'jest-mock-extended'
Expand Down Expand Up @@ -30,7 +31,7 @@ describe('EncryptionService', () => {
Promise.resolve({
// unencryptedMasterKey: dfd9cc70f1ad02d19e0efa020d82f557022f59ca6bedbec1df38e8fd37ae3bb9
masterKey:
'0205785d67737fa3bae8eb249cf8d3baed5942f1677d8c98b4cdeef55560a3bcf510bd008d00030003617070000d61726d6f72792d656e67696e6500156177732d63727970746f2d7075626c69632d6b657900444177336764324b6e58646f512f2b76745347367031444442384d65766d61434b324c7861426e65476a315531537777526b376b4d366868752f707a446f48724c77773d3d0007707572706f7365000f646174612d656e6372797074696f6e000100146e617276616c2e61726d6f72792e656e67696e65002561726d6f72792e656e67696e652e6b656b000000800000000c8a92a7c9deb43316f6c29e8d0030132d63c7337c9888a06b638966e83056a0575958b42588b7aed999b9659e6d4bc5bed4664d91fae0b14d48917e00cdbb02000010000749ed0ed3616b7990f9e73f5a42eb46dc182002612e33dcb8e3c7d4759184c46ce3f0893a87ac15257d53097ac5d74affffffff00000001000000000000000000000001000000205d7209b51db8cf8264b9065add71a8514dc26baa6987d8a0a3acb1c4a2503b0f3b7c974a35ed234c1b94668736cd8bfa00673065023100a5d8d192e9802649dab86af6e00ab6d7472533e85dfe1006cb8bd9ef2472d15096fa42e742d18cb92530c762c3bd44d40230350299b42feaa1149c6ad1b25add24c30b3bf1c08263b96df0d43e2ad3e19802872e792040f1faf3d0a73bca6fb067ca',
'0x0205785d67737fa3bae8eb249cf8d3baed5942f1677d8c98b4cdeef55560a3bcf510bd008d00030003617070000d61726d6f72792d656e67696e6500156177732d63727970746f2d7075626c69632d6b657900444177336764324b6e58646f512f2b76745347367031444442384d65766d61434b324c7861426e65476a315531537777526b376b4d366868752f707a446f48724c77773d3d0007707572706f7365000f646174612d656e6372797074696f6e000100146e617276616c2e61726d6f72792e656e67696e65002561726d6f72792e656e67696e652e6b656b000000800000000c8a92a7c9deb43316f6c29e8d0030132d63c7337c9888a06b638966e83056a0575958b42588b7aed999b9659e6d4bc5bed4664d91fae0b14d48917e00cdbb02000010000749ed0ed3616b7990f9e73f5a42eb46dc182002612e33dcb8e3c7d4759184c46ce3f0893a87ac15257d53097ac5d74affffffff00000001000000000000000000000001000000205d7209b51db8cf8264b9065add71a8514dc26baa6987d8a0a3acb1c4a2503b0f3b7c974a35ed234c1b94668736cd8bfa00673065023100a5d8d192e9802649dab86af6e00ab6d7472533e85dfe1006cb8bd9ef2472d15096fa42e742d18cb92530c762c3bd44d40230350299b42feaa1149c6ad1b25add24c30b3bf1c08263b96df0d43e2ad3e19802872e792040f1faf3d0a73bca6fb067ca',
id: 'test-engine-id'
})
)
Expand Down Expand Up @@ -70,10 +71,19 @@ describe('EncryptionService', () => {
})

it('should encrypt then decrypt successfully, with a buffer from a hexstring', async () => {
const data = 'dfd9cc70f1ad02d19e0efa020d82f557022f59ca6bedbec1df38e8fd37ae3bb9'
const encrypted = await service.encrypt(Buffer.from(data, 'hex'))
const data = '0xdfd9cc70f1ad02d19e0efa020d82f557022f59ca6bedbec1df38e8fd37ae3bb9'
const encrypted = await service.encrypt(toBytes(data))
const decrypted = await service.decrypt(encrypted)

expect(decrypted.toString('hex')).toBe(data)
expect(toHex(decrypted)).toBe(data)
})

it('should decrypt a hex-encoded string', async () => {
const data = 'Hello World'
const encryptedBuffer = await service.encrypt(data)
const encryptedHex = toHex(encryptedBuffer)
const decrypted = await service.decrypt(encryptedHex)

expect(decrypted.toString('utf-8')).toBe(data)
})
})
18 changes: 12 additions & 6 deletions apps/policy-engine/src/encryption/core/encryption.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
RawAesWrappingSuiteIdentifier,
buildClient
} from '@aws-crypto/client-node'
import { Hex, toBytes, toHex } from '@narval/policy-engine-shared'
import { Inject, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import crypto from 'crypto'
Expand Down Expand Up @@ -56,7 +57,7 @@ export class EncryptionService implements OnApplicationBootstrap {
encryptedMasterKey = await this.generateMasterKey(kek)
}

const decryptedMasterKey = await this.decryptMasterKey(kek, Buffer.from(encryptedMasterKey, 'hex'))
const decryptedMasterKey = await this.decryptMasterKey(kek, toBytes(encryptedMasterKey))
const isolatedMasterKey = Buffer.alloc(decryptedMasterKey.length)
decryptedMasterKey.copy(isolatedMasterKey, 0, 0, decryptedMasterKey.length)

Expand Down Expand Up @@ -110,7 +111,7 @@ export class EncryptionService implements OnApplicationBootstrap {
return result
}

private async decryptMasterKey(kek: Buffer, ciphertext: Buffer): Promise<Buffer> {
private async decryptMasterKey(kek: Buffer, ciphertext: Uint8Array): Promise<Buffer> {
const keyring = this.getKeyEncryptionKeyring(kek)
const { plaintext, messageHeader } = await decrypt(keyring, ciphertext)

Expand All @@ -124,7 +125,7 @@ export class EncryptionService implements OnApplicationBootstrap {
return plaintext
}

async encrypt(cleartext: string | Buffer): Promise<Buffer> {
async encrypt(cleartext: string | Buffer | Uint8Array): Promise<Buffer> {
const keyring = this.keyring
if (!keyring) throw new Error('Keyring not set')

Expand All @@ -135,11 +136,16 @@ export class EncryptionService implements OnApplicationBootstrap {
return result
}

async decrypt(ciphertext: Buffer): Promise<Buffer> {
async decrypt(ciphertext: Buffer | Uint8Array | Hex): Promise<Buffer> {
const keyring = this.keyring
if (!keyring) throw new Error('Keyring not set')

const { plaintext, messageHeader } = await decrypt(keyring, ciphertext)
let ciphertextBuffer = ciphertext
if (typeof ciphertext === 'string') {
ciphertextBuffer = toBytes(ciphertext)
}

const { plaintext, messageHeader } = await decrypt(keyring, ciphertextBuffer)

// Verify the context wasn't changed
const { encryptionContext } = messageHeader
Expand All @@ -158,7 +164,7 @@ export class EncryptionService implements OnApplicationBootstrap {

// Encrypt it with the Key Encryption Key (KEK) that was derived from the MP
const encryptedMk = await this.encryptMaterKey(kek, mkBuffer)
const encryptedMkString = encryptedMk.toString('hex')
const encryptedMkString = toHex(encryptedMk)

// Save the Result.
const existingEngine = await this.encryptionRepository.getEngine(this.engineId)
Expand Down
64 changes: 60 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"@nestjs/core": "^10.3.0",
"@nestjs/platform-express": "^10.3.3",
"@nestjs/swagger": "^7.2.0",
"@noble/curves": "1.3.0",
"@open-policy-agent/opa-wasm": "^1.8.0",
"@prisma/client": "^5.7.1",
"@tanstack/react-query": "^5.24.1",
Expand Down
1 change: 1 addition & 0 deletions packages/policy-engine-shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './lib/type/data-store.type'
export * from './lib/type/domain.type'
export * from './lib/type/entity.type'
export * from './lib/util/caip.util'
export * from './lib/util/encoding.util'
export * as EntityUtil from './lib/util/entity.util'
export * from './lib/util/enum.util'
export * from './lib/util/evm.util'
Expand Down
20 changes: 12 additions & 8 deletions packages/policy-engine-shared/src/lib/schema/data-store.schema.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { z } from 'zod'
import { entitiesSchema } from './entity.schema'

export const jsonWebKeySetSchema = z.object({
kty: z.string().describe('Key Type (e.g. RSA or EC'),
use: z.string(),
kid: z.string().describe('Arbitrary key ID'),
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'),
n: z.string().describe('Key modulus'),
e: z.string().describe('Key exponent')
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 dataStoreProtocolSchema = z.enum(['file'])

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

export const entityDataSchema = z.object({
Expand All @@ -32,6 +36,6 @@ export const entitySignatureSchema = z.object({

export const entityJsonWebKeySetSchema = z.object({
entity: z.object({
keys: z.array(jsonWebKeySetSchema)
keys: z.array(jsonWebKeySchema)
})
})
4 changes: 2 additions & 2 deletions packages/policy-engine-shared/src/lib/type/data-store.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {
entityDataSchema,
entityJsonWebKeySetSchema,
entitySignatureSchema,
jsonWebKeySetSchema
jsonWebKeySchema
} from '../schema/data-store.schema'

export type JsonWebKeySet = z.infer<typeof jsonWebKeySetSchema>
export type JsonWebKey = z.infer<typeof jsonWebKeySchema>

export type DataStoreProtocol = z.infer<typeof dataStoreProtocolSchema>
export const DataStoreProtocol = dataStoreProtocolSchema.Enum
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { toBytes, toHex } from 'viem/utils'
7 changes: 7 additions & 0 deletions packages/signature/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export const Alg = {

export type Alg = (typeof Alg)[keyof typeof Alg]

export const Use = {
SIG: 'sig',
ENC: 'enc'
} as const

export type Use = (typeof Use)[keyof typeof Use]

/**
* Defines the header of JWT.
*
Expand Down
8 changes: 4 additions & 4 deletions packages/signature/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Alg, AlgTypes, Curves } from './types'
import { Alg, KeyTypes, Curves } from './types'

export const algToJwk = (
alg: Alg
Expand All @@ -10,19 +10,19 @@ export const algToJwk = (
switch (alg) {
case Alg.ES256K:
return {
kty: AlgTypes.EC,
kty: KeyTypes.EC,
crv: Curves.SECP256K1,
alg: Alg.ES256K
}
case Alg.ES256:
return {
kty: AlgTypes.EC,
kty: KeyTypes.EC,
crv: Curves.P256,
alg: Alg.ES256
}
case Alg.RS256:
return {
kty: AlgTypes.RSA,
kty: KeyTypes.RSA,
alg: Alg.RS256
}
default:
Expand Down

0 comments on commit d098e00

Please sign in to comment.