diff --git a/packages/core/package.json b/packages/core/package.json index f38ce37605..360da5bd98 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -32,7 +32,7 @@ "@stablelib/ed25519": "^1.0.2", "@stablelib/random": "^1.0.1", "@stablelib/sha256": "^1.0.1", - "@types/indy-sdk": "^1.16.19", + "@types/indy-sdk": "^1.16.21", "@types/node-fetch": "^2.5.10", "@types/ws": "^7.4.6", "abort-controller": "^3.0.0", diff --git a/packages/core/src/modules/connections/DidExchangeProtocol.ts b/packages/core/src/modules/connections/DidExchangeProtocol.ts index a1a865ccd2..6b308adced 100644 --- a/packages/core/src/modules/connections/DidExchangeProtocol.ts +++ b/packages/core/src/modules/connections/DidExchangeProtocol.ts @@ -2,6 +2,7 @@ import type { AgentContext } from '../../agent' import type { ResolvedDidCommService } from '../../agent/MessageSender' import type { InboundMessageContext } from '../../agent/models/InboundMessageContext' import type { ParsedMessageType } from '../../utils/messageType' +import type { PeerDidCreateOptions } from '../dids' import type { OutOfBandDidCommService } from '../oob/domain/OutOfBandDidCommService' import type { OutOfBandRecord } from '../oob/repository' import type { ConnectionRecord } from './repository' @@ -16,14 +17,17 @@ import { Logger } from '../../logger' import { inject, injectable } from '../../plugins' import { JsonEncoder } from '../../utils/JsonEncoder' import { JsonTransformer } from '../../utils/JsonTransformer' -import { DidDocument } from '../dids' -import { DidDocumentRole } from '../dids/domain/DidDocumentRole' -import { createDidDocumentFromServices } from '../dids/domain/createPeerDidFromServices' +import { + DidDocument, + DidRegistrarService, + DidDocumentRole, + createPeerDidDocumentFromServices, + DidKey, + getNumAlgoFromPeerDid, + PeerDidNumAlgo, +} from '../dids' import { getKeyDidMappingByVerificationMethod } from '../dids/domain/key-type' import { didKeyToInstanceOfKey } from '../dids/helpers' -import { DidKey } from '../dids/methods/key/DidKey' -import { getNumAlgoFromPeerDid, PeerDidNumAlgo } from '../dids/methods/peer/didPeer' -import { didDocumentJsonToNumAlgo1Did } from '../dids/methods/peer/peerDidNumAlgo1' import { DidRecord, DidRepository } from '../dids/repository' import { OutOfBandRole } from '../oob/domain/OutOfBandRole' import { OutOfBandState } from '../oob/domain/OutOfBandState' @@ -48,17 +52,20 @@ interface DidExchangeRequestParams { @injectable() export class DidExchangeProtocol { private connectionService: ConnectionService + private didRegistrarService: DidRegistrarService private jwsService: JwsService private didRepository: DidRepository private logger: Logger public constructor( connectionService: ConnectionService, + didRegistrarService: DidRegistrarService, didRepository: DidRepository, jwsService: JwsService, @inject(InjectionSymbols.Logger) logger: Logger ) { this.connectionService = connectionService + this.didRegistrarService = didRegistrarService this.didRepository = didRepository this.jwsService = jwsService this.logger = logger @@ -165,6 +172,8 @@ export class DidExchangeProtocol { ) } + // TODO: Move this into the didcomm module, and add a method called store received did document. + // This can be called from both the did exchange and the connection protocol. const didDocument = await this.extractDidDocument(messageContext.agentContext, message) const didRecord = new DidRecord({ id: message.did, @@ -406,32 +415,28 @@ export class DidExchangeProtocol { } private async createPeerDidDoc(agentContext: AgentContext, services: ResolvedDidCommService[]) { - const didDocument = createDidDocumentFromServices(services) + // Create did document without the id property + const didDocument = createPeerDidDocumentFromServices(services) - const peerDid = didDocumentJsonToNumAlgo1Did(didDocument.toJSON()) - didDocument.id = peerDid - - const didRecord = new DidRecord({ - id: peerDid, - role: DidDocumentRole.Created, + // Register did:peer document. This will generate the id property and save it to a did record + const result = await this.didRegistrarService.create(agentContext, { + method: 'peer', didDocument, - tags: { - // We need to save the recipientKeys, so we can find the associated did - // of a key when we receive a message from another connection. - recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + options: { + numAlgo: PeerDidNumAlgo.GenesisDoc, }, }) - this.logger.debug('Saving DID record', { - id: didRecord.id, - role: didRecord.role, - tags: didRecord.getTags(), - didDocument: 'omitted...', + if (result.didState?.state !== 'finished') { + throw new AriesFrameworkError(`Did document creation failed: ${JSON.stringify(result.didState)}`) + } + + this.logger.debug(`Did document with did ${result.didState.did} created.`, { + did: result.didState.did, + didDocument: result.didState.didDocument, }) - await this.didRepository.save(agentContext, didRecord) - this.logger.debug('Did record created.', didRecord) - return didDocument + return result.didState.didDocument } private async createSignedAttachment(agentContext: AgentContext, didDoc: DidDocument, verkeys: string[]) { diff --git a/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts index 25ade9e32d..e4520b6528 100644 --- a/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts +++ b/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts @@ -1,5 +1,6 @@ import type { AgentContext } from '../../../agent' import type { Wallet } from '../../../wallet/Wallet' +import type { DidDocument } from '../../dids' import type { Routing } from '../services/ConnectionService' import { Subject } from 'rxjs' @@ -22,9 +23,11 @@ import { uuid } from '../../../utils/uuid' import { IndyWallet } from '../../../wallet/IndyWallet' import { AckMessage, AckStatus } from '../../common' import { DidKey, IndyAgentService } from '../../dids' +import { DidDocumentRole } from '../../dids/domain/DidDocumentRole' import { DidCommV1Service } from '../../dids/domain/service/DidCommV1Service' import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1' -import { DidRepository } from '../../dids/repository' +import { DidRecord, DidRepository } from '../../dids/repository' +import { DidRegistrarService } from '../../dids/services/DidRegistrarService' import { OutOfBandRole } from '../../oob/domain/OutOfBandRole' import { OutOfBandState } from '../../oob/domain/OutOfBandState' import { ConnectionRequestMessage, ConnectionResponseMessage, TrustPingMessage } from '../messages' @@ -43,9 +46,22 @@ import { ConnectionService } from '../services/ConnectionService' import { convertToNewDidDocument } from '../services/helpers' jest.mock('../repository/ConnectionRepository') +jest.mock('../../dids/services/DidRegistrarService') jest.mock('../../dids/repository/DidRepository') const ConnectionRepositoryMock = ConnectionRepository as jest.Mock const DidRepositoryMock = DidRepository as jest.Mock +const DidRegistrarServiceMock = DidRegistrarService as jest.Mock + +const didRegistrarService = new DidRegistrarServiceMock() +mockFunction(didRegistrarService.create).mockResolvedValue({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:peer:123', + didDocument: {} as DidDocument, + }, +}) const connectionImageUrl = 'https://example.com/image.png' @@ -78,13 +94,26 @@ describe('ConnectionService', () => { eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) connectionRepository = new ConnectionRepositoryMock() didRepository = new DidRepositoryMock() - connectionService = new ConnectionService(agentConfig.logger, connectionRepository, didRepository, eventEmitter) + connectionService = new ConnectionService( + agentConfig.logger, + connectionRepository, + didRepository, + didRegistrarService, + eventEmitter + ) myRouting = { recipientKey: Key.fromFingerprint('z6MkwFkSP4uv5PhhKJCGehtjuZedkotC7VF64xtMsxuM8R3W'), endpoints: agentConfig.endpoints ?? [], routingKeys: [], mediatorId: 'fakeMediatorId', } + + mockFunction(didRepository.getById).mockResolvedValue( + new DidRecord({ + id: 'did:peer:123', + role: DidDocumentRole.Created, + }) + ) }) describe('createRequest', () => { diff --git a/packages/core/src/modules/connections/services/ConnectionService.ts b/packages/core/src/modules/connections/services/ConnectionService.ts index 6b26c59a15..aa2960dbe0 100644 --- a/packages/core/src/modules/connections/services/ConnectionService.ts +++ b/packages/core/src/modules/connections/services/ConnectionService.ts @@ -2,6 +2,7 @@ import type { AgentContext } from '../../../agent' import type { AgentMessage } from '../../../agent/AgentMessage' import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' import type { AckMessage } from '../../common' +import type { PeerDidCreateOptions } from '../../dids/methods/peer/PeerDidRegistrar' import type { OutOfBandDidCommService } from '../../oob/domain/OutOfBandDidCommService' import type { OutOfBandRecord } from '../../oob/repository' import type { ConnectionStateChangedEvent } from '../ConnectionEvents' @@ -21,9 +22,10 @@ import { Logger } from '../../../logger' import { inject, injectable } from '../../../plugins' import { JsonTransformer } from '../../../utils/JsonTransformer' import { indyDidFromPublicKeyBase58 } from '../../../utils/did' -import { DidKey, IndyAgentService } from '../../dids' +import { DidKey, DidRegistrarService, IndyAgentService } from '../../dids' import { DidDocumentRole } from '../../dids/domain/DidDocumentRole' import { didKeyToVerkey } from '../../dids/helpers' +import { PeerDidNumAlgo } from '../../dids/methods/peer/didPeer' import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1' import { DidRecord, DidRepository } from '../../dids/repository' import { DidRecordMetadataKeys } from '../../dids/repository/didRecordMetadataTypes' @@ -59,6 +61,7 @@ export interface ConnectionRequestParams { export class ConnectionService { private connectionRepository: ConnectionRepository private didRepository: DidRepository + private didRegistrarService: DidRegistrarService private eventEmitter: EventEmitter private logger: Logger @@ -66,10 +69,12 @@ export class ConnectionService { @inject(InjectionSymbols.Logger) logger: Logger, connectionRepository: ConnectionRepository, didRepository: DidRepository, + didRegistrarService: DidRegistrarService, eventEmitter: EventEmitter ) { this.connectionRepository = connectionRepository this.didRepository = didRepository + this.didRegistrarService = didRegistrarService this.eventEmitter = eventEmitter this.logger = logger } @@ -101,10 +106,7 @@ export class ConnectionService { // We take just the first one for now. const [invitationDid] = outOfBandInvitation.invitationDids - const { did: peerDid } = await this.createDid(agentContext, { - role: DidDocumentRole.Created, - didDoc, - }) + const didDocument = await this.registerCreatedPeerDidDocument(agentContext, didDoc) const connectionRecord = await this.createConnection(agentContext, { protocol: HandshakeProtocol.Connections, @@ -112,7 +114,7 @@ export class ConnectionService { state: DidExchangeState.InvitationReceived, theirLabel: outOfBandInvitation.label, alias: config?.alias, - did: peerDid, + did: didDocument.id, mediatorId, autoAcceptConnection: config?.autoAcceptConnection, outOfBandId: outOfBandRecord.id, @@ -161,10 +163,7 @@ export class ConnectionService { }) } - const { did: peerDid } = await this.createDid(messageContext.agentContext, { - role: DidDocumentRole.Received, - didDoc: message.connection.didDoc, - }) + const didDocument = await this.storeReceivedPeerDidDocument(messageContext.agentContext, message.connection.didDoc) const connectionRecord = await this.createConnection(messageContext.agentContext, { protocol: HandshakeProtocol.Connections, @@ -173,7 +172,7 @@ export class ConnectionService { theirLabel: message.label, imageUrl: message.imageUrl, outOfBandId: outOfBandRecord.id, - theirDid: peerDid, + theirDid: didDocument.id, threadId: message.threadId, mediatorId: outOfBandRecord.mediatorId, autoAcceptConnection: outOfBandRecord.autoAcceptConnection, @@ -210,10 +209,7 @@ export class ConnectionService { ) ) - const { did: peerDid } = await this.createDid(agentContext, { - role: DidDocumentRole.Created, - didDoc, - }) + const didDocument = await this.registerCreatedPeerDidDocument(agentContext, didDoc) const connection = new Connection({ did: didDoc.id, @@ -233,7 +229,7 @@ export class ConnectionService { connectionSig: await signData(connectionJson, agentContext.wallet, signingKey), }) - connectionRecord.did = peerDid + connectionRecord.did = didDocument.id await this.updateState(agentContext, connectionRecord, DidExchangeState.ResponseSent) this.logger.debug(`Create message ${ConnectionResponseMessage.type.messageTypeUri} end`, { @@ -309,12 +305,9 @@ export class ConnectionService { throw new AriesFrameworkError('DID Document is missing.') } - const { did: peerDid } = await this.createDid(messageContext.agentContext, { - role: DidDocumentRole.Received, - didDoc: connection.didDoc, - }) + const didDocument = await this.storeReceivedPeerDidDocument(messageContext.agentContext, connection.didDoc) - connectionRecord.theirDid = peerDid + connectionRecord.theirDid = didDocument.id connectionRecord.threadId = message.threadId await this.updateState(messageContext.agentContext, connectionRecord, DidExchangeState.ResponseReceived) @@ -632,15 +625,52 @@ export class ConnectionService { return connectionRecord } - private async createDid(agentContext: AgentContext, { role, didDoc }: { role: DidDocumentRole; didDoc: DidDoc }) { + private async registerCreatedPeerDidDocument(agentContext: AgentContext, didDoc: DidDoc) { + // Convert the legacy did doc to a new did document + const didDocument = convertToNewDidDocument(didDoc) + + // Register did:peer document. This will generate the id property and save it to a did record + const result = await this.didRegistrarService.create(agentContext, { + method: 'peer', + didDocument, + options: { + numAlgo: PeerDidNumAlgo.GenesisDoc, + }, + }) + + if (result.didState?.state !== 'finished') { + throw new AriesFrameworkError(`Did document creation failed: ${JSON.stringify(result.didState)}`) + } + + this.logger.debug(`Did document with did ${result.didState.did} created.`, { + did: result.didState.did, + didDocument: result.didState.didDocument, + }) + + const didRecord = await this.didRepository.getById(agentContext, result.didState.did) + + // Store the unqualified did with the legacy did document in the metadata + // Can be removed at a later stage if we know for sure we don't need it anymore + didRecord.metadata.set(DidRecordMetadataKeys.LegacyDid, { + unqualifiedDid: didDoc.id, + didDocumentString: JsonTransformer.serialize(didDoc), + }) + + await this.didRepository.update(agentContext, didRecord) + return result.didState.didDocument + } + + private async storeReceivedPeerDidDocument(agentContext: AgentContext, didDoc: DidDoc) { // Convert the legacy did doc to a new did document const didDocument = convertToNewDidDocument(didDoc) + // TODO: Move this into the didcomm module, and add a method called store received did document. + // This can be called from both the did exchange and the connection protocol. const peerDid = didDocumentJsonToNumAlgo1Did(didDocument.toJSON()) didDocument.id = peerDid const didRecord = new DidRecord({ id: peerDid, - role, + role: DidDocumentRole.Received, didDocument, tags: { // We need to save the recipientKeys, so we can find the associated did @@ -665,7 +695,7 @@ export class ConnectionService { await this.didRepository.save(agentContext, didRecord) this.logger.debug('Did record created.', didRecord) - return { did: peerDid, didDocument } + return didDocument } private createDidDoc(routing: Routing) { diff --git a/packages/core/src/modules/dids/DidsApi.ts b/packages/core/src/modules/dids/DidsApi.ts index 7599be95cb..59134e5f6d 100644 --- a/packages/core/src/modules/dids/DidsApi.ts +++ b/packages/core/src/modules/dids/DidsApi.ts @@ -1,37 +1,102 @@ -import type { Key } from '../../crypto' -import type { DidResolutionOptions } from './types' +import type { + DidCreateOptions, + DidCreateResult, + DidDeactivateOptions, + DidDeactivateResult, + DidResolutionOptions, + DidUpdateOptions, + DidUpdateResult, +} from './types' import { AgentContext } from '../../agent' import { injectable } from '../../plugins' +import { DidsModuleConfig } from './DidsModuleConfig' import { DidRepository } from './repository' -import { DidResolverService } from './services/DidResolverService' +import { DidRegistrarService, DidResolverService } from './services' @injectable() export class DidsApi { - private resolverService: DidResolverService + public config: DidsModuleConfig + + private didResolverService: DidResolverService + private didRegistrarService: DidRegistrarService private didRepository: DidRepository private agentContext: AgentContext - public constructor(resolverService: DidResolverService, didRepository: DidRepository, agentContext: AgentContext) { - this.resolverService = resolverService + public constructor( + didResolverService: DidResolverService, + didRegistrarService: DidRegistrarService, + didRepository: DidRepository, + agentContext: AgentContext, + config: DidsModuleConfig + ) { + this.didResolverService = didResolverService + this.didRegistrarService = didRegistrarService this.didRepository = didRepository this.agentContext = agentContext + this.config = config } + /** + * Resolve a did to a did document. + * + * Follows the interface as defined in https://w3c-ccg.github.io/did-resolution/ + */ public resolve(didUrl: string, options?: DidResolutionOptions) { - return this.resolverService.resolve(this.agentContext, didUrl, options) + return this.didResolverService.resolve(this.agentContext, didUrl, options) } - public resolveDidDocument(didUrl: string) { - return this.resolverService.resolveDidDocument(this.agentContext, didUrl) + /** + * Create, register and store a did and did document. + * + * Follows the interface as defined in https://identity.foundation/did-registration + */ + public create( + options: CreateOptions + ): Promise { + return this.didRegistrarService.create(this.agentContext, options) + } + + /** + * Update an existing did document. + * + * Follows the interface as defined in https://identity.foundation/did-registration + */ + public update( + options: UpdateOptions + ): Promise { + return this.didRegistrarService.update(this.agentContext, options) } - public findByRecipientKey(recipientKey: Key) { - return this.didRepository.findByRecipientKey(this.agentContext, recipientKey) + /** + * Deactivate an existing did. + * + * Follows the interface as defined in https://identity.foundation/did-registration + */ + public deactivate( + options: DeactivateOptions + ): Promise { + return this.didRegistrarService.deactivate(this.agentContext, options) + } + + /** + * Resolve a did to a did document. This won't return the associated metadata as defined + * in the did resolution specification, and will throw an error if the did document could not + * be resolved. + */ + public resolveDidDocument(didUrl: string) { + return this.didResolverService.resolveDidDocument(this.agentContext, didUrl) } - public findAllByRecipientKey(recipientKey: Key) { - return this.didRepository.findAllByRecipientKey(this.agentContext, recipientKey) + /** + * Get a list of all dids created by the agent. This will return a list of {@link DidRecord} objects. + * Each document will have an id property with the value of the did. Optionally, it will contain a did document, + * but this is only for documents that can't be resolved from the did itself or remotely. + * + * You can call `${@link DidsModule.resolve} to resolve the did document based on the did itself. + */ + public getCreatedDids({ method }: { method?: string } = {}) { + return this.didRepository.getCreatedDids(this.agentContext, { method }) } } diff --git a/packages/core/src/modules/dids/DidsModule.ts b/packages/core/src/modules/dids/DidsModule.ts index 5cc570ec48..0a43f0a154 100644 --- a/packages/core/src/modules/dids/DidsModule.ts +++ b/packages/core/src/modules/dids/DidsModule.ts @@ -1,10 +1,19 @@ import type { DependencyManager, Module } from '../../plugins' +import type { DidsModuleConfigOptions } from './DidsModuleConfig' import { DidsApi } from './DidsApi' +import { DidsModuleConfig } from './DidsModuleConfig' +import { DidResolverToken, DidRegistrarToken } from './domain' import { DidRepository } from './repository' -import { DidResolverService } from './services' +import { DidResolverService, DidRegistrarService } from './services' export class DidsModule implements Module { + public readonly config: DidsModuleConfig + + public constructor(config?: DidsModuleConfigOptions) { + this.config = new DidsModuleConfig(config) + } + /** * Registers the dependencies of the dids module module on the dependency manager. */ @@ -12,8 +21,22 @@ export class DidsModule implements Module { // Api dependencyManager.registerContextScoped(DidsApi) + // Config + dependencyManager.registerInstance(DidsModuleConfig, this.config) + // Services dependencyManager.registerSingleton(DidResolverService) + dependencyManager.registerSingleton(DidRegistrarService) dependencyManager.registerSingleton(DidRepository) + + // Register all did resolvers + for (const Resolver of this.config.resolvers) { + dependencyManager.registerSingleton(DidResolverToken, Resolver) + } + + // Register all did registrars + for (const Registrar of this.config.registrars) { + dependencyManager.registerSingleton(DidRegistrarToken, Registrar) + } } } diff --git a/packages/core/src/modules/dids/DidsModuleConfig.ts b/packages/core/src/modules/dids/DidsModuleConfig.ts new file mode 100644 index 0000000000..e05bd0daca --- /dev/null +++ b/packages/core/src/modules/dids/DidsModuleConfig.ts @@ -0,0 +1,68 @@ +import type { Constructor } from '../../utils/mixins' +import type { DidRegistrar, DidResolver } from './domain' + +import { + KeyDidRegistrar, + SovDidRegistrar, + PeerDidRegistrar, + KeyDidResolver, + PeerDidResolver, + SovDidResolver, + WebDidResolver, +} from './methods' + +/** + * DidsModuleConfigOptions defines the interface for the options of the DidsModuleConfig class. + * This can contain optional parameters that have default values in the config class itself. + */ +export interface DidsModuleConfigOptions { + /** + * List of did registrars that should be registered on the dids module. The registrar must + * follow the {@link DidRegistrar} interface, and must be constructable. The registrar will be injected + * into the `DidRegistrarService` and should be decorated with the `@injectable` decorator. + * + * If no registrars are provided, the default registrars will be used. The `PeerDidRegistrar` will ALWAYS be + * registered, as it is needed for the connections and out of band module to function. Other did methods can be + * disabled. + * + * @default [KeyDidRegistrar, SovDidRegistrar, PeerDidRegistrar] + */ + registrars?: Constructor[] + + /** + * List of did resolvers that should be registered on the dids module. The resolver must + * follow the {@link DidResolver} interface, and must be constructable. The resolver will be injected + * into the `DidResolverService` and should be decorated with the `@injectable` decorator. + * + * If no resolvers are provided, the default resolvers will be used. The `PeerDidResolver` will ALWAYS be + * registered, as it is needed for the connections and out of band module to function. Other did methods can be + * disabled. + * + * @default [SovDidResolver, WebDidResolver, KeyDidResolver, PeerDidResolver] + */ + resolvers?: Constructor[] +} + +export class DidsModuleConfig { + private options: DidsModuleConfigOptions + + public constructor(options?: DidsModuleConfigOptions) { + this.options = options ?? {} + } + + /** See {@link DidsModuleConfigOptions.registrars} */ + public get registrars() { + const registrars = this.options.registrars ?? [KeyDidRegistrar, SovDidRegistrar, PeerDidRegistrar] + + // If the peer did registrar is not included yet, add it + return registrars.includes(PeerDidRegistrar) ? registrars : [...registrars, PeerDidRegistrar] + } + + /** See {@link DidsModuleConfigOptions.resolvers} */ + public get resolvers() { + const resolvers = this.options.resolvers ?? [SovDidResolver, WebDidResolver, KeyDidResolver, PeerDidResolver] + + // If the peer did resolver is not included yet, add it + return resolvers.includes(PeerDidResolver) ? resolvers : [...resolvers, PeerDidResolver] + } +} diff --git a/packages/core/src/modules/dids/__tests__/DidsModule.test.ts b/packages/core/src/modules/dids/__tests__/DidsModule.test.ts index 00926a9ace..3a372fea59 100644 --- a/packages/core/src/modules/dids/__tests__/DidsModule.test.ts +++ b/packages/core/src/modules/dids/__tests__/DidsModule.test.ts @@ -1,8 +1,22 @@ +import type { Constructor } from '../../../utils/mixins' +import type { DidRegistrar, DidResolver } from '../domain' + import { DependencyManager } from '../../../plugins/DependencyManager' import { DidsApi } from '../DidsApi' import { DidsModule } from '../DidsModule' +import { DidsModuleConfig } from '../DidsModuleConfig' +import { DidRegistrarToken, DidResolverToken } from '../domain' +import { + KeyDidRegistrar, + KeyDidResolver, + PeerDidRegistrar, + PeerDidResolver, + SovDidRegistrar, + SovDidResolver, + WebDidResolver, +} from '../methods' import { DidRepository } from '../repository' -import { DidResolverService } from '../services' +import { DidRegistrarService, DidResolverService } from '../services' jest.mock('../../../plugins/DependencyManager') const DependencyManagerMock = DependencyManager as jest.Mock @@ -11,13 +25,40 @@ const dependencyManager = new DependencyManagerMock() describe('DidsModule', () => { test('registers dependencies on the dependency manager', () => { - new DidsModule().register(dependencyManager) + const didsModule = new DidsModule() + didsModule.register(dependencyManager) expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(DidsApi) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(DidsModuleConfig, didsModule.config) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(10) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidResolverService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidRegistrarService) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidRepository) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidResolverToken, SovDidResolver) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidResolverToken, WebDidResolver) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidResolverToken, KeyDidResolver) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidResolverToken, PeerDidResolver) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidRegistrarToken, KeyDidRegistrar) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidRegistrarToken, SovDidRegistrar) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidRegistrarToken, PeerDidRegistrar) + }) + + test('takes the values from the dids config', () => { + const registrar = {} as Constructor + const resolver = {} as Constructor + + new DidsModule({ + registrars: [registrar], + resolvers: [resolver], + }).register(dependencyManager) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidResolverToken, resolver) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidRegistrarToken, registrar) }) }) diff --git a/packages/core/src/modules/dids/__tests__/DidsModuleConfig.test.ts b/packages/core/src/modules/dids/__tests__/DidsModuleConfig.test.ts new file mode 100644 index 0000000000..08edee502e --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/DidsModuleConfig.test.ts @@ -0,0 +1,46 @@ +import type { Constructor } from '../../../utils/mixins' +import type { DidRegistrar, DidResolver } from '../domain' + +import { + KeyDidRegistrar, + SovDidRegistrar, + PeerDidRegistrar, + KeyDidResolver, + PeerDidResolver, + SovDidResolver, + WebDidResolver, +} from '..' +import { DidsModuleConfig } from '../DidsModuleConfig' + +describe('DidsModuleConfig', () => { + test('sets default values', () => { + const config = new DidsModuleConfig() + + expect(config.registrars).toEqual([KeyDidRegistrar, SovDidRegistrar, PeerDidRegistrar]) + expect(config.resolvers).toEqual([SovDidResolver, WebDidResolver, KeyDidResolver, PeerDidResolver]) + }) + + test('sets values', () => { + const registrars = [PeerDidRegistrar, {} as Constructor] + const resolvers = [PeerDidResolver, {} as Constructor] + const config = new DidsModuleConfig({ + registrars, + resolvers, + }) + + expect(config.registrars).toEqual(registrars) + expect(config.resolvers).toEqual(resolvers) + }) + + test('adds peer did resolver and registrar if not provided in config', () => { + const registrar = {} as Constructor + const resolver = {} as Constructor + const config = new DidsModuleConfig({ + registrars: [registrar], + resolvers: [resolver], + }) + + expect(config.registrars).toEqual([registrar, PeerDidRegistrar]) + expect(config.resolvers).toEqual([resolver, PeerDidResolver]) + }) +}) diff --git a/packages/core/src/modules/dids/__tests__/dids-registrar.e2e.test.ts b/packages/core/src/modules/dids/__tests__/dids-registrar.e2e.test.ts new file mode 100644 index 0000000000..264dffe6f9 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/dids-registrar.e2e.test.ts @@ -0,0 +1,267 @@ +import type { KeyDidCreateOptions } from '../methods/key/KeyDidRegistrar' +import type { PeerDidNumAlgo0CreateOptions } from '../methods/peer/PeerDidRegistrar' +import type { SovDidCreateOptions } from '../methods/sov/SovDidRegistrar' +import type { Wallet } from '@aries-framework/core' + +import { convertPublicKeyToX25519, generateKeyPairFromSeed } from '@stablelib/ed25519' + +import { genesisPath, getBaseConfig } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { KeyType } from '../../../crypto' +import { TypedArrayEncoder } from '../../../utils' +import { indyDidFromPublicKeyBase58 } from '../../../utils/did' +import { PeerDidNumAlgo } from '../methods/peer/didPeer' + +import { InjectionSymbols, JsonTransformer } from '@aries-framework/core' + +const { config, agentDependencies } = getBaseConfig('Faber Dids Registrar', { + indyLedgers: [ + { + id: `localhost`, + isProduction: false, + genesisPath, + transactionAuthorAgreement: { version: '1', acceptanceMechanism: 'accept' }, + }, + ], +}) + +describe('dids', () => { + let agent: Agent + + beforeAll(async () => { + agent = new Agent(config, agentDependencies) + await agent.initialize() + }) + + afterAll(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + it('should create a did:key did', async () => { + const did = await agent.dids.create({ + method: 'key', + options: { + keyType: KeyType.Ed25519, + }, + secret: { + seed: '96213c3d7fc8d4d6754c7a0fd969598e', + }, + }) + + // Same seed should resolve to same did:key + expect(JsonTransformer.toJSON(did)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + publicKeyBase58: 'ApA26cozGW5Maa62TNTwtgcxrb7bYjAmf9aQ5cYruCDE', + }, + ], + service: undefined, + authentication: [ + 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + ], + assertionMethod: [ + 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + ], + keyAgreement: [ + { + id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6LSjDbRQQKm9HM4qPBErYyX93BCSzSk1XkwP5EgDrL6eNhh', + type: 'X25519KeyAgreementKey2019', + controller: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + publicKeyBase58: '8YRFt6Wu3pdKjzoUKuTZpSxibqudJvanW6WzjPgZvzvw', + }, + ], + capabilityInvocation: [ + 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + ], + capabilityDelegation: [ + 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc#z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + ], + id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + }, + secret: { seed: '96213c3d7fc8d4d6754c7a0fd969598e' }, + }, + }) + }) + + it('should create a did:peer did', async () => { + const did = await agent.dids.create({ + method: 'peer', + options: { + keyType: KeyType.Ed25519, + numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, + }, + secret: { + seed: 'e008ef10b7c163114b3857542b3736eb', + }, + }) + + // Same seed should resolve to same did:peer + expect(JsonTransformer.toJSON(did)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + id: 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh#z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + type: 'Ed25519VerificationKey2018', + controller: 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + publicKeyBase58: 'GLsyPBT2AgMne8XUvmZKkqLUuFkSjLp3ibkcjc6gjhyK', + }, + ], + service: undefined, + authentication: [ + 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh#z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + ], + assertionMethod: [ + 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh#z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + ], + keyAgreement: [ + { + id: 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh#z6LSdqscQpQy12kNU1kYf7odtabo2Nhr3x3coUjsUZgwxwCj', + type: 'X25519KeyAgreementKey2019', + controller: 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + publicKeyBase58: '3AhStWc6ua2dNdNn8UHgZzPKBEAjMLsTvW2Bz73RFZRy', + }, + ], + capabilityInvocation: [ + 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh#z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + ], + capabilityDelegation: [ + 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh#z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + ], + id: 'did:peer:0z6Mkuo91yRhTWDrFkdNBcLXAbvtUiq2J9E4QQcfYZt4hevkh', + }, + secret: { seed: 'e008ef10b7c163114b3857542b3736eb' }, + }, + }) + }) + + it('should create a did:sov did', async () => { + // Generate a seed and the indy did. This allows us to create a new did every time + // but still check if the created output document is as expected. + const seed = Array(32 + 1) + .join((Math.random().toString(36) + '00000000000000000').slice(2, 18)) + .slice(0, 32) + + const publicKeyEd25519 = generateKeyPairFromSeed(TypedArrayEncoder.fromString(seed)).publicKey + const x25519PublicKeyBase58 = TypedArrayEncoder.toBase58(convertPublicKeyToX25519(publicKeyEd25519)) + const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(publicKeyEd25519) + const indyDid = indyDidFromPublicKeyBase58(ed25519PublicKeyBase58) + + const wallet = agent.injectionContainer.resolve(InjectionSymbols.Wallet) + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain, @typescript-eslint/no-non-null-assertion + const submitterDid = `did:sov:${wallet.publicDid?.did!}` + + const did = await agent.dids.create({ + method: 'sov', + options: { + submitterDid, + alias: 'Alias', + endpoints: { + endpoint: 'https://example.com/endpoint', + types: ['DIDComm', 'did-communication', 'endpoint'], + routingKeys: ['a-routing-key'], + }, + }, + secret: { + seed, + }, + }) + + expect(JsonTransformer.toJSON(did)).toMatchObject({ + didDocumentMetadata: { + qualifiedIndyDid: `did:indy:localhost:${indyDid}`, + }, + didRegistrationMetadata: { + indyNamespace: 'localhost', + }, + didState: { + state: 'finished', + did: `did:sov:${indyDid}`, + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + 'https://didcomm.org/messaging/contexts/v2', + ], + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + id: `did:sov:${indyDid}#key-1`, + type: 'Ed25519VerificationKey2018', + controller: `did:sov:${indyDid}`, + publicKeyBase58: ed25519PublicKeyBase58, + }, + { + id: `did:sov:${indyDid}#key-agreement-1`, + type: 'X25519KeyAgreementKey2019', + controller: `did:sov:${indyDid}`, + publicKeyBase58: x25519PublicKeyBase58, + }, + ], + service: [ + { + id: `did:sov:${indyDid}#endpoint`, + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }, + { + accept: ['didcomm/aip2;env=rfc19'], + id: `did:sov:${indyDid}#did-communication`, + priority: 0, + recipientKeys: [`did:sov:${indyDid}#key-agreement-1`], + routingKeys: ['a-routing-key'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + }, + { + accept: ['didcomm/v2'], + id: `did:sov:${indyDid}#didcomm-1`, + routingKeys: ['a-routing-key'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'DIDComm', + }, + ], + authentication: [`did:sov:${indyDid}#key-1`], + assertionMethod: [`did:sov:${indyDid}#key-1`], + keyAgreement: [`did:sov:${indyDid}#key-agreement-1`], + capabilityInvocation: undefined, + capabilityDelegation: undefined, + id: `did:sov:${indyDid}`, + }, + secret: { + seed, + }, + }, + }) + }) +}) diff --git a/packages/core/tests/dids.test.ts b/packages/core/src/modules/dids/__tests__/dids-resolver.e2e.test.ts similarity index 77% rename from packages/core/tests/dids.test.ts rename to packages/core/src/modules/dids/__tests__/dids-resolver.e2e.test.ts index 50c90c704d..39e3710c19 100644 --- a/packages/core/tests/dids.test.ts +++ b/packages/core/src/modules/dids/__tests__/dids-resolver.e2e.test.ts @@ -1,9 +1,13 @@ -import { Agent } from '../src/agent/Agent' -import { JsonTransformer } from '../src/utils/JsonTransformer' +import type { Wallet } from '../../../wallet' -import { getBaseConfig } from './helpers' +import { convertPublicKeyToX25519 } from '@stablelib/ed25519' -const { config, agentDependencies } = getBaseConfig('Faber Dids', {}) +import { getBaseConfig } from '../../../../tests/helpers' +import { sleep } from '../../../utils/sleep' + +import { InjectionSymbols, Key, KeyType, JsonTransformer, Agent } from '@aries-framework/core' + +const { config, agentDependencies } = getBaseConfig('Faber Dids Resolver', {}) describe('dids', () => { let agent: Agent @@ -19,37 +23,51 @@ describe('dids', () => { }) it('should resolve a did:sov did', async () => { - const did = await agent.dids.resolve(`did:sov:TL1EaPFCZ8Si5aUrqScBDt`) + const wallet = agent.injectionContainer.resolve(InjectionSymbols.Wallet) + const { did: unqualifiedDid, verkey: publicKeyBase58 } = await wallet.createDid() - expect(JsonTransformer.toJSON(did)).toMatchObject({ + await agent.ledger.registerPublicDid(unqualifiedDid, publicKeyBase58, 'Alias', 'TRUSTEE') + + // Terrible, but the did can't be immediately resolved, so we need to wait a bit + await sleep(1000) + + const did = `did:sov:${unqualifiedDid}` + const didResult = await agent.dids.resolve(did) + + const x25519PublicKey = convertPublicKeyToX25519( + Key.fromPublicKeyBase58(publicKeyBase58, KeyType.Ed25519).publicKey + ) + const x25519PublicKeyBase58 = Key.fromPublicKey(x25519PublicKey, KeyType.X25519).publicKeyBase58 + + expect(JsonTransformer.toJSON(didResult)).toMatchObject({ didDocument: { '@context': [ 'https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1', 'https://w3id.org/security/suites/x25519-2019/v1', ], - id: 'did:sov:TL1EaPFCZ8Si5aUrqScBDt', + id: did, alsoKnownAs: undefined, controller: undefined, verificationMethod: [ { type: 'Ed25519VerificationKey2018', - controller: 'did:sov:TL1EaPFCZ8Si5aUrqScBDt', - id: 'did:sov:TL1EaPFCZ8Si5aUrqScBDt#key-1', - publicKeyBase58: 'FMGcFuU3QwAQLywxvmEnSorQT3NwU9wgDMMTaDFtvswm', + controller: did, + id: `${did}#key-1`, + publicKeyBase58, }, { - controller: 'did:sov:TL1EaPFCZ8Si5aUrqScBDt', + controller: did, type: 'X25519KeyAgreementKey2019', - id: 'did:sov:TL1EaPFCZ8Si5aUrqScBDt#key-agreement-1', - publicKeyBase58: '6oKfyWDYRpbutQWDUu8ots6GoqAZJ9HYRzPuuEiqfyM', + id: `${did}#key-agreement-1`, + publicKeyBase58: x25519PublicKeyBase58, }, ], capabilityDelegation: undefined, capabilityInvocation: undefined, - authentication: ['did:sov:TL1EaPFCZ8Si5aUrqScBDt#key-1'], - assertionMethod: ['did:sov:TL1EaPFCZ8Si5aUrqScBDt#key-1'], - keyAgreement: ['did:sov:TL1EaPFCZ8Si5aUrqScBDt#key-agreement-1'], + authentication: [`${did}#key-1`], + assertionMethod: [`${did}#key-1`], + keyAgreement: [`${did}#key-agreement-1`], service: undefined, }, didDocumentMetadata: {}, diff --git a/packages/core/src/modules/dids/__tests__/peer-did.test.ts b/packages/core/src/modules/dids/__tests__/peer-did.test.ts index 38f5747b17..14eac24fdd 100644 --- a/packages/core/src/modules/dids/__tests__/peer-did.test.ts +++ b/packages/core/src/modules/dids/__tests__/peer-did.test.ts @@ -1,5 +1,4 @@ import type { AgentContext } from '../../../agent' -import type { IndyLedgerService } from '../../ledger' import { Subject } from 'rxjs' @@ -14,6 +13,7 @@ import { DidCommV1Service, DidDocument, DidDocumentBuilder } from '../domain' import { DidDocumentRole } from '../domain/DidDocumentRole' import { convertPublicKeyToX25519, getEd25519VerificationMethod } from '../domain/key-type/ed25519' import { getX25519VerificationMethod } from '../domain/key-type/x25519' +import { PeerDidResolver } from '../methods' import { DidKey } from '../methods/key' import { getNumAlgoFromPeerDid, PeerDidNumAlgo } from '../methods/peer/didPeer' import { didDocumentJsonToNumAlgo1Did } from '../methods/peer/peerDidNumAlgo1' @@ -42,7 +42,7 @@ describe('peer dids', () => { didRepository = new DidRepository(storageService, eventEmitter) // Mocking IndyLedgerService as we're only interested in the did:peer resolver - didResolverService = new DidResolverService({} as unknown as IndyLedgerService, didRepository, config.logger) + didResolverService = new DidResolverService(config.logger, [new PeerDidResolver(didRepository)]) }) afterEach(async () => { diff --git a/packages/core/src/modules/dids/domain/DidRegistrar.ts b/packages/core/src/modules/dids/domain/DidRegistrar.ts new file mode 100644 index 0000000000..200cda6ab6 --- /dev/null +++ b/packages/core/src/modules/dids/domain/DidRegistrar.ts @@ -0,0 +1,19 @@ +import type { AgentContext } from '../../../agent' +import type { + DidCreateOptions, + DidDeactivateOptions, + DidUpdateOptions, + DidCreateResult, + DidUpdateResult, + DidDeactivateResult, +} from '../types' + +export const DidRegistrarToken = Symbol('DidRegistrar') + +export interface DidRegistrar { + readonly supportedMethods: string[] + + create(agentContext: AgentContext, options: DidCreateOptions): Promise + update(agentContext: AgentContext, options: DidUpdateOptions): Promise + deactivate(agentContext: AgentContext, options: DidDeactivateOptions): Promise +} diff --git a/packages/core/src/modules/dids/domain/DidResolver.ts b/packages/core/src/modules/dids/domain/DidResolver.ts index 050ea2cd97..e4512a1e57 100644 --- a/packages/core/src/modules/dids/domain/DidResolver.ts +++ b/packages/core/src/modules/dids/domain/DidResolver.ts @@ -1,6 +1,8 @@ import type { AgentContext } from '../../../agent' import type { ParsedDid, DidResolutionResult, DidResolutionOptions } from '../types' +export const DidResolverToken = Symbol('DidResolver') + export interface DidResolver { readonly supportedMethods: string[] resolve( diff --git a/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts b/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts index 607df90e01..09837a0d2e 100644 --- a/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts +++ b/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts @@ -213,7 +213,7 @@ describe('Did | DidDocument', () => { }) describe('findVerificationMethodByKeyType', () => { - it('return first verfication method that match key type', async () => { + it('return first verification method that match key type', async () => { expect(await findVerificationMethodByKeyType('Ed25519VerificationKey2018', didDocumentInstance)).toBeInstanceOf( VerificationMethod ) diff --git a/packages/core/src/modules/dids/domain/index.ts b/packages/core/src/modules/dids/domain/index.ts index bf0ff1c854..cae70d066f 100644 --- a/packages/core/src/modules/dids/domain/index.ts +++ b/packages/core/src/modules/dids/domain/index.ts @@ -2,3 +2,6 @@ export * from './service' export * from './verificationMethod' export * from './DidDocument' export * from './DidDocumentBuilder' +export * from './DidDocumentRole' +export * from './DidRegistrar' +export * from './DidResolver' diff --git a/packages/core/src/modules/dids/domain/parse.ts b/packages/core/src/modules/dids/domain/parse.ts index aebeccec6f..ab293ee878 100644 --- a/packages/core/src/modules/dids/domain/parse.ts +++ b/packages/core/src/modules/dids/domain/parse.ts @@ -3,7 +3,7 @@ import type { ParsedDid } from '../types' import { parse } from 'did-resolver' export function parseDid(did: string): ParsedDid { - const parsed = parse(did) + const parsed = tryParseDid(did) if (!parsed) { throw new Error(`Error parsing did '${did}'`) @@ -11,3 +11,7 @@ export function parseDid(did: string): ParsedDid { return parsed } + +export function tryParseDid(did: string): ParsedDid | null { + return parse(did) +} diff --git a/packages/core/src/modules/dids/index.ts b/packages/core/src/modules/dids/index.ts index d9473ea73f..9ad363c0a2 100644 --- a/packages/core/src/modules/dids/index.ts +++ b/packages/core/src/modules/dids/index.ts @@ -4,4 +4,5 @@ export * from './DidsApi' export * from './repository' export * from './services' export * from './DidsModule' -export { DidKey } from './methods/key/DidKey' +export * from './methods' +export * from './DidsModuleConfig' diff --git a/packages/core/src/modules/dids/methods/index.ts b/packages/core/src/modules/dids/methods/index.ts new file mode 100644 index 0000000000..ebacc7f2c2 --- /dev/null +++ b/packages/core/src/modules/dids/methods/index.ts @@ -0,0 +1,4 @@ +export * from './key' +export * from './peer' +export * from './sov' +export * from './web' diff --git a/packages/core/src/modules/dids/methods/key/KeyDidRegistrar.ts b/packages/core/src/modules/dids/methods/key/KeyDidRegistrar.ts new file mode 100644 index 0000000000..7fcd557e84 --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/KeyDidRegistrar.ts @@ -0,0 +1,129 @@ +import type { AgentContext } from '../../../../agent' +import type { KeyType } from '../../../../crypto' +import type { DidRegistrar } from '../../domain/DidRegistrar' +import type { DidCreateOptions, DidCreateResult, DidDeactivateResult, DidUpdateResult } from '../../types' + +import { injectable } from '../../../../plugins' +import { DidDocumentRole } from '../../domain/DidDocumentRole' +import { DidRepository, DidRecord } from '../../repository' + +import { DidKey } from './DidKey' + +@injectable() +export class KeyDidRegistrar implements DidRegistrar { + public readonly supportedMethods = ['key'] + private didRepository: DidRepository + + public constructor(didRepository: DidRepository) { + this.didRepository = didRepository + } + + public async create(agentContext: AgentContext, options: KeyDidCreateOptions): Promise { + const keyType = options.options.keyType + const seed = options.secret?.seed + + if (!keyType) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Missing key type', + }, + } + } + + if (seed && (typeof seed !== 'string' || seed.length !== 32)) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Invalid seed provided', + }, + } + } + + try { + const key = await agentContext.wallet.createKey({ + keyType, + seed, + }) + + const didKey = new DidKey(key) + + // Save the did so we know we created it and can issue with it + const didRecord = new DidRecord({ + id: didKey.did, + role: DidDocumentRole.Created, + }) + await this.didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: didKey.did, + didDocument: didKey.didDocument, + secret: { + // FIXME: the uni-registrar creates the seed in the registrar method + // if it doesn't exist so the seed can always be returned. Currently + // we can only return it if the seed was passed in by the user. Once + // we have a secure method for generating seeds we should use the same + // approach + seed: options.secret?.seed, + }, + }, + } + } catch (error) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot update did:key did`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot deactivate did:key did`, + }, + } + } +} + +export interface KeyDidCreateOptions extends DidCreateOptions { + method: 'key' + // For now we don't support creating a did:key with a did or did document + did?: never + didDocument?: never + options: { + keyType: KeyType + } + secret?: { + seed?: string + } +} + +// Update and Deactivate not supported for did:key +export type KeyDidUpdateOptions = never +export type KeyDidDeactivateOptions = never diff --git a/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts b/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts index 41f4a0e221..4929e76fec 100644 --- a/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts +++ b/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts @@ -2,8 +2,11 @@ import type { AgentContext } from '../../../../agent' import type { DidResolver } from '../../domain/DidResolver' import type { DidResolutionResult } from '../../types' +import { injectable } from '../../../../plugins' + import { DidKey } from './DidKey' +@injectable() export class KeyDidResolver implements DidResolver { public readonly supportedMethods = ['key'] diff --git a/packages/core/src/modules/dids/methods/key/__tests__/KeyDidRegistrar.test.ts b/packages/core/src/modules/dids/methods/key/__tests__/KeyDidRegistrar.test.ts new file mode 100644 index 0000000000..19bc6bf29e --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/__tests__/KeyDidRegistrar.test.ts @@ -0,0 +1,153 @@ +import type { Wallet } from '../../../../../wallet' + +import { getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { DidDocumentRole } from '../../../domain/DidDocumentRole' +import { DidRepository } from '../../../repository/DidRepository' +import { KeyDidRegistrar } from '../KeyDidRegistrar' + +import didKeyz6MksLeFixture from './__fixtures__/didKeyz6MksLe.json' + +jest.mock('../../../repository/DidRepository') +const DidRepositoryMock = DidRepository as jest.Mock + +const walletMock = { + createKey: jest.fn(() => Key.fromFingerprint('z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU')), +} as unknown as Wallet + +const agentContext = getAgentContext({ + wallet: walletMock, +}) + +describe('DidRegistrar', () => { + describe('KeyDidRegistrar', () => { + let keyDidRegistrar: KeyDidRegistrar + let didRepositoryMock: DidRepository + + beforeEach(() => { + didRepositoryMock = new DidRepositoryMock() + keyDidRegistrar = new KeyDidRegistrar(didRepositoryMock) + }) + + it('should correctly create a did:key document using Ed25519 key type', async () => { + const seed = '96213c3d7fc8d4d6754c712fd969598e' + + const result = await keyDidRegistrar.create(agentContext, { + method: 'key', + options: { + keyType: KeyType.Ed25519, + }, + secret: { + seed, + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU', + didDocument: didKeyz6MksLeFixture, + secret: { + seed: '96213c3d7fc8d4d6754c712fd969598e', + }, + }, + }) + + expect(walletMock.createKey).toHaveBeenCalledWith({ keyType: KeyType.Ed25519, seed }) + }) + + it('should return an error state if no key type is provided', async () => { + const result = await keyDidRegistrar.create(agentContext, { + method: 'key', + // @ts-expect-error - key type is required in interface + options: {}, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Missing key type', + }, + }) + }) + + it('should return an error state if an invalid seed is provided', async () => { + const result = await keyDidRegistrar.create(agentContext, { + method: 'key', + + options: { + keyType: KeyType.Ed25519, + }, + secret: { + seed: 'invalid', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Invalid seed provided', + }, + }) + }) + + it('should store the did document', async () => { + const seed = '96213c3d7fc8d4d6754c712fd969598e' + const did = 'did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU' + + await keyDidRegistrar.create(agentContext, { + method: 'key', + + options: { + keyType: KeyType.Ed25519, + }, + secret: { + seed, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + id: did, + role: DidDocumentRole.Created, + didDocument: undefined, + }) + }) + + it('should return an error state when calling update', async () => { + const result = await keyDidRegistrar.update() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot update did:key did`, + }, + }) + }) + + it('should return an error state when calling deactivate', async () => { + const result = await keyDidRegistrar.deactivate() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot deactivate did:key did`, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/key/__tests__/__fixtures__/didKeyz6MksLe.json b/packages/core/src/modules/dids/methods/key/__tests__/__fixtures__/didKeyz6MksLe.json new file mode 100644 index 0000000000..4182e6d1ff --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/__tests__/__fixtures__/didKeyz6MksLe.json @@ -0,0 +1,36 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "verificationMethod": [ + { + "id": "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU", + "type": "Ed25519VerificationKey2018", + "controller": "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU", + "publicKeyBase58": "DtPcLpky6Yi6zPecfW8VZH3xNoDkvQfiGWp8u5n9nAj6" + } + ], + "authentication": [ + "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ], + "assertionMethod": [ + "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ], + "keyAgreement": [ + { + "id": "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz", + "type": "X25519KeyAgreementKey2019", + "controller": "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU", + "publicKeyBase58": "7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE" + } + ], + "capabilityInvocation": [ + "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ], + "capabilityDelegation": [ + "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ], + "id": "did:key:z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" +} diff --git a/packages/core/src/modules/dids/methods/key/index.ts b/packages/core/src/modules/dids/methods/key/index.ts index c832783193..3c5ea1244d 100644 --- a/packages/core/src/modules/dids/methods/key/index.ts +++ b/packages/core/src/modules/dids/methods/key/index.ts @@ -1 +1,3 @@ export { DidKey } from './DidKey' +export * from './KeyDidRegistrar' +export * from './KeyDidResolver' diff --git a/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts b/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts new file mode 100644 index 0000000000..d194c4cbf1 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts @@ -0,0 +1,205 @@ +import type { AgentContext } from '../../../../agent' +import type { KeyType } from '../../../../crypto' +import type { DidRegistrar } from '../../domain/DidRegistrar' +import type { DidCreateOptions, DidCreateResult, DidDeactivateResult, DidUpdateResult } from '../../types' + +import { injectable } from '../../../../plugins' +import { JsonTransformer } from '../../../../utils' +import { DidDocument } from '../../domain' +import { DidDocumentRole } from '../../domain/DidDocumentRole' +import { DidRepository, DidRecord } from '../../repository' + +import { PeerDidNumAlgo } from './didPeer' +import { keyToNumAlgo0DidDocument } from './peerDidNumAlgo0' +import { didDocumentJsonToNumAlgo1Did } from './peerDidNumAlgo1' +import { didDocumentToNumAlgo2Did } from './peerDidNumAlgo2' + +@injectable() +export class PeerDidRegistrar implements DidRegistrar { + public readonly supportedMethods = ['peer'] + private didRepository: DidRepository + + public constructor(didRepository: DidRepository) { + this.didRepository = didRepository + } + + public async create( + agentContext: AgentContext, + options: PeerDidNumAlgo0CreateOptions | PeerDidNumAlgo1CreateOptions | PeerDidNumAlgo2CreateOptions + ): Promise { + let didDocument: DidDocument + + try { + if (isPeerDidNumAlgo0CreateOptions(options)) { + const keyType = options.options.keyType + const seed = options.secret?.seed + + if (!keyType) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Missing key type', + }, + } + } + + if (seed && (typeof seed !== 'string' || seed.length !== 32)) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Invalid seed provided', + }, + } + } + + const key = await agentContext.wallet.createKey({ + keyType, + seed, + }) + + // TODO: validate did:peer document + + didDocument = keyToNumAlgo0DidDocument(key) + } else if (isPeerDidNumAlgo1CreateOptions(options)) { + const didDocumentJson = options.didDocument.toJSON() + const did = didDocumentJsonToNumAlgo1Did(didDocumentJson) + + didDocument = JsonTransformer.fromJSON({ ...didDocumentJson, id: did }, DidDocument) + } else if (isPeerDidNumAlgo2CreateOptions(options)) { + const didDocumentJson = options.didDocument.toJSON() + const did = didDocumentToNumAlgo2Did(options.didDocument) + + didDocument = JsonTransformer.fromJSON({ ...didDocumentJson, id: did }, DidDocument) + } else { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `Missing or incorrect numAlgo provided`, + }, + } + } + + // Save the did so we know we created it and can use it for didcomm + const didRecord = new DidRecord({ + id: didDocument.id, + role: DidDocumentRole.Created, + didDocument: isPeerDidNumAlgo1CreateOptions(options) ? didDocument : undefined, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + }, + }) + await this.didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: didDocument.id, + didDocument, + secret: { + // FIXME: the uni-registrar creates the seed in the registrar method + // if it doesn't exist so the seed can always be returned. Currently + // we can only return it if the seed was passed in by the user. Once + // we have a secure method for generating seeds we should use the same + // approach + seed: options.secret?.seed, + }, + }, + } + } catch (error) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `unknown error: ${error.message}`, + }, + } + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:peer not implemented yet`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:peer not implemented yet`, + }, + } + } +} + +function isPeerDidNumAlgo1CreateOptions(options: PeerDidCreateOptions): options is PeerDidNumAlgo1CreateOptions { + return options.options.numAlgo === PeerDidNumAlgo.GenesisDoc +} + +function isPeerDidNumAlgo0CreateOptions(options: PeerDidCreateOptions): options is PeerDidNumAlgo0CreateOptions { + return options.options.numAlgo === PeerDidNumAlgo.InceptionKeyWithoutDoc +} + +function isPeerDidNumAlgo2CreateOptions(options: PeerDidCreateOptions): options is PeerDidNumAlgo2CreateOptions { + return options.options.numAlgo === PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc +} + +export type PeerDidCreateOptions = + | PeerDidNumAlgo0CreateOptions + | PeerDidNumAlgo1CreateOptions + | PeerDidNumAlgo2CreateOptions + +export interface PeerDidNumAlgo0CreateOptions extends DidCreateOptions { + method: 'peer' + did?: never + didDocument?: never + options: { + keyType: KeyType.Ed25519 + numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc + } + secret?: { + seed?: string + } +} + +export interface PeerDidNumAlgo1CreateOptions extends DidCreateOptions { + method: 'peer' + did?: never + didDocument: DidDocument + options: { + numAlgo: PeerDidNumAlgo.GenesisDoc + } + secret?: undefined +} + +export interface PeerDidNumAlgo2CreateOptions extends DidCreateOptions { + method: 'peer' + did?: never + didDocument: DidDocument + options: { + numAlgo: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc + } + secret?: undefined +} + +// Update and Deactivate not supported for did:peer +export type PeerDidUpdateOptions = never +export type PeerDidDeactivateOptions = never diff --git a/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts b/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts index 85fad84c54..3ee2c1b473 100644 --- a/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts +++ b/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts @@ -1,15 +1,17 @@ import type { AgentContext } from '../../../../agent' import type { DidDocument } from '../../domain' import type { DidResolver } from '../../domain/DidResolver' -import type { DidRepository } from '../../repository' import type { DidResolutionResult } from '../../types' import { AriesFrameworkError } from '../../../../error' +import { injectable } from '../../../../plugins' +import { DidRepository } from '../../repository' import { getNumAlgoFromPeerDid, isValidPeerDid, PeerDidNumAlgo } from './didPeer' import { didToNumAlgo0DidDocument } from './peerDidNumAlgo0' import { didToNumAlgo2DidDocument } from './peerDidNumAlgo2' +@injectable() export class PeerDidResolver implements DidResolver { public readonly supportedMethods = ['peer'] diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/didPeer.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts similarity index 100% rename from packages/core/src/modules/dids/methods/peer/__tests__/didPeer.test.ts rename to packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts~9399a710 (feat: find existing connection based on invitation did (#698)) b/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts~9399a710 (feat: find existing connection based on invitation did (#698)) deleted file mode 100644 index abc697a492..0000000000 --- a/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts~9399a710 (feat: find existing connection based on invitation did (#698)) +++ /dev/null @@ -1,117 +0,0 @@ -import { JsonTransformer } from '../../../../../utils' -import didKeyBls12381g1 from '../../../__tests__/__fixtures__/didKeyBls12381g1.json' -import didKeyBls12381g1g2 from '../../../__tests__/__fixtures__/didKeyBls12381g1g2.json' -import didKeyBls12381g2 from '../../../__tests__/__fixtures__/didKeyBls12381g2.json' -import didKeyEd25519 from '../../../__tests__/__fixtures__/didKeyEd25519.json' -import didKeyX25519 from '../../../__tests__/__fixtures__/didKeyX25519.json' -import { DidDocument, Key } from '../../../domain' -import { DidPeer, PeerDidNumAlgo } from '../DidPeer' -import { outOfBandServiceToNumAlgo2Did } from '../peerDidNumAlgo2' - -import didPeer1zQmRDidCommServices from './__fixtures__/didPeer1zQmR-did-comm-service.json' -import didPeer1zQmR from './__fixtures__/didPeer1zQmR.json' -import didPeer1zQmZ from './__fixtures__/didPeer1zQmZ.json' -import didPeer2Ez6L from './__fixtures__/didPeer2Ez6L.json' - -describe('DidPeer', () => { - test('transforms a key correctly into a peer did method 0 did document', async () => { - const didDocuments = [didKeyEd25519, didKeyBls12381g1, didKeyX25519, didKeyBls12381g1g2, didKeyBls12381g2] - - for (const didDocument of didDocuments) { - const key = Key.fromFingerprint(didDocument.id.split(':')[2]) - - const didPeer = DidPeer.fromKey(key) - const expectedDidPeerDocument = JSON.parse( - JSON.stringify(didDocument).replace(new RegExp('did:key:', 'g'), 'did:peer:0') - ) - - expect(didPeer.didDocument.toJSON()).toMatchObject(expectedDidPeerDocument) - } - }) - - test('transforms a method 2 did correctly into a did document', () => { - expect(DidPeer.fromDid(didPeer2Ez6L.id).didDocument.toJSON()).toMatchObject(didPeer2Ez6L) - }) - - test('transforms a method 0 did correctly into a did document', () => { - const didDocuments = [didKeyEd25519, didKeyBls12381g1, didKeyX25519, didKeyBls12381g1g2, didKeyBls12381g2] - - for (const didDocument of didDocuments) { - const didPeer = DidPeer.fromDid(didDocument.id.replace('did:key:', 'did:peer:0')) - const expectedDidPeerDocument = JSON.parse( - JSON.stringify(didDocument).replace(new RegExp('did:key:', 'g'), 'did:peer:0') - ) - - expect(didPeer.didDocument.toJSON()).toMatchObject(expectedDidPeerDocument) - } - }) - - test('transforms a did document into a valid method 2 did', () => { - const didPeer2 = DidPeer.fromDidDocument( - JsonTransformer.fromJSON(didPeer2Ez6L, DidDocument), - PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc - ) - - expect(didPeer2.did).toBe(didPeer2Ez6L.id) - }) - - test('transforms a did comm service into a valid method 2 did', () => { - const didDocument = JsonTransformer.fromJSON(didPeer1zQmRDidCommServices, DidDocument) - const peerDid = outOfBandServiceToNumAlgo2Did(didDocument.didCommServices[0]) - const peerDidInstance = DidPeer.fromDid(peerDid) - - // TODO the following `console.log` statement throws an error "TypeError: Cannot read property 'toLowerCase' - // of undefined" because of this: - // - // `service.id = `${did}#${service.type.toLowerCase()}-${serviceIndex++}`` - - // console.log(peerDidInstance.didDocument) - - expect(peerDid).toBe( - 'did:peer:2.Ez6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCJ9' - ) - expect(peerDid).toBe(peerDidInstance.did) - }) - - test('transforms a did document into a valid method 1 did', () => { - const didPeer1 = DidPeer.fromDidDocument( - JsonTransformer.fromJSON(didPeer1zQmR, DidDocument), - PeerDidNumAlgo.GenesisDoc - ) - - expect(didPeer1.did).toBe(didPeer1zQmR.id) - }) - - // FIXME: we need some input data from AFGO for this test to succeed (we create a hash of the document, so any inconsistency is fatal) - xtest('transforms a did document from aries-framework-go into a valid method 1 did', () => { - const didPeer1 = DidPeer.fromDidDocument( - JsonTransformer.fromJSON(didPeer1zQmZ, DidDocument), - PeerDidNumAlgo.GenesisDoc - ) - - expect(didPeer1.did).toBe(didPeer1zQmZ.id) - }) - - test('extracts the numAlgo from the peer did', async () => { - // NumAlgo 0 - const key = Key.fromFingerprint(didKeyEd25519.id.split(':')[2]) - const didPeerNumAlgo0 = DidPeer.fromKey(key) - - expect(didPeerNumAlgo0.numAlgo).toBe(PeerDidNumAlgo.InceptionKeyWithoutDoc) - expect(DidPeer.fromDid('did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL').numAlgo).toBe( - PeerDidNumAlgo.InceptionKeyWithoutDoc - ) - - // NumAlgo 1 - const peerDidNumAlgo1 = 'did:peer:1zQmZMygzYqNwU6Uhmewx5Xepf2VLp5S4HLSwwgf2aiKZuwa' - expect(DidPeer.fromDid(peerDidNumAlgo1).numAlgo).toBe(PeerDidNumAlgo.GenesisDoc) - - // NumAlgo 2 - const peerDidNumAlgo2 = - 'did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' - expect(DidPeer.fromDid(peerDidNumAlgo2).numAlgo).toBe(PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc) - expect(DidPeer.fromDidDocument(JsonTransformer.fromJSON(didPeer2Ez6L, DidDocument)).numAlgo).toBe( - PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc - ) - }) -}) diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts new file mode 100644 index 0000000000..c6843609a3 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts @@ -0,0 +1,367 @@ +import type { Wallet } from '../../../../../wallet' + +import { getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { DidCommV1Service, DidDocumentBuilder } from '../../../domain' +import { DidDocumentRole } from '../../../domain/DidDocumentRole' +import { getEd25519VerificationMethod } from '../../../domain/key-type/ed25519' +import { DidRepository } from '../../../repository/DidRepository' +import { PeerDidRegistrar } from '../PeerDidRegistrar' +import { PeerDidNumAlgo } from '../didPeer' + +import didPeer0z6MksLeFixture from './__fixtures__/didPeer0z6MksLe.json' + +jest.mock('../../../repository/DidRepository') +const DidRepositoryMock = DidRepository as jest.Mock + +const walletMock = { + createKey: jest.fn(() => Key.fromFingerprint('z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU')), +} as unknown as Wallet +const agentContext = getAgentContext({ wallet: walletMock }) + +describe('DidRegistrar', () => { + describe('PeerDidRegistrar', () => { + let peerDidRegistrar: PeerDidRegistrar + let didRepositoryMock: DidRepository + + beforeEach(() => { + didRepositoryMock = new DidRepositoryMock() + peerDidRegistrar = new PeerDidRegistrar(didRepositoryMock) + }) + + describe('did:peer:0', () => { + it('should correctly create a did:peer:0 document using Ed25519 key type', async () => { + const seed = '96213c3d7fc8d4d6754c712fd969598e' + + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + options: { + keyType: KeyType.Ed25519, + numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, + }, + secret: { + seed, + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU', + didDocument: didPeer0z6MksLeFixture, + secret: { + seed: '96213c3d7fc8d4d6754c712fd969598e', + }, + }, + }) + }) + + it('should return an error state if no key type is provided', async () => { + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + // @ts-expect-error - key type is required in interface + options: { + numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Missing key type', + }, + }) + }) + + it('should return an error state if an invalid seed is provided', async () => { + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + options: { + keyType: KeyType.Ed25519, + numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, + }, + secret: { + seed: 'invalid', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Invalid seed provided', + }, + }) + }) + + it('should store the did without the did document', async () => { + const seed = '96213c3d7fc8d4d6754c712fd969598e' + const did = 'did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU' + + await peerDidRegistrar.create(agentContext, { + method: 'peer', + options: { + keyType: KeyType.Ed25519, + numAlgo: PeerDidNumAlgo.InceptionKeyWithoutDoc, + }, + secret: { + seed, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + id: did, + role: DidDocumentRole.Created, + _tags: { + recipientKeyFingerprints: [], + }, + didDocument: undefined, + }) + }) + }) + + describe('did:peer:1', () => { + const verificationMethod = getEd25519VerificationMethod({ + key: Key.fromFingerprint('z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz'), + // controller in method 1 did should be #id + controller: '#id', + id: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', + }) + + const didDocument = new DidDocumentBuilder('') + .addVerificationMethod(verificationMethod) + .addAuthentication(verificationMethod.id) + .addService( + new DidCommV1Service({ + id: '#service-0', + recipientKeys: [verificationMethod.id], + serviceEndpoint: 'https://example.com', + accept: ['didcomm/aip2;env=rfc19'], + }) + ) + .build() + + it('should correctly create a did:peer:1 document from a did document', async () => { + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + didDocument: didDocument, + options: { + numAlgo: PeerDidNumAlgo.GenesisDoc, + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:peer:1zQmUTNcSy2J2sAmX6Ad2bdPvhVnHPUaod8Skpt8DWPpZaiL', + didDocument: { + '@context': ['https://w3id.org/did/v1'], + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + id: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', + type: 'Ed25519VerificationKey2018', + controller: '#id', + publicKeyBase58: '7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE', + }, + ], + service: [ + { + id: '#service-0', + serviceEndpoint: 'https://example.com', + type: 'did-communication', + priority: 0, + recipientKeys: ['#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16'], + accept: ['didcomm/aip2;env=rfc19'], + }, + ], + authentication: ['#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16'], + assertionMethod: undefined, + keyAgreement: undefined, + capabilityInvocation: undefined, + capabilityDelegation: undefined, + id: 'did:peer:1zQmUTNcSy2J2sAmX6Ad2bdPvhVnHPUaod8Skpt8DWPpZaiL', + }, + }, + }) + }) + + it('should store the did with the did document', async () => { + const did = 'did:peer:1zQmUTNcSy2J2sAmX6Ad2bdPvhVnHPUaod8Skpt8DWPpZaiL' + + const { didState } = await peerDidRegistrar.create(agentContext, { + method: 'peer', + didDocument, + options: { + numAlgo: PeerDidNumAlgo.GenesisDoc, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + id: did, + didDocument: didState.didDocument, + role: DidDocumentRole.Created, + _tags: { + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + }, + }) + }) + }) + + describe('did:peer:2', () => { + const key = Key.fromFingerprint('z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz') + const verificationMethod = getEd25519VerificationMethod({ + key, + // controller in method 1 did should be #id + controller: '#id', + // Use relative id for peer dids + id: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', + }) + + const didDocument = new DidDocumentBuilder('') + .addVerificationMethod(verificationMethod) + .addAuthentication(verificationMethod.id) + .addService( + new DidCommV1Service({ + id: '#service-0', + recipientKeys: [verificationMethod.id], + serviceEndpoint: 'https://example.com', + accept: ['didcomm/aip2;env=rfc19'], + }) + ) + .build() + + it('should correctly create a did:peer:2 document from a did document', async () => { + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + didDocument: didDocument, + options: { + numAlgo: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:peer:2.Vz6MkkjPVCX7M8D6jJSCQNzYb4T6giuSN8Fm463gWNZ65DMSc.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbIiM0MWZiMmVjNy0xZjhiLTQyYmYtOTFhMi00ZWY5MDkyZGRjMTYiXSwiYSI6WyJkaWRjb21tL2FpcDI7ZW52PXJmYzE5Il19', + didDocument: { + '@context': ['https://w3id.org/did/v1'], + id: 'did:peer:2.Vz6MkkjPVCX7M8D6jJSCQNzYb4T6giuSN8Fm463gWNZ65DMSc.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbIiM0MWZiMmVjNy0xZjhiLTQyYmYtOTFhMi00ZWY5MDkyZGRjMTYiXSwiYSI6WyJkaWRjb21tL2FpcDI7ZW52PXJmYzE5Il19', + service: [ + { + serviceEndpoint: 'https://example.com', + type: 'did-communication', + priority: 0, + recipientKeys: ['#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16'], + accept: ['didcomm/aip2;env=rfc19'], + id: '#service-0', + }, + ], + verificationMethod: [ + { + id: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', + type: 'Ed25519VerificationKey2018', + controller: '#id', + publicKeyBase58: '7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE', + }, + ], + authentication: ['#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16'], + }, + secret: {}, + }, + }) + }) + + it('should store the did without the did document', async () => { + const did = + 'did:peer:2.Vz6MkkjPVCX7M8D6jJSCQNzYb4T6giuSN8Fm463gWNZ65DMSc.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbIiM0MWZiMmVjNy0xZjhiLTQyYmYtOTFhMi00ZWY5MDkyZGRjMTYiXSwiYSI6WyJkaWRjb21tL2FpcDI7ZW52PXJmYzE5Il19' + + await peerDidRegistrar.create(agentContext, { + method: 'peer', + didDocument, + options: { + numAlgo: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + id: did, + role: DidDocumentRole.Created, + _tags: { + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + }, + didDocument: undefined, + }) + }) + }) + + it('should return an error state if an unsupported numAlgo is provided', async () => { + const result = await peerDidRegistrar.create( + agentContext, + // @ts-expect-error - this is not a valid numAlgo + { + method: 'peer', + options: { + numAlgo: 4, + }, + } + ) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Missing or incorrect numAlgo provided', + }, + }) + }) + + it('should return an error state when calling update', async () => { + const result = await peerDidRegistrar.update() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:peer not implemented yet`, + }, + }) + }) + + it('should return an error state when calling deactivate', async () => { + const result = await peerDidRegistrar.deactivate() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:peer not implemented yet`, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer0z6MksLe.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer0z6MksLe.json new file mode 100644 index 0000000000..21142434f5 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer0z6MksLe.json @@ -0,0 +1,36 @@ +{ + "id": "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU", + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "verificationMethod": [ + { + "id": "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU", + "type": "Ed25519VerificationKey2018", + "controller": "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU", + "publicKeyBase58": "DtPcLpky6Yi6zPecfW8VZH3xNoDkvQfiGWp8u5n9nAj6" + } + ], + "authentication": [ + "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ], + "assertionMethod": [ + "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ], + "keyAgreement": [ + { + "id": "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz", + "type": "X25519KeyAgreementKey2019", + "controller": "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU", + "publicKeyBase58": "7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE" + } + ], + "capabilityInvocation": [ + "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ], + "capabilityDelegation": [ + "did:peer:0z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU#z6MksLeew51QS6Ca6tVKM56LQNbxCNVcLHv4xXj4jMkAhPWU" + ] +} diff --git a/packages/core/src/modules/dids/domain/createPeerDidFromServices.ts b/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts similarity index 77% rename from packages/core/src/modules/dids/domain/createPeerDidFromServices.ts rename to packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts index 6f4dfe6a00..8f34254a92 100644 --- a/packages/core/src/modules/dids/domain/createPeerDidFromServices.ts +++ b/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts @@ -1,18 +1,17 @@ -import type { ResolvedDidCommService } from '../../../agent/MessageSender' +import type { ResolvedDidCommService } from '../../../../agent/MessageSender' import { convertPublicKeyToX25519 } from '@stablelib/ed25519' -import { KeyType, Key } from '../../../crypto' -import { AriesFrameworkError } from '../../../error' -import { uuid } from '../../../utils/uuid' -import { DidKey } from '../methods/key' +import { KeyType, Key } from '../../../../crypto' +import { AriesFrameworkError } from '../../../../error' +import { uuid } from '../../../../utils/uuid' +import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' +import { getEd25519VerificationMethod } from '../../domain/key-type/ed25519' +import { getX25519VerificationMethod } from '../../domain/key-type/x25519' +import { DidCommV1Service } from '../../domain/service/DidCommV1Service' +import { DidKey } from '../key' -import { DidDocumentBuilder } from './DidDocumentBuilder' -import { getEd25519VerificationMethod } from './key-type/ed25519' -import { getX25519VerificationMethod } from './key-type/x25519' -import { DidCommV1Service } from './service/DidCommV1Service' - -export function createDidDocumentFromServices(services: ResolvedDidCommService[]) { +export function createPeerDidDocumentFromServices(services: ResolvedDidCommService[]) { const didDocumentBuilder = new DidDocumentBuilder('') // Keep track off all added key id based on the fingerprint so we can add them to the recipientKeys as references diff --git a/packages/core/src/modules/dids/methods/peer/index.ts b/packages/core/src/modules/dids/methods/peer/index.ts new file mode 100644 index 0000000000..aa2eb72e57 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/index.ts @@ -0,0 +1,4 @@ +export * from './PeerDidRegistrar' +export * from './PeerDidResolver' +export * from './didPeer' +export * from './createPeerDidDocumentFromServices' diff --git a/packages/core/src/modules/dids/methods/sov/SovDidRegistrar.ts b/packages/core/src/modules/dids/methods/sov/SovDidRegistrar.ts new file mode 100644 index 0000000000..cf5350ccbd --- /dev/null +++ b/packages/core/src/modules/dids/methods/sov/SovDidRegistrar.ts @@ -0,0 +1,195 @@ +import type { AgentContext } from '../../../../agent' +import type { IndyEndpointAttrib } from '../../../ledger' +import type { DidRegistrar } from '../../domain/DidRegistrar' +import type { DidCreateOptions, DidCreateResult, DidDeactivateResult, DidUpdateResult } from '../../types' +import type * as Indy from 'indy-sdk' + +import { AgentDependencies } from '../../../../agent/AgentDependencies' +import { InjectionSymbols } from '../../../../constants' +import { inject, injectable } from '../../../../plugins' +import { assertIndyWallet } from '../../../../wallet/util/assertIndyWallet' +import { IndyLedgerService, IndyPoolService } from '../../../ledger' +import { DidDocumentRole } from '../../domain/DidDocumentRole' +import { DidRecord, DidRepository } from '../../repository' + +import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './util' + +@injectable() +export class SovDidRegistrar implements DidRegistrar { + public readonly supportedMethods = ['sov'] + private didRepository: DidRepository + private indy: typeof Indy + private indyLedgerService: IndyLedgerService + private indyPoolService: IndyPoolService + + public constructor( + didRepository: DidRepository, + indyLedgerService: IndyLedgerService, + indyPoolService: IndyPoolService, + @inject(InjectionSymbols.AgentDependencies) agentDependencies: AgentDependencies + ) { + this.didRepository = didRepository + this.indy = agentDependencies.indy + this.indyLedgerService = indyLedgerService + this.indyPoolService = indyPoolService + } + + public async create(agentContext: AgentContext, options: SovDidCreateOptions): Promise { + const { alias, role, submitterDid } = options.options + const seed = options.secret?.seed + + if (seed && (typeof seed !== 'string' || seed.length !== 32)) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Invalid seed provided', + }, + } + } + + if (!submitterDid.startsWith('did:sov:')) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Submitter did must be a valid did:sov did', + }, + } + } + + try { + // NOTE: we need to use the createAndStoreMyDid method from indy to create the did + // If we just create a key and handle the creating of the did ourselves, indy will throw a + // WalletItemNotFound when it needs to sign ledger transactions using this did. This means we need + // to rely directly on the indy SDK, as we don't want to expose a createDid method just for. + // FIXME: once askar/indy-vdr is supported we need to adjust this to work with both indy-sdk and askar + assertIndyWallet(agentContext.wallet) + const [unqualifiedIndyDid, verkey] = await this.indy.createAndStoreMyDid(agentContext.wallet.handle, { + seed, + }) + + const qualifiedSovDid = `did:sov:${unqualifiedIndyDid}` + const unqualifiedSubmitterDid = submitterDid.replace('did:sov:', '') + + // TODO: it should be possible to pass the pool used for writing to the indy ledger service. + // The easiest way to do this would be to make the submitterDid a fully qualified did, including the indy namespace. + await this.indyLedgerService.registerPublicDid( + agentContext, + unqualifiedSubmitterDid, + unqualifiedIndyDid, + verkey, + alias, + role + ) + + // Create did document + const didDocumentBuilder = sovDidDocumentFromDid(qualifiedSovDid, verkey) + + // Add services if endpoints object was passed. + if (options.options.endpoints) { + await this.indyLedgerService.setEndpointsForDid(agentContext, unqualifiedIndyDid, options.options.endpoints) + addServicesFromEndpointsAttrib( + didDocumentBuilder, + qualifiedSovDid, + options.options.endpoints, + `${qualifiedSovDid}#key-agreement-1` + ) + } + + // Build did document. + const didDocument = didDocumentBuilder.build() + + // FIXME: we need to update this to the `indyNamespace` once https://github.com/hyperledger/aries-framework-javascript/issues/944 has been resolved + const indyNamespace = this.indyPoolService.ledgerWritePool.config.id + const qualifiedIndyDid = `did:indy:${indyNamespace}:${unqualifiedIndyDid}` + + // Save the did so we know we created it and can issue with it + const didRecord = new DidRecord({ + id: qualifiedSovDid, + role: DidDocumentRole.Created, + tags: { + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + qualifiedIndyDid, + }, + }) + await this.didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: { + qualifiedIndyDid, + }, + didRegistrationMetadata: { + indyNamespace, + }, + didState: { + state: 'finished', + did: qualifiedSovDid, + didDocument, + secret: { + // FIXME: the uni-registrar creates the seed in the registrar method + // if it doesn't exist so the seed can always be returned. Currently + // we can only return it if the seed was passed in by the user. Once + // we have a secure method for generating seeds we should use the same + // approach + seed: options.secret?.seed, + }, + }, + } + } catch (error) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:sov not implemented yet`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:sov not implemented yet`, + }, + } + } +} + +export interface SovDidCreateOptions extends DidCreateOptions { + method: 'sov' + did?: undefined + // As did:sov is so limited, we require everything needed to construct the did document to be passed + // through the options object. Once we support did:indy we can allow the didDocument property. + didDocument?: never + options: { + alias: string + role?: Indy.NymRole + endpoints?: IndyEndpointAttrib + submitterDid: string + } + secret?: { + seed?: string + } +} + +// Update and Deactivate not supported for did:sov +export type IndyDidUpdateOptions = never +export type IndyDidDeactivateOptions = never diff --git a/packages/core/src/modules/dids/methods/sov/SovDidResolver.ts b/packages/core/src/modules/dids/methods/sov/SovDidResolver.ts index 325b5cf185..79636d3fff 100644 --- a/packages/core/src/modules/dids/methods/sov/SovDidResolver.ts +++ b/packages/core/src/modules/dids/methods/sov/SovDidResolver.ts @@ -1,19 +1,13 @@ import type { AgentContext } from '../../../../agent' -import type { IndyEndpointAttrib, IndyLedgerService } from '../../../ledger' import type { DidResolver } from '../../domain/DidResolver' -import type { ParsedDid, DidResolutionResult } from '../../types' +import type { DidResolutionResult, ParsedDid } from '../../types' -import { convertPublicKeyToX25519 } from '@stablelib/ed25519' +import { injectable } from '../../../../plugins' +import { IndyLedgerService } from '../../../ledger' -import { TypedArrayEncoder } from '../../../../utils/TypedArrayEncoder' -import { getFullVerkey } from '../../../../utils/did' -import { SECURITY_X25519_CONTEXT_URL } from '../../../vc/constants' -import { ED25519_SUITE_CONTEXT_URL_2018 } from '../../../vc/signature-suites/ed25519/constants' -import { DidDocumentService } from '../../domain' -import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' -import { DidCommV1Service } from '../../domain/service/DidCommV1Service' -import { DidCommV2Service } from '../../domain/service/DidCommV2Service' +import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './util' +@injectable() export class SovDidResolver implements DidResolver { private indyLedgerService: IndyLedgerService @@ -28,37 +22,11 @@ export class SovDidResolver implements DidResolver { try { const nym = await this.indyLedgerService.getPublicDid(agentContext, parsed.id) - const endpoints = await this.indyLedgerService.getEndpointsForDid(agentContext, did) + const endpoints = await this.indyLedgerService.getEndpointsForDid(agentContext, parsed.id) - const verificationMethodId = `${parsed.did}#key-1` const keyAgreementId = `${parsed.did}#key-agreement-1` - - const publicKeyBase58 = getFullVerkey(nym.did, nym.verkey) - const publicKeyX25519 = TypedArrayEncoder.toBase58( - convertPublicKeyToX25519(TypedArrayEncoder.fromBase58(publicKeyBase58)) - ) - - const builder = new DidDocumentBuilder(parsed.did) - - .addContext(ED25519_SUITE_CONTEXT_URL_2018) - .addContext(SECURITY_X25519_CONTEXT_URL) - .addVerificationMethod({ - controller: parsed.did, - id: verificationMethodId, - publicKeyBase58: getFullVerkey(nym.did, nym.verkey), - type: 'Ed25519VerificationKey2018', - }) - .addVerificationMethod({ - controller: parsed.did, - id: keyAgreementId, - publicKeyBase58: publicKeyX25519, - type: 'X25519KeyAgreementKey2019', - }) - .addAuthentication(verificationMethodId) - .addAssertionMethod(verificationMethodId) - .addKeyAgreement(keyAgreementId) - - this.addServices(builder, parsed, endpoints, keyAgreementId) + const builder = sovDidDocumentFromDid(parsed.did, nym.verkey) + addServicesFromEndpointsAttrib(builder, parsed.did, endpoints, keyAgreementId) return { didDocument: builder.build(), @@ -76,88 +44,4 @@ export class SovDidResolver implements DidResolver { } } } - - // Process Indy Attrib Endpoint Types according to: https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html > Read (Resolve) > DID Service Endpoint - private processEndpointTypes(types?: string[]) { - const expectedTypes = ['endpoint', 'did-communication', 'DIDComm'] - const defaultTypes = ['endpoint', 'did-communication'] - - // Return default types if types "is NOT present [or] empty" - if (!types || types?.length <= 0) { - return defaultTypes - } - - // Return default types if types "contain any other values" - for (const type of types) { - if (!expectedTypes.includes(type)) { - return defaultTypes - } - } - - // Return provided types - return types - } - - private addServices( - builder: DidDocumentBuilder, - parsed: ParsedDid, - endpoints: IndyEndpointAttrib, - keyAgreementId: string - ) { - const { endpoint, routingKeys, types, ...otherEndpoints } = endpoints - - if (endpoint) { - const processedTypes = this.processEndpointTypes(types) - - // If 'endpoint' included in types, add id to the services array - if (processedTypes.includes('endpoint')) { - builder.addService( - new DidDocumentService({ - id: `${parsed.did}#endpoint`, - serviceEndpoint: endpoint, - type: 'endpoint', - }) - ) - } - - // If 'did-communication' included in types, add DIDComm v1 entry - if (processedTypes.includes('did-communication')) { - builder.addService( - new DidCommV1Service({ - id: `${parsed.did}#did-communication`, - serviceEndpoint: endpoint, - priority: 0, - routingKeys: routingKeys ?? [], - recipientKeys: [keyAgreementId], - accept: ['didcomm/aip2;env=rfc19'], - }) - ) - - // If 'DIDComm' included in types, add DIDComm v2 entry - if (processedTypes.includes('DIDComm')) { - builder - .addService( - new DidCommV2Service({ - id: `${parsed.did}#didcomm-1`, - serviceEndpoint: endpoint, - routingKeys: routingKeys ?? [], - accept: ['didcomm/v2'], - }) - ) - .addContext('https://didcomm.org/messaging/contexts/v2') - } - } - } - - // Add other endpoint types - for (const [type, endpoint] of Object.entries(otherEndpoints)) { - builder.addService( - new DidDocumentService({ - id: `${parsed.did}#${type}`, - serviceEndpoint: endpoint as string, - type, - }) - ) - } - } } diff --git a/packages/core/src/modules/dids/methods/sov/__tests__/SovDidRegistrar.test.ts b/packages/core/src/modules/dids/methods/sov/__tests__/SovDidRegistrar.test.ts new file mode 100644 index 0000000000..e2a3041652 --- /dev/null +++ b/packages/core/src/modules/dids/methods/sov/__tests__/SovDidRegistrar.test.ts @@ -0,0 +1,345 @@ +import type { Wallet } from '../../../../../wallet' +import type { IndyPool } from '../../../../ledger' +import type * as Indy from 'indy-sdk' + +import { getAgentConfig, getAgentContext, mockFunction, mockProperty } from '../../../../../../tests/helpers' +import { SigningProviderRegistry } from '../../../../../crypto/signing-provider' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { IndyWallet } from '../../../../../wallet/IndyWallet' +import { IndyLedgerService } from '../../../../ledger/services/IndyLedgerService' +import { IndyPoolService } from '../../../../ledger/services/IndyPoolService' +import { DidDocumentRole } from '../../../domain/DidDocumentRole' +import { DidRepository } from '../../../repository/DidRepository' +import { SovDidRegistrar } from '../SovDidRegistrar' + +jest.mock('../../../repository/DidRepository') +const DidRepositoryMock = DidRepository as jest.Mock + +jest.mock('../../../../ledger/services/IndyLedgerService') +const IndyLedgerServiceMock = IndyLedgerService as jest.Mock + +jest.mock('../../../../ledger/services/IndyPoolService') +const IndyPoolServiceMock = IndyPoolService as jest.Mock +const indyPoolServiceMock = new IndyPoolServiceMock() +mockProperty(indyPoolServiceMock, 'ledgerWritePool', { config: { id: 'pool1' } } as IndyPool) + +const agentConfig = getAgentConfig('SovDidRegistrar') + +const createDidMock = jest.fn(async () => ['R1xKJw17sUoXhejEpugMYJ', 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu']) + +const wallet = new IndyWallet(agentConfig.agentDependencies, agentConfig.logger, new SigningProviderRegistry([])) +mockProperty(wallet, 'handle', 10) + +const agentContext = getAgentContext({ + wallet, +}) + +const didRepositoryMock = new DidRepositoryMock() +const indyLedgerServiceMock = new IndyLedgerServiceMock() +const sovDidRegistrar = new SovDidRegistrar(didRepositoryMock, indyLedgerServiceMock, indyPoolServiceMock, { + ...agentConfig.agentDependencies, + indy: { createAndStoreMyDid: createDidMock } as unknown as typeof Indy, +}) + +describe('DidRegistrar', () => { + describe('SovDidRegistrar', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + it('should return an error state if an invalid seed is provided', async () => { + const result = await sovDidRegistrar.create(agentContext, { + method: 'sov', + + options: { + submitterDid: 'did:sov:BzCbsNYhMrjHiqZDTUASHg', + alias: 'Hello', + }, + secret: { + seed: 'invalid', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Invalid seed provided', + }, + }) + }) + + it('should return an error state if the wallet is not an indy wallet', async () => { + const agentContext = getAgentContext({ + wallet: {} as unknown as Wallet, + }) + + const result = await sovDidRegistrar.create(agentContext, { + method: 'sov', + + options: { + submitterDid: 'did:sov:BzCbsNYhMrjHiqZDTUASHg', + alias: 'Hello', + }, + secret: { + seed: '12345678901234567890123456789012', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'unknownError: Expected wallet to be instance of IndyWallet, found Object', + }, + }) + }) + + it('should return an error state if the submitter did is not qualified with did:sov', async () => { + const result = await sovDidRegistrar.create(agentContext, { + method: 'sov', + options: { + submitterDid: 'BzCbsNYhMrjHiqZDTUASHg', + alias: 'Hello', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Submitter did must be a valid did:sov did', + }, + }) + }) + + it('should correctly create a did:sov document without services', async () => { + const seed = '96213c3d7fc8d4d6754c712fd969598e' + const result = await sovDidRegistrar.create(agentContext, { + method: 'sov', + options: { + alias: 'Hello', + submitterDid: 'did:sov:BzCbsNYhMrjHiqZDTUASHg', + role: 'STEWARD', + }, + secret: { + seed, + }, + }) + + expect(indyLedgerServiceMock.registerPublicDid).toHaveBeenCalledWith( + agentContext, + // Unqualified submitter did + 'BzCbsNYhMrjHiqZDTUASHg', + // Unqualified created indy did + 'R1xKJw17sUoXhejEpugMYJ', + // Verkey + 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + // Alias + 'Hello', + // Role + 'STEWARD' + ) + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: { + qualifiedIndyDid: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + }, + didRegistrationMetadata: { + indyNamespace: 'pool1', + }, + didState: { + state: 'finished', + did: 'did:sov:R1xKJw17sUoXhejEpugMYJ', + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:sov:R1xKJw17sUoXhejEpugMYJ', + verificationMethod: [ + { + id: 'did:sov:R1xKJw17sUoXhejEpugMYJ#key-1', + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:R1xKJw17sUoXhejEpugMYJ', + publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + }, + { + id: 'did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1', + type: 'X25519KeyAgreementKey2019', + controller: 'did:sov:R1xKJw17sUoXhejEpugMYJ', + publicKeyBase58: 'Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', + }, + ], + authentication: ['did:sov:R1xKJw17sUoXhejEpugMYJ#key-1'], + assertionMethod: ['did:sov:R1xKJw17sUoXhejEpugMYJ#key-1'], + keyAgreement: ['did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1'], + }, + secret: { + seed, + }, + }, + }) + }) + + it('should correctly create a did:sov document with services', async () => { + const seed = '96213c3d7fc8d4d6754c712fd969598e' + const result = await sovDidRegistrar.create(agentContext, { + method: 'sov', + options: { + alias: 'Hello', + submitterDid: 'did:sov:BzCbsNYhMrjHiqZDTUASHg', + role: 'STEWARD', + endpoints: { + endpoint: 'https://example.com/endpoint', + routingKeys: ['key-1'], + types: ['DIDComm', 'did-communication', 'endpoint'], + }, + }, + secret: { + seed, + }, + }) + + expect(indyLedgerServiceMock.registerPublicDid).toHaveBeenCalledWith( + agentContext, + // Unqualified submitter did + 'BzCbsNYhMrjHiqZDTUASHg', + // Unqualified created indy did + 'R1xKJw17sUoXhejEpugMYJ', + // Verkey + 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + // Alias + 'Hello', + // Role + 'STEWARD' + ) + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: { + qualifiedIndyDid: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + }, + didRegistrationMetadata: { + indyNamespace: 'pool1', + }, + didState: { + state: 'finished', + did: 'did:sov:R1xKJw17sUoXhejEpugMYJ', + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + 'https://didcomm.org/messaging/contexts/v2', + ], + id: 'did:sov:R1xKJw17sUoXhejEpugMYJ', + verificationMethod: [ + { + id: 'did:sov:R1xKJw17sUoXhejEpugMYJ#key-1', + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:R1xKJw17sUoXhejEpugMYJ', + publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + }, + { + id: 'did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1', + type: 'X25519KeyAgreementKey2019', + controller: 'did:sov:R1xKJw17sUoXhejEpugMYJ', + publicKeyBase58: 'Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', + }, + ], + service: [ + { + id: 'did:sov:R1xKJw17sUoXhejEpugMYJ#endpoint', + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }, + { + id: 'did:sov:R1xKJw17sUoXhejEpugMYJ#did-communication', + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + priority: 0, + recipientKeys: ['did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1'], + routingKeys: ['key-1'], + accept: ['didcomm/aip2;env=rfc19'], + }, + { + id: 'did:sov:R1xKJw17sUoXhejEpugMYJ#didcomm-1', + serviceEndpoint: 'https://example.com/endpoint', + type: 'DIDComm', + routingKeys: ['key-1'], + accept: ['didcomm/v2'], + }, + ], + authentication: ['did:sov:R1xKJw17sUoXhejEpugMYJ#key-1'], + assertionMethod: ['did:sov:R1xKJw17sUoXhejEpugMYJ#key-1'], + keyAgreement: ['did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1'], + }, + secret: { + seed, + }, + }, + }) + }) + + it('should store the did document', async () => { + const seed = '96213c3d7fc8d4d6754c712fd969598e' + await sovDidRegistrar.create(agentContext, { + method: 'sov', + options: { + alias: 'Hello', + submitterDid: 'did:sov:BzCbsNYhMrjHiqZDTUASHg', + role: 'STEWARD', + endpoints: { + endpoint: 'https://example.com/endpoint', + routingKeys: ['key-1'], + types: ['DIDComm', 'did-communication', 'endpoint'], + }, + }, + secret: { + seed, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + id: 'did:sov:R1xKJw17sUoXhejEpugMYJ', + role: DidDocumentRole.Created, + _tags: { + recipientKeyFingerprints: ['z6LSrH6AdsQeZuKKmG6Ehx7abEQZsVg2psR2VU536gigUoAe'], + qualifiedIndyDid: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + }, + didDocument: undefined, + }) + }) + + it('should return an error state when calling update', async () => { + const result = await sovDidRegistrar.update() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:sov not implemented yet`, + }, + }) + }) + + it('should return an error state when calling deactivate', async () => { + const result = await sovDidRegistrar.deactivate() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:sov not implemented yet`, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/sov/index.ts b/packages/core/src/modules/dids/methods/sov/index.ts new file mode 100644 index 0000000000..82c05ea971 --- /dev/null +++ b/packages/core/src/modules/dids/methods/sov/index.ts @@ -0,0 +1,2 @@ +export * from './SovDidRegistrar' +export * from './SovDidResolver' diff --git a/packages/core/src/modules/dids/methods/sov/util.ts b/packages/core/src/modules/dids/methods/sov/util.ts new file mode 100644 index 0000000000..638779dd21 --- /dev/null +++ b/packages/core/src/modules/dids/methods/sov/util.ts @@ -0,0 +1,123 @@ +import type { IndyEndpointAttrib } from '../../../ledger' + +import { TypedArrayEncoder } from '../../../../utils' +import { getFullVerkey } from '../../../../utils/did' +import { SECURITY_X25519_CONTEXT_URL } from '../../../vc/constants' +import { ED25519_SUITE_CONTEXT_URL_2018 } from '../../../vc/signature-suites/ed25519/constants' +import { DidDocumentService, DidDocumentBuilder, DidCommV1Service, DidCommV2Service } from '../../domain' +import { convertPublicKeyToX25519 } from '../../domain/key-type/ed25519' + +export function sovDidDocumentFromDid(fullDid: string, verkey: string) { + const verificationMethodId = `${fullDid}#key-1` + const keyAgreementId = `${fullDid}#key-agreement-1` + + const publicKeyBase58 = getFullVerkey(fullDid, verkey) + const publicKeyX25519 = TypedArrayEncoder.toBase58( + convertPublicKeyToX25519(TypedArrayEncoder.fromBase58(publicKeyBase58)) + ) + + const builder = new DidDocumentBuilder(fullDid) + .addContext(ED25519_SUITE_CONTEXT_URL_2018) + .addContext(SECURITY_X25519_CONTEXT_URL) + .addVerificationMethod({ + controller: fullDid, + id: verificationMethodId, + publicKeyBase58: publicKeyBase58, + type: 'Ed25519VerificationKey2018', + }) + .addVerificationMethod({ + controller: fullDid, + id: keyAgreementId, + publicKeyBase58: publicKeyX25519, + type: 'X25519KeyAgreementKey2019', + }) + .addAuthentication(verificationMethodId) + .addAssertionMethod(verificationMethodId) + .addKeyAgreement(keyAgreementId) + + return builder +} + +// Process Indy Attrib Endpoint Types according to: https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html > Read (Resolve) > DID Service Endpoint +function processEndpointTypes(types?: string[]) { + const expectedTypes = ['endpoint', 'did-communication', 'DIDComm'] + const defaultTypes = ['endpoint', 'did-communication'] + + // Return default types if types "is NOT present [or] empty" + if (!types || types.length <= 0) { + return defaultTypes + } + + // Return default types if types "contain any other values" + for (const type of types) { + if (!expectedTypes.includes(type)) { + return defaultTypes + } + } + + // Return provided types + return types +} + +export function addServicesFromEndpointsAttrib( + builder: DidDocumentBuilder, + did: string, + endpoints: IndyEndpointAttrib, + keyAgreementId: string +) { + const { endpoint, routingKeys, types, ...otherEndpoints } = endpoints + + if (endpoint) { + const processedTypes = processEndpointTypes(types) + + // If 'endpoint' included in types, add id to the services array + if (processedTypes.includes('endpoint')) { + builder.addService( + new DidDocumentService({ + id: `${did}#endpoint`, + serviceEndpoint: endpoint, + type: 'endpoint', + }) + ) + } + + // If 'did-communication' included in types, add DIDComm v1 entry + if (processedTypes.includes('did-communication')) { + builder.addService( + new DidCommV1Service({ + id: `${did}#did-communication`, + serviceEndpoint: endpoint, + priority: 0, + routingKeys: routingKeys ?? [], + recipientKeys: [keyAgreementId], + accept: ['didcomm/aip2;env=rfc19'], + }) + ) + + // If 'DIDComm' included in types, add DIDComm v2 entry + if (processedTypes.includes('DIDComm')) { + builder + .addService( + new DidCommV2Service({ + id: `${did}#didcomm-1`, + serviceEndpoint: endpoint, + routingKeys: routingKeys ?? [], + accept: ['didcomm/v2'], + }) + ) + .addContext('https://didcomm.org/messaging/contexts/v2') + } + } + } + + // Add other endpoint types + for (const [type, endpoint] of Object.entries(otherEndpoints)) { + builder.addService( + new DidDocumentService({ + id: `${did}#${type}`, + serviceEndpoint: endpoint as string, + type, + }) + ) + } +} diff --git a/packages/core/src/modules/dids/methods/web/WebDidResolver.ts b/packages/core/src/modules/dids/methods/web/WebDidResolver.ts index 77d9b1e295..84cac28e59 100644 --- a/packages/core/src/modules/dids/methods/web/WebDidResolver.ts +++ b/packages/core/src/modules/dids/methods/web/WebDidResolver.ts @@ -5,9 +5,11 @@ import type { ParsedDid, DidResolutionResult, DidResolutionOptions } from '../.. import { Resolver } from 'did-resolver' import * as didWeb from 'web-did-resolver' +import { injectable } from '../../../../plugins' import { JsonTransformer } from '../../../../utils/JsonTransformer' import { DidDocument } from '../../domain' +@injectable() export class WebDidResolver implements DidResolver { public readonly supportedMethods diff --git a/packages/core/src/modules/dids/methods/web/index.ts b/packages/core/src/modules/dids/methods/web/index.ts new file mode 100644 index 0000000000..59e66593dd --- /dev/null +++ b/packages/core/src/modules/dids/methods/web/index.ts @@ -0,0 +1 @@ +export * from './WebDidResolver' diff --git a/packages/core/src/modules/dids/repository/DidRepository.ts b/packages/core/src/modules/dids/repository/DidRepository.ts index 3384558c7a..854d2e6d46 100644 --- a/packages/core/src/modules/dids/repository/DidRepository.ts +++ b/packages/core/src/modules/dids/repository/DidRepository.ts @@ -6,6 +6,7 @@ import { InjectionSymbols } from '../../../constants' import { inject, injectable } from '../../../plugins' import { Repository } from '../../../storage/Repository' import { StorageService } from '../../../storage/StorageService' +import { DidDocumentRole } from '../domain/DidDocumentRole' import { DidRecord } from './DidRecord' @@ -25,4 +26,11 @@ export class DidRepository extends Repository { public findAllByRecipientKey(agentContext: AgentContext, recipientKey: Key) { return this.findByQuery(agentContext, { recipientKeyFingerprints: [recipientKey.fingerprint] }) } + + public getCreatedDids(agentContext: AgentContext, { method }: { method?: string }) { + return this.findByQuery(agentContext, { + role: DidDocumentRole.Created, + method, + }) + } } diff --git a/packages/core/src/modules/dids/services/DidRegistrarService.ts b/packages/core/src/modules/dids/services/DidRegistrarService.ts new file mode 100644 index 0000000000..43a77c8c25 --- /dev/null +++ b/packages/core/src/modules/dids/services/DidRegistrarService.ts @@ -0,0 +1,159 @@ +import type { AgentContext } from '../../../agent' +import type { DidRegistrar } from '../domain/DidRegistrar' +import type { + DidCreateOptions, + DidCreateResult, + DidDeactivateOptions, + DidDeactivateResult, + DidUpdateOptions, + DidUpdateResult, +} from '../types' + +import { InjectionSymbols } from '../../../constants' +import { Logger } from '../../../logger' +import { inject, injectable, injectAll } from '../../../plugins' +import { DidRegistrarToken } from '../domain/DidRegistrar' +import { tryParseDid } from '../domain/parse' + +@injectable() +export class DidRegistrarService { + private logger: Logger + private registrars: DidRegistrar[] + + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + @injectAll(DidRegistrarToken) registrars: DidRegistrar[] + ) { + this.logger = logger + this.registrars = registrars + } + + public async create( + agentContext: AgentContext, + options: CreateOptions + ): Promise { + this.logger.debug(`creating did ${options.did ?? options.method}`) + + const errorResult = { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: options.did, + }, + } as const + + if ((!options.did && !options.method) || (options.did && options.method)) { + return { + ...errorResult, + didState: { + ...errorResult.didState, + reason: 'Either did OR method must be specified', + }, + } + } + + const method = options.method ?? tryParseDid(options.did as string)?.method + if (!method) { + return { + ...errorResult, + didState: { + ...errorResult.didState, + reason: `Could not extract method from did ${options.did}`, + }, + } + } + + const registrar = this.findRegistrarForMethod(method) + if (!registrar) { + return { + ...errorResult, + didState: { + ...errorResult.didState, + reason: `Unsupported did method: '${method}'`, + }, + } + } + + return await registrar.create(agentContext, options) + } + + public async update(agentContext: AgentContext, options: DidUpdateOptions): Promise { + this.logger.debug(`updating did ${options.did}`) + + const method = tryParseDid(options.did)?.method + + const errorResult = { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: options.did, + }, + } as const + + if (!method) { + return { + ...errorResult, + didState: { + ...errorResult.didState, + reason: `Could not extract method from did ${options.did}`, + }, + } + } + + const registrar = this.findRegistrarForMethod(method) + if (!registrar) { + return { + ...errorResult, + didState: { + ...errorResult.didState, + reason: `Unsupported did method: '${method}'`, + }, + } + } + + return await registrar.update(agentContext, options) + } + + public async deactivate(agentContext: AgentContext, options: DidDeactivateOptions): Promise { + this.logger.debug(`deactivating did ${options.did}`) + + const errorResult = { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: options.did, + }, + } as const + + const method = tryParseDid(options.did)?.method + if (!method) { + return { + ...errorResult, + didState: { + ...errorResult.didState, + reason: `Could not extract method from did ${options.did}`, + }, + } + } + + const registrar = this.findRegistrarForMethod(method) + if (!registrar) { + return { + ...errorResult, + didState: { + ...errorResult.didState, + reason: `Unsupported did method: '${method}'`, + }, + } + } + + return await registrar.deactivate(agentContext, options) + } + + private findRegistrarForMethod(method: string): DidRegistrar | null { + return this.registrars.find((r) => r.supportedMethods.includes(method)) ?? null + } +} diff --git a/packages/core/src/modules/dids/services/DidResolverService.ts b/packages/core/src/modules/dids/services/DidResolverService.ts index 3a0020a8b5..a365706dc4 100644 --- a/packages/core/src/modules/dids/services/DidResolverService.ts +++ b/packages/core/src/modules/dids/services/DidResolverService.ts @@ -5,14 +5,9 @@ import type { DidResolutionOptions, DidResolutionResult, ParsedDid } from '../ty import { InjectionSymbols } from '../../../constants' import { AriesFrameworkError } from '../../../error' import { Logger } from '../../../logger' -import { injectable, inject } from '../../../plugins' -import { IndyLedgerService } from '../../ledger' +import { injectable, inject, injectAll } from '../../../plugins' +import { DidResolverToken } from '../domain/DidResolver' import { parseDid } from '../domain/parse' -import { KeyDidResolver } from '../methods/key/KeyDidResolver' -import { PeerDidResolver } from '../methods/peer/PeerDidResolver' -import { SovDidResolver } from '../methods/sov/SovDidResolver' -import { WebDidResolver } from '../methods/web/WebDidResolver' -import { DidRepository } from '../repository' @injectable() export class DidResolverService { @@ -20,18 +15,11 @@ export class DidResolverService { private resolvers: DidResolver[] public constructor( - indyLedgerService: IndyLedgerService, - didRepository: DidRepository, - @inject(InjectionSymbols.Logger) logger: Logger + @inject(InjectionSymbols.Logger) logger: Logger, + @injectAll(DidResolverToken) resolvers: DidResolver[] ) { this.logger = logger - - this.resolvers = [ - new SovDidResolver(indyLedgerService), - new WebDidResolver(), - new KeyDidResolver(), - new PeerDidResolver(didRepository), - ] + this.resolvers = resolvers } public async resolve( diff --git a/packages/core/src/modules/dids/services/__tests__/DidRegistrarService.test.ts b/packages/core/src/modules/dids/services/__tests__/DidRegistrarService.test.ts new file mode 100644 index 0000000000..b92659ebe4 --- /dev/null +++ b/packages/core/src/modules/dids/services/__tests__/DidRegistrarService.test.ts @@ -0,0 +1,199 @@ +import type { DidDocument, DidRegistrar } from '../../domain' + +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../tests/helpers' +import { DidRegistrarService } from '../DidRegistrarService' + +const agentConfig = getAgentConfig('DidResolverService') +const agentContext = getAgentContext() + +const didRegistrarMock = { + supportedMethods: ['key'], + create: jest.fn(), + update: jest.fn(), + deactivate: jest.fn(), +} as DidRegistrar + +const didRegistrarService = new DidRegistrarService(agentConfig.logger, [didRegistrarMock]) + +describe('DidResolverService', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('create', () => { + it('should correctly find and call the correct registrar for a specified did', async () => { + const returnValue = { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: ':(', + }, + } as const + mockFunction(didRegistrarMock.create).mockResolvedValue(returnValue) + + const result = await didRegistrarService.create(agentContext, { did: 'did:key:xxxx' }) + expect(result).toEqual(returnValue) + + expect(didRegistrarMock.create).toHaveBeenCalledTimes(1) + expect(didRegistrarMock.create).toHaveBeenCalledWith(agentContext, { did: 'did:key:xxxx' }) + }) + + it('should return error state failed if no did or method is provided', async () => { + const result = await didRegistrarService.create(agentContext, {}) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: undefined, + reason: 'Either did OR method must be specified', + }, + }) + }) + + it('should return error state failed if both did and method are provided', async () => { + const result = await didRegistrarService.create(agentContext, { did: 'did:key:xxxx', method: 'key' }) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: 'did:key:xxxx', + reason: 'Either did OR method must be specified', + }, + }) + }) + + it('should return error state failed if no method could be extracted from the did or method', async () => { + const result = await didRegistrarService.create(agentContext, { did: 'did:a' }) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: 'did:a', + reason: 'Could not extract method from did did:a', + }, + }) + }) + + it('should return error with state failed if the did has no registrar', async () => { + const result = await didRegistrarService.create(agentContext, { did: 'did:something:123' }) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: 'did:something:123', + reason: "Unsupported did method: 'something'", + }, + }) + }) + }) + + describe('update', () => { + it('should correctly find and call the correct registrar for a specified did', async () => { + const returnValue = { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: ':(', + }, + } as const + mockFunction(didRegistrarMock.update).mockResolvedValue(returnValue) + + const didDocument = {} as unknown as DidDocument + + const result = await didRegistrarService.update(agentContext, { did: 'did:key:xxxx', didDocument }) + expect(result).toEqual(returnValue) + + expect(didRegistrarMock.update).toHaveBeenCalledTimes(1) + expect(didRegistrarMock.update).toHaveBeenCalledWith(agentContext, { did: 'did:key:xxxx', didDocument }) + }) + + it('should return error state failed if no method could be extracted from the did', async () => { + const result = await didRegistrarService.update(agentContext, { did: 'did:a', didDocument: {} as DidDocument }) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: 'did:a', + reason: 'Could not extract method from did did:a', + }, + }) + }) + + it('should return error with state failed if the did has no registrar', async () => { + const result = await didRegistrarService.update(agentContext, { + did: 'did:something:123', + didDocument: {} as DidDocument, + }) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: 'did:something:123', + reason: "Unsupported did method: 'something'", + }, + }) + }) + }) + + describe('deactivate', () => { + it('should correctly find and call the correct registrar for a specified did', async () => { + const returnValue = { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: ':(', + }, + } as const + mockFunction(didRegistrarMock.deactivate).mockResolvedValue(returnValue) + + const result = await didRegistrarService.deactivate(agentContext, { did: 'did:key:xxxx' }) + expect(result).toEqual(returnValue) + + expect(didRegistrarMock.deactivate).toHaveBeenCalledTimes(1) + expect(didRegistrarMock.deactivate).toHaveBeenCalledWith(agentContext, { did: 'did:key:xxxx' }) + }) + + it('should return error state failed if no method could be extracted from the did', async () => { + const result = await didRegistrarService.deactivate(agentContext, { did: 'did:a' }) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: 'did:a', + reason: 'Could not extract method from did did:a', + }, + }) + }) + + it('should return error with state failed if the did has no registrar', async () => { + const result = await didRegistrarService.deactivate(agentContext, { did: 'did:something:123' }) + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + did: 'did:something:123', + reason: "Unsupported did method: 'something'", + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/dids/__tests__/DidResolverService.test.ts b/packages/core/src/modules/dids/services/__tests__/DidResolverService.test.ts similarity index 53% rename from packages/core/src/modules/dids/__tests__/DidResolverService.test.ts rename to packages/core/src/modules/dids/services/__tests__/DidResolverService.test.ts index 7dff728532..c472ec8899 100644 --- a/packages/core/src/modules/dids/__tests__/DidResolverService.test.ts +++ b/packages/core/src/modules/dids/services/__tests__/DidResolverService.test.ts @@ -1,33 +1,24 @@ -import type { IndyLedgerService } from '../../ledger' -import type { DidRepository } from '../repository' +import type { DidResolver } from '../../domain' -import { getAgentConfig, getAgentContext, mockProperty } from '../../../../tests/helpers' -import { JsonTransformer } from '../../../utils/JsonTransformer' -import { DidDocument } from '../domain' -import { parseDid } from '../domain/parse' -import { KeyDidResolver } from '../methods/key/KeyDidResolver' -import { DidResolverService } from '../services/DidResolverService' +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../tests/helpers' +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import didKeyEd25519Fixture from '../../__tests__/__fixtures__/didKeyEd25519.json' +import { DidDocument } from '../../domain' +import { parseDid } from '../../domain/parse' +import { DidResolverService } from '../DidResolverService' -import didKeyEd25519Fixture from './__fixtures__/didKeyEd25519.json' - -jest.mock('../methods/key/KeyDidResolver') +const didResolverMock = { + supportedMethods: ['key'], + resolve: jest.fn(), +} as DidResolver const agentConfig = getAgentConfig('DidResolverService') const agentContext = getAgentContext() describe('DidResolverService', () => { - const indyLedgerServiceMock = jest.fn() as unknown as IndyLedgerService - const didDocumentRepositoryMock = jest.fn() as unknown as DidRepository - const didResolverService = new DidResolverService( - indyLedgerServiceMock, - didDocumentRepositoryMock, - agentConfig.logger - ) + const didResolverService = new DidResolverService(agentConfig.logger, [didResolverMock]) it('should correctly find and call the correct resolver for a specified did', async () => { - const didKeyResolveSpy = jest.spyOn(KeyDidResolver.prototype, 'resolve') - mockProperty(KeyDidResolver.prototype, 'supportedMethods', ['key']) - const returnValue = { didDocument: JsonTransformer.fromJSON(didKeyEd25519Fixture, DidDocument), didDocumentMetadata: {}, @@ -35,13 +26,13 @@ describe('DidResolverService', () => { contentType: 'application/did+ld+json', }, } - didKeyResolveSpy.mockResolvedValue(returnValue) + mockFunction(didResolverMock.resolve).mockResolvedValue(returnValue) const result = await didResolverService.resolve(agentContext, 'did:key:xxxx', { someKey: 'string' }) expect(result).toEqual(returnValue) - expect(didKeyResolveSpy).toHaveBeenCalledTimes(1) - expect(didKeyResolveSpy).toHaveBeenCalledWith(agentContext, 'did:key:xxxx', parseDid('did:key:xxxx'), { + expect(didResolverMock.resolve).toHaveBeenCalledTimes(1) + expect(didResolverMock.resolve).toHaveBeenCalledWith(agentContext, 'did:key:xxxx', parseDid('did:key:xxxx'), { someKey: 'string', }) }) diff --git a/packages/core/src/modules/dids/services/index.ts b/packages/core/src/modules/dids/services/index.ts index 1b4265132d..9c86ace87a 100644 --- a/packages/core/src/modules/dids/services/index.ts +++ b/packages/core/src/modules/dids/services/index.ts @@ -1 +1,2 @@ export * from './DidResolverService' +export * from './DidRegistrarService' diff --git a/packages/core/src/modules/dids/types.ts b/packages/core/src/modules/dids/types.ts index 8c5231aa6e..257260f2e8 100644 --- a/packages/core/src/modules/dids/types.ts +++ b/packages/core/src/modules/dids/types.ts @@ -14,3 +14,74 @@ export interface DidResolutionResult { didDocument: DidDocument | null didDocumentMetadata: DidDocumentMetadata } + +// Based on https://identity.foundation/did-registration +export type DidRegistrationExtraOptions = Record +export type DidRegistrationSecretOptions = Record +export type DidRegistrationMetadata = Record +export type DidDocumentOperation = 'setDidDocument' | 'addToDidDocument' | 'removeFromDidDocument' + +export interface DidOperationStateFinished { + state: 'finished' + did: string + secret?: DidRegistrationSecretOptions + didDocument: DidDocument +} + +export interface DidOperationStateFailed { + state: 'failed' + did?: string + secret?: DidRegistrationSecretOptions + didDocument?: DidDocument + reason: string +} + +export interface DidOperationState { + state: 'action' | 'wait' + did?: string + secret?: DidRegistrationSecretOptions + didDocument?: DidDocument +} + +export interface DidCreateOptions { + method?: string + did?: string + options?: DidRegistrationExtraOptions + secret?: DidRegistrationSecretOptions + didDocument?: DidDocument +} + +export interface DidCreateResult { + jobId?: string + didState: DidOperationState | DidOperationStateFinished | DidOperationStateFailed + didRegistrationMetadata: DidRegistrationMetadata + didDocumentMetadata: DidResolutionMetadata +} + +export interface DidUpdateOptions { + did: string + options?: DidRegistrationExtraOptions + secret?: DidRegistrationSecretOptions + didDocumentOperation?: DidDocumentOperation + didDocument: DidDocument | Partial +} + +export interface DidUpdateResult { + jobId?: string + didState: DidOperationState | DidOperationStateFinished | DidOperationStateFailed + didRegistrationMetadata: DidRegistrationMetadata + didDocumentMetadata: DidResolutionMetadata +} + +export interface DidDeactivateOptions { + did: string + options?: DidRegistrationExtraOptions + secret?: DidRegistrationSecretOptions +} + +export interface DidDeactivateResult { + jobId?: string + didState: DidOperationState | DidOperationStateFinished | DidOperationStateFailed + didRegistrationMetadata: DidRegistrationMetadata + didDocumentMetadata: DidResolutionMetadata +} diff --git a/packages/core/src/modules/ledger/services/IndyLedgerService.ts b/packages/core/src/modules/ledger/services/IndyLedgerService.ts index 99142004ef..1e7916a84a 100644 --- a/packages/core/src/modules/ledger/services/IndyLedgerService.ts +++ b/packages/core/src/modules/ledger/services/IndyLedgerService.ts @@ -85,7 +85,7 @@ export class IndyLedgerService { verkey, alias, role, - pool, + pool: pool.id, }) throw error @@ -99,6 +99,34 @@ export class IndyLedgerService { return didResponse } + public async setEndpointsForDid( + agentContext: AgentContext, + did: string, + endpoints: IndyEndpointAttrib + ): Promise { + const pool = this.indyPoolService.ledgerWritePool + + try { + this.logger.debug(`Set endpoints for did '${did}' on ledger '${pool.id}'`, endpoints) + + const request = await this.indy.buildAttribRequest(did, did, null, { endpoint: endpoints }, null) + + const response = await this.submitWriteRequest(agentContext, pool, request, did) + this.logger.debug(`Successfully set endpoints for did '${did}' on ledger '${pool.id}'`, { + response, + endpoints, + }) + } catch (error) { + this.logger.error(`Error setting endpoints for did '${did}' on ledger '${pool.id}'`, { + error, + did, + endpoints, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error + } + } + public async getEndpointsForDid(agentContext: AgentContext, did: string) { const { pool } = await this.indyPoolService.getPoolForDid(agentContext, did) diff --git a/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts b/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts index f9c09eafe0..8f03f47fa4 100644 --- a/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts +++ b/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts @@ -16,6 +16,7 @@ import { DidExchangeState } from '../../../connections' import { ConnectionRepository } from '../../../connections/repository/ConnectionRepository' import { ConnectionService } from '../../../connections/services/ConnectionService' import { DidRepository } from '../../../dids/repository/DidRepository' +import { DidRegistrarService } from '../../../dids/services/DidRegistrarService' import { RecipientModuleConfig } from '../../RecipientModuleConfig' import { DeliveryRequestMessage, @@ -44,6 +45,9 @@ const EventEmitterMock = EventEmitter as jest.Mock jest.mock('../../../../agent/MessageSender') const MessageSenderMock = MessageSender as jest.Mock +jest.mock('../../../dids/services/DidRegistrarService') +const DidRegistrarServiceMock = DidRegistrarService as jest.Mock + const connectionImageUrl = 'https://example.com/image.png' const mockConnection = getMockConnection({ @@ -59,6 +63,7 @@ describe('MediationRecipientService', () => { let wallet: Wallet let mediationRepository: MediationRepository let didRepository: DidRepository + let didRegistrarService: DidRegistrarService let eventEmitter: EventEmitter let connectionService: ConnectionService let connectionRepository: ConnectionRepository @@ -84,7 +89,14 @@ describe('MediationRecipientService', () => { eventEmitter = new EventEmitterMock() connectionRepository = new ConnectionRepositoryMock() didRepository = new DidRepositoryMock() - connectionService = new ConnectionService(config.logger, connectionRepository, didRepository, eventEmitter) + didRegistrarService = new DidRegistrarServiceMock() + connectionService = new ConnectionService( + config.logger, + connectionRepository, + didRepository, + didRegistrarService, + eventEmitter + ) mediationRepository = new MediationRepositoryMock() messageSender = new MessageSenderMock() diff --git a/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts b/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts index f6edc17ce4..beccb899ef 100644 --- a/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts +++ b/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts @@ -8,8 +8,6 @@ import { JsonTransformer } from '../../../utils/JsonTransformer' import { IndyWallet } from '../../../wallet/IndyWallet' import { WalletError } from '../../../wallet/error' import { DidKey, DidResolverService } from '../../dids' -import { DidRepository } from '../../dids/repository' -import { IndyLedgerService } from '../../ledger/services/IndyLedgerService' import { SignatureSuiteRegistry } from '../SignatureSuiteRegistry' import { W3cCredentialService } from '../W3cCredentialService' import { orArrayToArray } from '../jsonldUtil' @@ -52,9 +50,6 @@ const signingProviderRegistry = new SigningProviderRegistry([new Bls12381g2Signi jest.mock('../../ledger/services/IndyLedgerService') -const IndyLedgerServiceMock = IndyLedgerService as jest.Mock -const DidRepositoryMock = DidRepository as unknown as jest.Mock - jest.mock('../repository/W3cCredentialRepository') const W3cCredentialRepositoryMock = W3cCredentialRepository as jest.Mock @@ -89,11 +84,7 @@ describe('W3cCredentialService', () => { agentConfig, wallet, }) - didResolverService = new DidResolverService( - new IndyLedgerServiceMock(), - new DidRepositoryMock(), - agentConfig.logger - ) + didResolverService = new DidResolverService(agentConfig.logger, []) w3cCredentialRepository = new W3cCredentialRepositoryMock() w3cCredentialService = new W3cCredentialService(w3cCredentialRepository, didResolverService, signatureSuiteRegistry) w3cCredentialService.documentLoaderWithContext = () => customDocumentLoader diff --git a/packages/core/src/utils/__tests__/MultiBaseEncoder.test.ts b/packages/core/src/utils/__tests__/MultibaseEncoder.test.ts similarity index 100% rename from packages/core/src/utils/__tests__/MultiBaseEncoder.test.ts rename to packages/core/src/utils/__tests__/MultibaseEncoder.test.ts diff --git a/packages/core/src/utils/__tests__/MultiHashEncoder.test.ts b/packages/core/src/utils/__tests__/MultihashEncoder.test.ts similarity index 100% rename from packages/core/src/utils/__tests__/MultiHashEncoder.test.ts rename to packages/core/src/utils/__tests__/MultihashEncoder.test.ts diff --git a/packages/core/src/wallet/IndyWallet.ts b/packages/core/src/wallet/IndyWallet.ts index 0e1462cb3e..caced3613d 100644 --- a/packages/core/src/wallet/IndyWallet.ts +++ b/packages/core/src/wallet/IndyWallet.ts @@ -18,15 +18,17 @@ import type { } from './Wallet' import type { default as Indy, WalletStorageConfig } from 'indy-sdk' +import { inject, injectable } from 'tsyringe' + import { AgentDependencies } from '../agent/AgentDependencies' import { InjectionSymbols } from '../constants' +import { KeyType } from '../crypto' import { Key } from '../crypto/Key' -import { KeyType } from '../crypto/KeyType' import { SigningProviderRegistry } from '../crypto/signing-provider/SigningProviderRegistry' import { AriesFrameworkError, IndySdkError, RecordDuplicateError, RecordNotFoundError } from '../error' import { Logger } from '../logger' -import { inject, injectable } from '../plugins' -import { JsonEncoder, TypedArrayEncoder } from '../utils' +import { TypedArrayEncoder } from '../utils' +import { JsonEncoder } from '../utils/JsonEncoder' import { isError } from '../utils/error' import { isIndyError } from '../utils/indyError' diff --git a/packages/core/src/wallet/Wallet.ts b/packages/core/src/wallet/Wallet.ts index 57f128830d..20218f3928 100644 --- a/packages/core/src/wallet/Wallet.ts +++ b/packages/core/src/wallet/Wallet.ts @@ -2,9 +2,9 @@ import type { Key, KeyType } from '../crypto' import type { Disposable } from '../plugins' import type { EncryptedMessage, + PlaintextMessage, WalletConfig, WalletConfigRekey, - PlaintextMessage, WalletExportImportConfig, } from '../types' import type { Buffer } from '../utils/buffer' diff --git a/packages/core/src/wallet/util/assertIndyWallet.ts b/packages/core/src/wallet/util/assertIndyWallet.ts index 6c6ac4a4eb..a26c43f0fe 100644 --- a/packages/core/src/wallet/util/assertIndyWallet.ts +++ b/packages/core/src/wallet/util/assertIndyWallet.ts @@ -5,6 +5,8 @@ import { IndyWallet } from '../IndyWallet' export function assertIndyWallet(wallet: Wallet): asserts wallet is IndyWallet { if (!(wallet instanceof IndyWallet)) { - throw new AriesFrameworkError(`Expected wallet to be instance of IndyWallet, found ${wallet}`) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const walletClassName = (wallet as any).constructor?.name ?? 'unknown' + throw new AriesFrameworkError(`Expected wallet to be instance of IndyWallet, found ${walletClassName}`) } } diff --git a/packages/core/tests/__fixtures__/didKeyz6Mkqbe1.json b/packages/core/tests/__fixtures__/didKeyz6Mkqbe1.json new file mode 100644 index 0000000000..ed4d179434 --- /dev/null +++ b/packages/core/tests/__fixtures__/didKeyz6Mkqbe1.json @@ -0,0 +1,38 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "id": "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG", + "verificationMethod": [ + { + "id": "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG#z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG", + "type": "Ed25519VerificationKey2018", + "controller": "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG", + "publicKeyBase58": "C9Ny7yk9PRKsfW9EJsTJVY12Xn1yke1Jfm24JSy8MLUt" + }, + { + "id": "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG#z6LSmTBUAjnhVwsrkbDQgJgViTH5cjozFjFMaguyvpUq2kcz", + "type": "X25519KeyAgreementKey2019", + "controller": "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG", + "publicKeyBase58": "An1JeRyqQVA7fCqe9fAYPs4bmbGsZ85ChiCJSMqJKNrE" + } + ], + "authentication": [ + "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG#z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG" + ], + "assertionMethod": [ + "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG#z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG" + ], + "capabilityInvocation": [ + "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG#z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG" + ], + "capabilityDelegation": [ + "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG#z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG" + ], + "keyAgreement": [ + "did:key:z6Mkqbe1iDzaixpLmzyvzSR9LdZ2MMHqAXFfMmvz8iw9GZGG#z6LSmTBUAjnhVwsrkbDQgJgViTH5cjozFjFMaguyvpUq2kcz" + ], + "service": [] +} diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 087554b902..e350c12d6d 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@animo-id/react-native-bbs-signatures": "^0.1.0", - "@types/indy-sdk-react-native": "npm:@types/indy-sdk@^1.16.19", + "@types/indy-sdk-react-native": "npm:@types/indy-sdk@^1.16.21", "@types/react-native": "^0.64.10", "indy-sdk-react-native": "^0.2.2", "react": "17.0.1", diff --git a/yarn.lock b/yarn.lock index a36ad3ec64..856c644730 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2408,10 +2408,10 @@ dependencies: "@types/node" "*" -"@types/indy-sdk-react-native@npm:@types/indy-sdk@^1.16.19", "@types/indy-sdk@^1.16.19": - version "1.16.19" - resolved "https://registry.yarnpkg.com/@types/indy-sdk/-/indy-sdk-1.16.19.tgz#f58fc4b5ae67f34cd95c2559fe259b43e0042ead" - integrity sha512-OVgBpLdghrWqPmxEMg76MgIUHo/MvR3xvUeFUJirqdnXGwOs5rQYiZvyECBYeaBEGrSleyAnn5+m4pUfweJyJw== +"@types/indy-sdk-react-native@npm:@types/indy-sdk@^1.16.21", "@types/indy-sdk@^1.16.21": + version "1.16.21" + resolved "https://registry.yarnpkg.com/@types/indy-sdk/-/indy-sdk-1.16.21.tgz#bb6178e2a515115b1bf225fb78506a3017d08aa8" + integrity sha512-SIu1iOa77lkxkGlW09OinFwebe7U5oDYwI70NnPoe9nbDr63i0FozITWEyIdC1BloKvZRXne6nM4i9zy6E3n6g== dependencies: buffer "^6.0.0"