Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/SDK-7 #25

Merged
merged 12 commits into from
Apr 25, 2024
32 changes: 30 additions & 2 deletions packages/did-provider-ebsi/__tests__/EbsiDidProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DIDManager, MemoryDIDStore } from '@veramo/did-manager'
import { MemoryKeyStore, MemoryPrivateKeyStore } from '@veramo/key-manager'
import { SphereonKeyManager } from '@sphereon/ssi-sdk-ext.key-manager'
import { SphereonKeyManagementSystem } from '@sphereon/ssi-sdk-ext.kms-local'
import { EbsiDidProvider } from '../src'
import { EbsiDidProvider, EbsiPublicKeyPurpose } from '../src'

const DID_METHOD = 'did:ebsi'
const PRIVATE_KEY_HEX = '7dd923e40f4615ac496119f7e793cc2899e99b64b88ca8603db986700089532b'
Expand Down Expand Up @@ -37,7 +37,35 @@ describe('@sphereon/did-provider-ebsi', () => {
const identifier: IIdentifier = await agent.didManagerCreate()

expect(identifier).toBeDefined()
expect(identifier.keys.length).toBe(1)
expect(identifier.keys.length).toBe(2)
const secp256k1 = identifier.keys.find((key) => key.type === 'Secp256k1')
const secp256r1 = identifier.keys.find((key) => key.type === 'Secp256r1')
expect(secp256k1).toBeDefined()
expect(secp256k1).toEqual(
expect.objectContaining({
kid: expect.any(String),
kms: 'mem',
type: 'Secp256k1',
publicKeyHex: expect.any(String),
meta: {
algorithms: ['ES256'],
purposes: [EbsiPublicKeyPurpose.CapabilityInvocation],
},
})
)
expect(secp256k1).toBeDefined()
expect(secp256r1).toEqual(
expect.objectContaining({
kid: expect.any(String),
kms: 'mem',
type: 'Secp256r1',
publicKeyHex: expect.any(String),
meta: {
algorithms: ['ES256K', 'ES256K-R', 'eth_signTransaction', 'eth_signTypedData', 'eth_signMessage', 'eth_rawSign'],
purposes: [EbsiPublicKeyPurpose.AssertionMethod, EbsiPublicKeyPurpose.Authentication],
},
})
)
})

