diff --git a/packages/did-provider-ebsi/package.json b/packages/did-provider-ebsi/package.json index 1167473c..118eddc2 100644 --- a/packages/did-provider-ebsi/package.json +++ b/packages/did-provider-ebsi/package.json @@ -21,6 +21,7 @@ "debug": "^4.3.4", "did-resolver": "^4.1.0", "ethers": "^6.11.1", + "jose": "^4.14.6", "multiformats": "9.9.0", "uint8arrays": "3.1.1" }, diff --git a/packages/did-provider-ebsi/src/EbsiDidProvider.ts b/packages/did-provider-ebsi/src/EbsiDidProvider.ts index 3797c08e..0e3c95e9 100644 --- a/packages/did-provider-ebsi/src/EbsiDidProvider.ts +++ b/packages/did-provider-ebsi/src/EbsiDidProvider.ts @@ -1,11 +1,21 @@ -import { IAgentContext, IIdentifier, IKeyManager, MinimalImportableKey } from '@veramo/core' +import { IAgentContext, IDIDManager, IIdentifier, IKeyManager, ManagedKeyInfo, 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, EbsiKeyType, EbsiPublicKeyPurpose, IContext, ICreateIdentifierArgs, IKeyOpts } from './types' -import { generateEbsiPrivateKeyHex, toMethodSpecificId } from './functions' +import { ebsiDIDSpecInfo, EbsiKeyType, EbsiPublicKeyPurpose, IContext, ICreateIdentifierArgs, IKeyOpts, Response, Response200 } from './types' +import { formatEbsiPublicKey, generateEbsiPrivateKeyHex, toMethodSpecificId } from './functions' +import { + addVerificationMethod, + addVerificationMethodRelationship, + insertDidDocument, + sendSignedTransaction, + updateBaseDocument, +} from './services/EbsiRPCService' +import { toJwk } from '@sphereon/ssi-sdk-ext.key-utils' +import { calculateJwkThumbprint } from 'jose' +import { Transaction } from 'ethers' const debug = Debug('sphereon:did-provider-ebsi') @@ -19,6 +29,7 @@ export class EbsiDidProvider extends AbstractIdentifierProvider { async createIdentifier(args: ICreateIdentifierArgs, context: IContext): Promise> { const { type, options, kms, alias } = { ...args } + if (!type || type === ebsiDIDSpecInfo.V1) { const secp256k1ManagedKeyInfo = await this.generateEbsiKeyPair( { @@ -45,6 +56,14 @@ export class EbsiDidProvider extends AbstractIdentifierProvider { alias, services: [], } + const id = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + + if (options === undefined) { + throw new Error(`Options must be provided ${JSON.stringify(options)}`) + } + + await this.createEbsiDid({ identifier, secp256k1ManagedKeyInfo, secp256r1ManagedKeyInfo, id, from: options.from }, context) + debug('Created', identifier.did) return identifier } else if (type === ebsiDIDSpecInfo.KEY) { @@ -53,6 +72,106 @@ export class EbsiDidProvider extends AbstractIdentifierProvider { throw Error(`Type ${type} not supported`) } + async createEbsiDid( + args: { + identifier: Omit + secp256k1ManagedKeyInfo: ManagedKeyInfo + secp256r1ManagedKeyInfo: ManagedKeyInfo + id: number + from: string + baseDocument?: string + }, + context: IContext + ): Promise { + const insertDidDocTransaction = await insertDidDocument({ + params: [ + { + from: args.from, + did: args.identifier.did, + baseDocument: + args.baseDocument ?? JSON.stringify({ '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'] }), + vMethoddId: await calculateJwkThumbprint(toJwk(args.secp256k1ManagedKeyInfo.publicKeyHex, 'Secp256k1')), + isSecp256k1: true, + publicKey: formatEbsiPublicKey({ key: args.secp256k1ManagedKeyInfo, type: 'Secp256k1' }), + notBefore: 1, + notAfter: 1, + }, + ], + id: args.id, + }) + + await this.sendTransaction({ docTransactionResponse: insertDidDocTransaction, kid: args.secp256r1ManagedKeyInfo.kid, id: args.id }, context) + + const addVerificationMethodTransaction = await addVerificationMethod({ + params: [ + { + from: args.from, // required + did: args.identifier.did, + isSecp256k1: true, + vMethoddId: await calculateJwkThumbprint(toJwk(args.secp256k1ManagedKeyInfo.publicKeyHex, 'Secp256k1')), + publicKey: formatEbsiPublicKey({ key: args.secp256k1ManagedKeyInfo, type: 'Secp256k1' }), + }, + ], + id: args.id, + }) + + await this.sendTransaction( + { docTransactionResponse: addVerificationMethodTransaction, kid: args.secp256r1ManagedKeyInfo.kid, id: args.id }, + context + ) + + const addVerificationMethodRelationshipTransaction = await addVerificationMethodRelationship({ + params: [ + { + from: args?.from, + did: args.identifier.did, + vMethoddId: await calculateJwkThumbprint(toJwk(args.secp256r1ManagedKeyInfo.publicKeyHex, 'Secp256r1')), + name: 'assertionMethod', + notAfter: 1, + notBefore: 1, + }, + ], + id: args.id, + }) + + await this.sendTransaction( + { docTransactionResponse: addVerificationMethodRelationshipTransaction, kid: args.secp256r1ManagedKeyInfo.kid, id: args.id }, + context + ) + } + + private sendTransaction = async (args: { docTransactionResponse: Response; kid: string; id: number }, context: IContext) => { + if ('status' in args.docTransactionResponse) { + throw new Error(JSON.stringify(args.docTransactionResponse, null, 2)) + } + const unsignedTransaction = (args.docTransactionResponse as Response200).result + + const signedRawTransaction = await context.agent.keyManagerSignEthTX({ + kid: args.kid, + transaction: unsignedTransaction, + }) + + const { r, s, v } = Transaction.from(signedRawTransaction).signature! + + const sTResponse = await sendSignedTransaction({ + params: [ + { + protocol: 'eth', + unsignedTransaction: unsignedTransaction, + r, + s, + v: v.toString(), + signedRawTransaction, + }, + ], + id: args.id, + }) + + if ('status' in sTResponse) { + throw new Error(JSON.stringify(sTResponse, null, 2)) + } + } + private async generateEbsiKeyPair(args: { keyOpts?: IKeyOpts; keyType: EbsiKeyType; kms?: string }, context: IAgentContext) { const { keyOpts, keyType, kms } = args let privateKeyHex = generateEbsiPrivateKeyHex( @@ -117,14 +236,28 @@ export class EbsiDidProvider extends AbstractIdentifierProvider { throw Error(`Not (yet) implemented for the EBSI did provider`) } - updateIdentifier( + // TODO How does it work? Not inferable from the api: https://hub.ebsi.eu/apis/pilot/did-registry/v5/post-jsonrpc#updatebasedocument + async updateIdentifier( args: { did: string document: Partial options?: { [p: string]: any } }, - context: IAgentContext + context: IAgentContext ): Promise { + const id = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + await updateBaseDocument({ + params: [ + { + from: args.options?.from ?? 'eth', + did: args.did, + baseDocument: + args.options?.baseDocument ?? + JSON.stringify({ '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'] }), + }, + ], + id, + }) throw Error(`Not (yet) implemented for the EBSI did provider`) } diff --git a/packages/did-provider-ebsi/src/services/EbsiRPCService.ts b/packages/did-provider-ebsi/src/services/EbsiRPCService.ts index 16c6f174..aec8ae2c 100644 --- a/packages/did-provider-ebsi/src/services/EbsiRPCService.ts +++ b/packages/did-provider-ebsi/src/services/EbsiRPCService.ts @@ -1,13 +1,16 @@ import { Headers } from 'cross-fetch' import { AddVerificationMethodParams, - AddVerificationMethodRelationshipParams, GetDidDocumentParams, GetDidDocumentsParams, + AddVerificationMethodRelationshipParams, + GetDidDocumentParams, + GetDidDocumentsParams, InsertDidDocumentParams, SendSignedTransactionParams, UpdateBaseDocumentParams, - Response, GetDidDocumentsResponse + Response, + GetDidDocumentsResponse, } from '../types' -import {DIDDocument} from "did-resolver"; +import { DIDDocument } from 'did-resolver' /** * @constant {string} jsonrpc @@ -43,7 +46,7 @@ export const insertDidDocument = async (args: { params: InsertDidDocumentParams[ * "didr_write" scope. * @param {{ params: UpdateBaseDocumentParams[], id:number }} args */ -export const updateBaseDocument = async (args: { params: UpdateBaseDocumentParams; id: number }): Promise => { +export const updateBaseDocument = async (args: { params: UpdateBaseDocumentParams[]; id: number }): Promise => { const { params, id } = args const options = { method: 'POST', @@ -147,14 +150,8 @@ export const getDidDocument = async (args: GetDidDocumentParams): Promise => { const { offset, size, controller } = args - const query = `?${[ - offset && `page[after]=${offset}`, - size && `page[size]=${size}`, - controller && `controller=${controller}`] - .filter(Boolean) - .join('&')}` + const query = `?${[offset && `page[after]=${offset}`, size && `page[size]=${size}`, controller && `controller=${controller}`] + .filter(Boolean) + .join('&')}` return await (await fetch(`https://api-pilot.ebsi.eu/did-registry/v5/identifiers${query}`)).json() } - -// Fixed the args vs params clash -// Key rotation? diff --git a/packages/did-provider-ebsi/src/types.ts b/packages/did-provider-ebsi/src/types.ts index 5d5a7bb2..a888ae9f 100644 --- a/packages/did-provider-ebsi/src/types.ts +++ b/packages/did-provider-ebsi/src/types.ts @@ -82,6 +82,8 @@ export interface ICreateIdentifierArgs { methodSpecificId?: string secp256k1Key?: IKeyOpts secp256r1Key?: IKeyOpts + from: string + baseDocument?: string } } @@ -242,7 +244,7 @@ export type ResponseNot200 = { * @property {string} validAt */ export type GetDidDocumentParams = { - did: string; + did: string validAt?: string } @@ -254,9 +256,9 @@ export type GetDidDocumentParams = { * @property {string} controller Filter by controller DID. */ export type GetDidDocumentsParams = { - offset?: string; - size?: number; - controller?: string; + offset?: string + size?: number + controller?: string } /** @@ -281,10 +283,10 @@ export type Item = { * @property {string} last - The link to the last page */ export type Links = { - first: string; - prev: string; - next: string; - last: string; + first: string + prev: string + next: string + last: string } /** @@ -297,11 +299,11 @@ export type Links = { * @property {Links} links - The links related to pagination */ export type GetDidDocumentsResponse = { - self: string; - items: Item[]; - total: number; - pageSize: number; - links: Links; + self: string + items: Item[] + total: number + pageSize: number + links: Links } -export type Response = Response200 | ResponseNot200 | GetDidDocumentsResponse; +export type Response = Response200 | ResponseNot200 | GetDidDocumentsResponse diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1bc63ec8..2b06eb8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,6 +185,9 @@ importers: ethers: specifier: ^6.11.1 version: 6.11.1 + jose: + specifier: ^4.14.6 + version: 4.14.6 multiformats: specifier: 9.9.0 version: 9.9.0 @@ -11253,7 +11256,6 @@ packages: /jose@4.14.6: resolution: {integrity: sha512-EqJPEUlZD0/CSUMubKtMaYUOtWe91tZXTWMJZoKSbLk+KtdhNdcvppH8lA9XwVu2V4Ailvsj0GBZJ2ZwDjfesQ==} - dev: true /js-sha256@0.9.0: resolution: {integrity: sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==}