Skip to content

Commit

Permalink
feat: Allow to cleanup keys and have ephemeral keys. Remove dep on km…
Browse files Browse the repository at this point in the history
…s-local from KMS. Always calculate jwkThumbprints no matter the KMS used
  • Loading branch information
nklomp committed Aug 11, 2024
1 parent adfc57a commit 94414ff
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 25 deletions.
2 changes: 1 addition & 1 deletion packages/key-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
"generate-plugin-schema": "sphereon dev generate-plugin-schema"
},
"dependencies": {
"@sphereon/ssi-sdk-ext.kms-local": "workspace:*",
"@veramo/core": "4.2.0",
"@veramo/key-manager": "4.2.0"
},
"devDependencies": {
"@sphereon/ssi-sdk-ext.kms-local": "workspace:*",
"@mattrglobal/bbs-signatures": "^1.3.1",
"@sphereon/ssi-sdk-ext.key-utils": "workspace:*",
"@sphereon/ssi-sdk.dev": "0.28.0"
Expand Down
81 changes: 65 additions & 16 deletions packages/key-manager/src/agent/SphereonKeyManager.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { KeyManager as VeramoKeyManager, AbstractKeyManagementSystem, AbstractKeyStore } from '@veramo/key-manager'

import { IKey, ManagedKeyInfo, TKeyType } from '@veramo/core'
import { KeyType, SphereonKeyManagementSystem } from '@sphereon/ssi-sdk-ext.kms-local'
import { ISphereonKeyManager, ISphereonKeyManagerSignArgs, ISphereonKeyManagerVerifyArgs } from '../types/ISphereonKeyManager'
import { calculateJwkThumbprintForKey } from '@sphereon/ssi-sdk-ext.key-utils'
import { IKey, KeyMetadata, ManagedKeyInfo } from '@veramo/core'
import { AbstractKeyManagementSystem, AbstractKeyStore, KeyManager as VeramoKeyManager } from '@veramo/key-manager'
import {
hasKeyOptions,
ISphereonKeyManager,
ISphereonKeyManagerCreateArgs,
ISphereonKeyManagerHandleExpirationsArgs,
ISphereonKeyManagerSignArgs,
ISphereonKeyManagerVerifyArgs,
} from '../types/ISphereonKeyManager'

export const sphereonKeyManagerMethods: Array<string> = [
'keyManagerCreate',
'keyManagerImport',
'keyManagerSign',
'keyManagerVerify',
'keyManagerListKeys',
'keyManagerHandleExpirations',
]

export class SphereonKeyManager extends VeramoKeyManager {
// local store reference, given the superclass store is private, and we need additional functions/calls
private localStore: AbstractKeyStore
private readonly availableKMSes: Record<string, AbstractKeyManagementSystem>
readonly localMethods: ISphereonKeyManager
Expand All @@ -27,35 +35,76 @@ export class SphereonKeyManager extends VeramoKeyManager {
this.localMethods = <ISphereonKeyManager>(<unknown>methods)
}

private getAvailableKms(name: string): AbstractKeyManagementSystem {
const kms = this.availableKMSes[name]
if (!kms) {
throw Error(`invalid_argument: This agent has no registered KeyManagementSystem with name='${name}'`)
override async keyManagerCreate(args: ISphereonKeyManagerCreateArgs): Promise<ManagedKeyInfo> {
const kms = this.getKmsByName(args.kms)
const meta: KeyMetadata = { ...args.meta, ...(args.opts && { opts: args.opts }) }
if (hasKeyOptions(meta) && meta.opts?.ephemeral && !meta.opts.expiration?.removalDate) {
// Make sure we set a delete date on an ephemeral key
meta.opts = {
...meta.opts,
expiration: { ...meta.opts?.expiration, removalDate: new Date(Date.now() + 5 * 60 * 1000) },
}
}
return kms
const partialKey = await kms.createKey({ type: args.type, meta })
const key: IKey = { ...partialKey, kms: args.kms }
key.meta = { ...meta, ...key.meta }
key.meta.jwkThumbprint = key.meta.jwkThumbprint ?? calculateJwkThumbprintForKey({ key })

await this.localStore.import(key)
if (key.privateKeyHex) {
// Make sure to not export the private key
delete key.privateKeyHex
}
return key
}

//FIXME extend the IKeyManagerSignArgs.data to be a string or array of strings

async keyManagerSign(args: ISphereonKeyManagerSignArgs): Promise<string> {
const keyInfo: IKey = (await this.localStore.get({ kid: args.keyRef })) as IKey
const kms = this.getAvailableKms(keyInfo.kms)
if (keyInfo.type === <TKeyType>KeyType.Bls12381G2) {
const kms = this.getKmsByName(keyInfo.kms)
if (keyInfo.type === 'Bls12381G2') {
return await kms.sign({ keyRef: keyInfo, data: Uint8Array.from(Buffer.from(args.data)) })
}
// @ts-ignore
// @ts-ignore // we can pass in uint8arrays as well, which the super also can handle but does not expose in its types
return await super.keyManagerSign(args)
}

async keyManagerVerify(args: ISphereonKeyManagerVerifyArgs): Promise<boolean> {
const kms = this.getAvailableKms(args.kms)
if (('verify' in kms && typeof kms.verify === 'function') || kms instanceof SphereonKeyManagementSystem) {
const kms = this.getKmsByName(args.kms)
if ('verify' in kms && typeof kms.verify === 'function') {
// @ts-ignore
return await kms.verify(args)
}
throw Error(`KMS ${kms} does not support verification`)
}

async keyManagerListKeys(): Promise<ManagedKeyInfo[]> {
return this.localStore.list({}) // FIXME there are no args it seems
return this.localStore.list({})
}

async keyManagerHandleExpirations(args: ISphereonKeyManagerHandleExpirationsArgs): Promise<Array<ManagedKeyInfo>> {
const keys = await this.keyManagerListKeys()
const expiredKeys = keys
.filter((key) => hasKeyOptions(key.meta))
.filter((key) => {
if (hasKeyOptions(key.meta) && key.meta?.opts?.expiration) {
const expiration = key.meta.opts.expiration
return !(expiration.expiryDate && expiration.expiryDate.getMilliseconds() > Date.now())
}
return false
})
if (args.skipRemovals !== true) {
await Promise.all(expiredKeys.map((key) => this.keyManagerDelete({ kid: key.kid })))
}
return keys
}

private getKmsByName(name: string): AbstractKeyManagementSystem {
const kms = this.availableKMSes[name]
if (!kms) {
throw Error(`invalid_argument: This agent has no registered KeyManagementSystem with name='${name}'`)
}
return kms
}
}
40 changes: 38 additions & 2 deletions packages/key-manager/src/types/ISphereonKeyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ManagedKeyInfo } from '@veramo/core'
export type PartialKey = ManagedKeyInfo & { privateKeyHex: string }

export interface ISphereonKeyManager extends IKeyManager, IPluginMethodMap {
keyManagerCreate(args: IKeyManagerCreateArgs): Promise<PartialKey>
keyManagerCreate(args: ISphereonKeyManagerCreateArgs): Promise<PartialKey>

keyManagerImport(key: MinimalImportableKey): Promise<PartialKey>

Expand All @@ -19,13 +19,34 @@ export interface ISphereonKeyManager extends IKeyManager, IPluginMethodMap {
keyManagerVerify(args: ISphereonKeyManagerVerifyArgs): Promise<boolean>

keyManagerListKeys(): Promise<Array<ManagedKeyInfo>>

/**
* Set keys to expired and remove keys eligible for deletion.
* @param args
*/
keyManagerHandleExpirations(args: ISphereonKeyManagerHandleExpirationsArgs): Promise<Array<ManagedKeyInfo>>
}

export interface IkeyOptions {
/**
* Is this a temporary key?
*/
ephemeral?: boolean

/**
* Expiration and remove the key
*/
expiration?: {
expiryDate?: Date
removalDate?: Date
}
}

/**
* Input arguments for {@link ISphereonKeyManager.keyManagerCreate | keyManagerCreate}
* @public
*/
export interface IKeyManagerCreateArgs {
export interface ISphereonKeyManagerCreateArgs {
/**
* Key type
*/
Expand All @@ -36,12 +57,21 @@ export interface IKeyManagerCreateArgs {
*/
kms: string

/**
* Key options
*/
opts?: IkeyOptions

/**
* Optional. Key meta data
*/
meta?: KeyMetadata
}

export function hasKeyOptions(object: any): object is { opts?: IkeyOptions } {
return object!! && 'opts' in object && ('ephemeral' in object.opts || 'expiration' in object.opts)
}

/**
* Input arguments for {@link ISphereonKeyManager.keyManagerGet | keyManagerGet}
* @public
Expand Down Expand Up @@ -76,6 +106,10 @@ export interface ISphereonKeyManagerSignArgs extends IKeyManagerSignArgs {
data: string | Uint8Array
}

export interface ISphereonKeyManagerHandleExpirationsArgs {
skipRemovals?: boolean
}

export interface ISphereonKeyManagerVerifyArgs {
kms: string
publicKeyHex: string
Expand All @@ -84,3 +118,5 @@ export interface ISphereonKeyManagerVerifyArgs {
data: Uint8Array
signature: string
}

export const isDefined = <T extends unknown>(object: T | undefined): object is T => object !== undefined
6 changes: 3 additions & 3 deletions packages/kms-musap-rn/src/MusapKeyManagerSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export class MusapKeyManagementSystem extends AbstractKeyManagementSystem {
}
}

mapKeyTypeToAlgorithmType = (type: TKeyType): KeyAlgorithmType => {
private mapKeyTypeToAlgorithmType = (type: TKeyType): KeyAlgorithmType => {
switch (type) {
case 'Secp256k1':
return 'ECCP256K1'
Expand All @@ -92,7 +92,7 @@ export class MusapKeyManagementSystem extends AbstractKeyManagementSystem {
}
}

mapAlgorithmTypeToKeyType = (type: KeyAlgorithm): TKeyType => {
private mapAlgorithmTypeToKeyType = (type: KeyAlgorithm): TKeyType => {
switch (type) {
case 'eccp256k1':
return 'Secp256k1'
Expand All @@ -107,7 +107,7 @@ export class MusapKeyManagementSystem extends AbstractKeyManagementSystem {

async deleteKey({ kid }: { kid: string }): Promise<boolean> {
try {
await this.musapKeyStore.removeKey(kid)
this.musapKeyStore.removeKey(kid)
return true
} catch (error) {
console.warn('Failed to delete key:', error)
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

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

0 comments on commit 94414ff

Please sign in to comment.