it('should create consistent identifier with provided key', async () => {
Expand Down
140 changes: 103 additions & 37 deletions packages/did-provider-ebsi/src/EbsiDidProvider.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { IAgentContext, IIdentifier, IKeyManager } from '@veramo/core'
import { IAgentContext, IIdentifier, IKeyManager, MinimalImportableKey } from '@veramo/core'
import Debug from 'debug'
import { AbstractIdentifierProvider } from '@veramo/did-manager/build/abstract-identifier-provider'
import { DIDDocument } from 'did-resolver'
import { IKey, IService } from '@veramo/core/build/types/IIdentifier'
import * as u8a from 'uint8arrays'
import { ebsiDIDSpecInfo, IContext, ICreateIdentifierArgs } from './types'
import { generateEbsiPrivateKeyHex, toMethodSpecificId } from './functions'
import { ebsiDIDSpecInfo, EbsiKeyType, EbsiPublicKeyPurpose, IContext, ICreateIdentifierArgs, IKeyOpts } from './types'
import { generateEbsiPrivateKeyHex, generateMethodSpecificId } from './functions'

const debug = Debug('sphereon:did-provider-ebsi')

Expand All @@ -17,48 +17,56 @@ export class EbsiDidProvider extends AbstractIdentifierProvider {
this.defaultKms = options.defaultKms
}

async createIdentifier(
{
kms,
options,
}: {
kms?: string
options?: ICreateIdentifierArgs
},
context: IContext
): Promise<Omit<IIdentifier, 'provider'>> {
if (!options?.type || options.type === ebsiDIDSpecInfo.V1) {
let privateKeyHex = generateEbsiPrivateKeyHex(
ebsiDIDSpecInfo.V1,
options?.options?.key?.privateKeyHex ? u8a.fromString(options.options.key.privateKeyHex, 'base16') : undefined
async createIdentifier(args: ICreateIdentifierArgs, context: IContext): Promise<Omit<IIdentifier, 'provider'>> {
const { type, options, kms, alias } = { ...args }
if (!type || type === ebsiDIDSpecInfo.V1) {
const secp256k1ManagedKeyInfo = await this.generateEbsiKeyPair(
{
keyOpts: options?.secp256k1Key,
keyType: 'Secp256k1',
kms,
},
context
)
if (privateKeyHex.startsWith('0x')) {
privateKeyHex = privateKeyHex.substring(2)
}
if (!privateKeyHex || privateKeyHex.length !== 64) {
throw Error('Private key should be 32 bytes / 64 chars hex')
}
const key = await context.agent.keyManagerImport({
type: 'Secp256k1',
kms: this.assertedKms(kms),
// meta: options?.options?.meta,
kid: options?.options?.key?.kid,
privateKeyHex,
})

const methodSpecificId = toMethodSpecificId(ebsiDIDSpecInfo.V1, options?.options?.methodSpecificId)
const secp256r1ManagedKeyInfo = await this.generateEbsiKeyPair(
{
keyOpts: options?.secp256r1Key,
keyType: 'Secp256r1',
kms,
},
context
)

const methodSpecificId = generateMethodSpecificId(ebsiDIDSpecInfo.V1)
const identifier: Omit<IIdentifier, 'provider'> = {
did: ebsiDIDSpecInfo.V1.method + methodSpecificId,
controllerKeyId: key.kid,
keys: [key],
controllerKeyId: secp256k1ManagedKeyInfo.kid,
keys: [secp256k1ManagedKeyInfo, secp256r1ManagedKeyInfo],
alias,
services: [],
}
debug('Created', identifier.did)
return identifier
} else if (options.type === ebsiDIDSpecInfo.KEY) {
throw Error(`Type ${options.type} not supported. Please use @sphereon/ssi-sdk-ext.did-provider-key for Natural Person EBSI DIDs`)
} else if (type === ebsiDIDSpecInfo.KEY) {
throw Error(`Type ${type} not supported. Please use @sphereon/ssi-sdk-ext.did-provider-key for Natural Person EBSI DIDs`)
}
throw Error(`Type ${options.type} not supported`)
throw Error(`Type ${type} not supported`)
}

private async generateEbsiKeyPair(args: { keyOpts?: IKeyOpts; keyType: EbsiKeyType; kms?: string }, context: IAgentContext<IKeyManager>) {
const { keyOpts, keyType, kms } = args
let privateKeyHex = generateEbsiPrivateKeyHex(
ebsiDIDSpecInfo.V1,
keyOpts?.privateKeyHex ? u8a.fromString(keyOpts.privateKeyHex, 'base16') : undefined
)
if (privateKeyHex.startsWith('0x')) {
privateKeyHex = privateKeyHex.substring(2)
}
if (!privateKeyHex || privateKeyHex.length !== 64) {
throw Error('Private key should be 32 bytes / 64 chars hex')
}
const importableKey = this.assertedKey({ key: { ...keyOpts, privateKeyHex }, type: keyType, kms })
return await context.agent.keyManagerImport(importableKey)
}

addKey(
Expand Down Expand Up @@ -120,11 +128,69 @@ export class EbsiDidProvider extends AbstractIdentifierProvider {
throw Error(`Not (yet) implemented for the EBSI did provider`)
}

private assertedKey = (args: { key?: IKeyOpts; type: EbsiKeyType; kms?: string }): MinimalImportableKey => {
const { key, type, kms } = args
const minimalImportableKey: Partial<MinimalImportableKey> = { ...key } ?? {}
minimalImportableKey.kms = this.assertedKms(kms)
minimalImportableKey.type = this.setDefaultKeyType({ key, type })
minimalImportableKey.meta = { purposes: this.assertedPurposes({ key }) ?? this.setDefaultPurposes({ key, type }) }
return minimalImportableKey as MinimalImportableKey
}

private assertedKms(kms?: string) {
const result = kms ?? this.defaultKms
if (!!result) {
return result
}
throw Error('no KMS supplied')
}

private setDefaultKeyType = (args: { key?: IKeyOpts; type: EbsiKeyType }): EbsiKeyType => {
if (!args.key?.type) {
return args.type
}
nklomp marked this conversation as resolved.
Show resolved Hide resolved
return args.key.type
}

private assertedPurposes = (args: { key?: IKeyOpts }): EbsiPublicKeyPurpose[] | undefined => {
const { key } = args
if (key?.purposes && key.purposes.length > 0) {
switch (key.type) {
case 'Secp256k1': {
if (key?.purposes && key.purposes.length > 0 && key.purposes?.includes(EbsiPublicKeyPurpose.CapabilityInvocation)) {
return key.purposes
}
throw new Error(`Secp256k1 key requires ${EbsiPublicKeyPurpose.CapabilityInvocation} purpose`)
}
case 'Secp256r1': {
if (
key?.purposes &&
key.purposes.length > 0 &&
key.purposes.every((purpose) => [EbsiPublicKeyPurpose.AssertionMethod, EbsiPublicKeyPurpose.Authentication].includes(purpose))
) {
return key.purposes
}
throw new Error(`Secp256r1 key requires ${[EbsiPublicKeyPurpose.AssertionMethod, EbsiPublicKeyPurpose.Authentication].join(', ')} purposes`)
}
default:
throw new Error(`Unsupported key type: ${key.type}`)
}
}
return key?.purposes
}

private setDefaultPurposes = (args: { key?: IKeyOpts; type: EbsiKeyType }): EbsiPublicKeyPurpose[] => {
const { key, type } = args
if (!key?.purposes || key.purposes.length === 0) {
switch (type) {
case 'Secp256k1':
return [EbsiPublicKeyPurpose.CapabilityInvocation]
case 'Secp256r1':
return [EbsiPublicKeyPurpose.AssertionMethod, EbsiPublicKeyPurpose.Authentication]
default:
throw new Error(`Unsupported key type: ${key?.type}`)
}
}
return key.purposes
}
}
8 changes: 1 addition & 7 deletions packages/did-provider-ebsi/src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,10 @@ import { randomBytes } from '@ethersproject/random'
import * as u8a from 'uint8arrays'
import { base58btc } from 'multiformats/bases/base58'

export function toMethodSpecificId(specInfo?: EbsiDidSpecInfo, methodSpecificId?: string): string {
export function generateMethodSpecificId(specInfo?: EbsiDidSpecInfo): string {
const spec = specInfo ?? ebsiDIDSpecInfo.V1
const length = spec.didLength ?? 16

if (methodSpecificId) {
if (methodSpecificId.length < length || methodSpecificId.length > length + 1) {
throw Error(`Invalid did length supplied (${methodSpecificId.length}. Expected ${length} for ${spec.type}`)
}
return methodSpecificId.length === length ? `z${methodSpecificId}` : methodSpecificId
}
const result = new Uint8Array(length + (spec.version ? 1 : 0))
if (spec.version) {
result.set([spec.version])
Expand Down
23 changes: 16 additions & 7 deletions packages/did-provider-ebsi/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IAgentContext, IKeyManager, MinimalImportableKey } from '@veramo/core'
import { IAgentContext, IKeyManager, MinimalImportableKey, TKeyType } from '@veramo/core'

export type IContext = IAgentContext<IKeyManager>

Expand Down Expand Up @@ -27,11 +27,9 @@ export const ebsiDIDSpecInfo: Record<string, EbsiDidSpecInfo> = {
},
}

export interface IKeyOpts {
methodSpecificId?: string // method specific id for import
key?: WithRequiredProperty<Partial<MinimalImportableKey>, 'privateKeyHex'> // Optional key to import with only privateKeyHex mandatory. If not specified a key with random kid will be created
/*type?: Key // The key type. Defaults to Secp256k1
use?: KeyUse // The key use*/
export interface IKeyOpts extends WithRequiredProperty<Partial<MinimalImportableKey>, 'privateKeyHex'> {
type?: EbsiKeyType
purposes?: EbsiPublicKeyPurpose[]
}

// Needed to make a single property required
Expand All @@ -43,5 +41,16 @@ export interface ICreateIdentifierArgs {
kms?: string
alias?: string
type?: EbsiDidSpecInfo
options?: IKeyOpts
options?: {
secp256k1Key?: IKeyOpts
secp256r1Key?: IKeyOpts
}
}

export type EbsiKeyType = Extract<TKeyType, 'Secp256k1' | 'Secp256r1'>

export enum EbsiPublicKeyPurpose {
Authentication = 'authentication',
AssertionMethod = 'assertionMethod',
nklomp marked this conversation as resolved.
Show resolved Hide resolved
CapabilityInvocation = 'capabilityInvocation',
}
Loading