From edf493dd7e707543af5bbdbf6daba2b02c74158d Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 14 Dec 2023 10:20:14 -0300 Subject: [PATCH] feat: did:peer:2 and did:peer:4 support in DID Exchange (#1550) Signed-off-by: Ariel Gentile --- .../core/src/agent/__tests__/Agent.test.ts | 2 +- .../src/modules/connections/ConnectionsApi.ts | 12 +- .../modules/connections/ConnectionsModule.ts | 2 +- .../connections/ConnectionsModuleConfig.ts | 23 + .../connections/DidExchangeProtocol.ts | 407 ++++++++++++------ .../__tests__/InMemoryDidRegistry.ts | 103 +++++ .../__tests__/didexchange-numalgo.e2e.test.ts | 194 +++++++++ .../messages/DidExchangeCompleteMessage.ts | 2 +- .../DidExchangeProblemReportMessage.ts | 2 +- .../messages/DidExchangeRequestMessage.ts | 2 +- .../messages/DidExchangeResponseMessage.ts | 11 +- .../connections/models/HandshakeProtocol.ts | 2 +- .../services/DidCommDocumentService.ts | 4 +- packages/core/src/modules/dids/DidsApi.ts | 3 + .../modules/dids/__tests__/DidsApi.test.ts | 46 +- .../src/modules/dids/domain/DidDocument.ts | 4 +- .../dids/methods/peer/PeerDidRegistrar.ts | 44 +- .../dids/methods/peer/PeerDidResolver.ts | 16 +- .../methods/peer/__tests__/DidPeer.test.ts | 20 +- .../peer/__tests__/PeerDidRegistrar.test.ts | 109 ++++- .../__fixtures__/didPeer4zQmUJdJ.json | 24 ++ .../__fixtures__/didPeer4zQmd8Cp.json | 39 ++ .../peer/__tests__/peerDidNumAlgo4.test.ts | 45 ++ .../peer/createPeerDidDocumentFromServices.ts | 6 +- .../src/modules/dids/methods/peer/didPeer.ts | 20 +- .../dids/methods/peer/peerDidNumAlgo4.ts | 138 ++++++ .../src/modules/dids/repository/DidRecord.ts | 4 + .../modules/dids/repository/DidRepository.ts | 14 +- packages/core/src/modules/oob/OutOfBandApi.ts | 6 +- packages/core/tests/oob.test.ts | 2 +- 30 files changed, 1135 insertions(+), 171 deletions(-) create mode 100644 packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts create mode 100644 packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts create mode 100644 packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json create mode 100644 packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json create mode 100644 packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts create mode 100644 packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts diff --git a/packages/core/src/agent/__tests__/Agent.test.ts b/packages/core/src/agent/__tests__/Agent.test.ts index 65fae042ec..c75d820c0f 100644 --- a/packages/core/src/agent/__tests__/Agent.test.ts +++ b/packages/core/src/agent/__tests__/Agent.test.ts @@ -246,7 +246,7 @@ describe('Agent', () => { 'https://didcomm.org/coordinate-mediation/1.0', 'https://didcomm.org/issue-credential/2.0', 'https://didcomm.org/present-proof/2.0', - 'https://didcomm.org/didexchange/1.0', + 'https://didcomm.org/didexchange/1.1', 'https://didcomm.org/discover-features/1.0', 'https://didcomm.org/discover-features/2.0', 'https://didcomm.org/messagepickup/1.0', diff --git a/packages/core/src/modules/connections/ConnectionsApi.ts b/packages/core/src/modules/connections/ConnectionsApi.ts index 5ca73ae19d..58ffbcf8c8 100644 --- a/packages/core/src/modules/connections/ConnectionsApi.ts +++ b/packages/core/src/modules/connections/ConnectionsApi.ts @@ -91,9 +91,14 @@ export class ConnectionsApi { imageUrl?: string protocol: HandshakeProtocol routing?: Routing + ourDid?: string } ) { - const { protocol, label, alias, imageUrl, autoAcceptConnection } = config + const { protocol, label, alias, imageUrl, autoAcceptConnection, ourDid } = config + + if (ourDid && !config.routing) { + throw new AriesFrameworkError('If an external did is specified, routing configuration must be defined as well') + } const routing = config.routing || @@ -106,8 +111,13 @@ export class ConnectionsApi { alias, routing, autoAcceptConnection, + ourDid, }) } else if (protocol === HandshakeProtocol.Connections) { + if (ourDid) { + throw new AriesFrameworkError('Using an externally defined did for connections protocol is unsupported') + } + result = await this.connectionService.createRequest(this.agentContext, outOfBandRecord, { label, alias, diff --git a/packages/core/src/modules/connections/ConnectionsModule.ts b/packages/core/src/modules/connections/ConnectionsModule.ts index 537f7695a7..25df6cb044 100644 --- a/packages/core/src/modules/connections/ConnectionsModule.ts +++ b/packages/core/src/modules/connections/ConnectionsModule.ts @@ -44,7 +44,7 @@ export class ConnectionsModule implements Module { roles: [ConnectionRole.Invitee, ConnectionRole.Inviter], }), new Protocol({ - id: 'https://didcomm.org/didexchange/1.0', + id: 'https://didcomm.org/didexchange/1.1', roles: [DidExchangeRole.Requester, DidExchangeRole.Responder], }) ) diff --git a/packages/core/src/modules/connections/ConnectionsModuleConfig.ts b/packages/core/src/modules/connections/ConnectionsModuleConfig.ts index e3bd0c0408..86465b293b 100644 --- a/packages/core/src/modules/connections/ConnectionsModuleConfig.ts +++ b/packages/core/src/modules/connections/ConnectionsModuleConfig.ts @@ -1,3 +1,5 @@ +import { PeerDidNumAlgo } from '../dids' + /** * ConnectionsModuleConfigOptions defines the interface for the options of the ConnectionsModuleConfig class. * This can contain optional parameters that have default values in the config class itself. @@ -13,15 +15,26 @@ export interface ConnectionsModuleConfigOptions { * @default false */ autoAcceptConnections?: boolean + + /** + * Peer did num algo to use in requests for DID exchange protocol (RFC 0023). It will be also used by default + * in responses in case that the request does not use a peer did. + * + * @default PeerDidNumAlgo.GenesisDoc + */ + peerNumAlgoForDidExchangeRequests?: PeerDidNumAlgo } export class ConnectionsModuleConfig { #autoAcceptConnections?: boolean + #peerNumAlgoForDidExchangeRequests?: PeerDidNumAlgo + private options: ConnectionsModuleConfigOptions public constructor(options?: ConnectionsModuleConfigOptions) { this.options = options ?? {} this.#autoAcceptConnections = this.options.autoAcceptConnections + this.#peerNumAlgoForDidExchangeRequests = this.options.peerNumAlgoForDidExchangeRequests } /** See {@link ConnectionsModuleConfigOptions.autoAcceptConnections} */ @@ -33,4 +46,14 @@ export class ConnectionsModuleConfig { public set autoAcceptConnections(autoAcceptConnections: boolean) { this.#autoAcceptConnections = autoAcceptConnections } + + /** See {@link ConnectionsModuleConfigOptions.peerNumAlgoForDidExchangeRequests} */ + public get peerNumAlgoForDidExchangeRequests() { + return this.#peerNumAlgoForDidExchangeRequests ?? PeerDidNumAlgo.GenesisDoc + } + + /** See {@link ConnectionsModuleConfigOptions.peerNumAlgoForDidExchangeRequests} */ + public set peerNumAlgoForDidExchangeRequests(peerNumAlgoForDidExchangeRequests: PeerDidNumAlgo) { + this.#peerNumAlgoForDidExchangeRequests = peerNumAlgoForDidExchangeRequests + } } diff --git a/packages/core/src/modules/connections/DidExchangeProtocol.ts b/packages/core/src/modules/connections/DidExchangeProtocol.ts index b9da15c304..1a7a1a5211 100644 --- a/packages/core/src/modules/connections/DidExchangeProtocol.ts +++ b/packages/core/src/modules/connections/DidExchangeProtocol.ts @@ -4,7 +4,6 @@ import type { AgentContext } from '../../agent' import type { InboundMessageContext } from '../../agent/models/InboundMessageContext' import type { ParsedMessageType } from '../../utils/messageType' import type { ResolvedDidCommService } from '../didcomm' -import type { PeerDidCreateOptions } from '../dids' import type { OutOfBandRecord } from '../oob/repository' import { InjectionSymbols } from '../../constants' @@ -16,31 +15,32 @@ import { Attachment, AttachmentData } from '../../decorators/attachment/Attachme import { AriesFrameworkError } from '../../error' import { Logger } from '../../logger' import { inject, injectable } from '../../plugins' -import { isDid } from '../../utils' +import { TypedArrayEncoder, isDid, Buffer } from '../../utils' import { JsonEncoder } from '../../utils/JsonEncoder' import { JsonTransformer } from '../../utils/JsonTransformer' import { base64ToBase64URL } from '../../utils/base64' import { DidDocument, - DidRegistrarService, - DidDocumentRole, createPeerDidDocumentFromServices, DidKey, getNumAlgoFromPeerDid, PeerDidNumAlgo, + DidsApi, + isValidPeerDid, + getAlternativeDidsForPeerDid, } from '../dids' import { getKeyFromVerificationMethod } from '../dids/domain/key-type' import { tryParseDid } from '../dids/domain/parse' import { didKeyToInstanceOfKey } from '../dids/helpers' -import { DidRecord, DidRepository } from '../dids/repository' +import { DidRepository } from '../dids/repository' import { OutOfBandRole } from '../oob/domain/OutOfBandRole' import { OutOfBandState } from '../oob/domain/OutOfBandState' +import { MediationRecipientService } from '../routing/services/MediationRecipientService' +import { ConnectionsModuleConfig } from './ConnectionsModuleConfig' import { DidExchangeStateMachine } from './DidExchangeStateMachine' import { DidExchangeProblemReportError, DidExchangeProblemReportReason } from './errors' -import { DidExchangeCompleteMessage } from './messages/DidExchangeCompleteMessage' -import { DidExchangeRequestMessage } from './messages/DidExchangeRequestMessage' -import { DidExchangeResponseMessage } from './messages/DidExchangeResponseMessage' +import { DidExchangeRequestMessage, DidExchangeResponseMessage, DidExchangeCompleteMessage } from './messages' import { DidExchangeRole, DidExchangeState, HandshakeProtocol } from './models' import { ConnectionService } from './services' @@ -49,27 +49,25 @@ interface DidExchangeRequestParams { alias?: string goal?: string goalCode?: string - routing: Routing + routing?: Routing autoAcceptConnection?: boolean + ourDid?: string } @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 @@ -84,21 +82,63 @@ export class DidExchangeProtocol { outOfBandRecord, params, }) + const config = agentContext.dependencyManager.resolve(ConnectionsModuleConfig) const { outOfBandInvitation } = outOfBandRecord - const { alias, goal, goalCode, routing, autoAcceptConnection } = params - + const { alias, goal, goalCode, routing, autoAcceptConnection, ourDid: did } = params // TODO: We should store only one did that we'll use to send the request message with success. // We take just the first one for now. const [invitationDid] = outOfBandInvitation.invitationDids + // Create message + const label = params.label ?? agentContext.config.label + + let didDocument, mediatorId + + // If our did is specified, make sure we have all key material for it + if (did) { + if (routing) throw new AriesFrameworkError(`'routing' is disallowed when defining 'ourDid'`) + + didDocument = await this.getDidDocumentForCreatedDid(agentContext, did) + const [mediatorRecord] = await agentContext.dependencyManager + .resolve(MediationRecipientService) + .findAllMediatorsByQuery(agentContext, { + recipientKeys: didDocument.recipientKeys.map((key) => key.publicKeyBase58), + }) + mediatorId = mediatorRecord?.id + // Otherwise, create a did:peer based on the provided routing + } else { + if (!routing) throw new AriesFrameworkError(`'routing' must be defined if 'ourDid' is not specified`) + + didDocument = await this.createPeerDidDoc( + agentContext, + this.routingToServices(routing), + config.peerNumAlgoForDidExchangeRequests + ) + mediatorId = routing.mediatorId + } + + const parentThreadId = outOfBandRecord.outOfBandInvitation.id + + const message = new DidExchangeRequestMessage({ label, parentThreadId, did: didDocument.id, goal, goalCode }) + + // Create sign attachment containing didDoc + if (isValidPeerDid(didDocument.id) && getNumAlgoFromPeerDid(didDocument.id) === PeerDidNumAlgo.GenesisDoc) { + const didDocAttach = await this.createSignedAttachment( + agentContext, + didDocument.toJSON(), + didDocument.recipientKeys.map((key) => key.publicKeyBase58) + ) + message.didDoc = didDocAttach + } + const connectionRecord = await this.connectionService.createConnection(agentContext, { protocol: HandshakeProtocol.DidExchange, role: DidExchangeRole.Requester, alias, state: DidExchangeState.InvitationReceived, theirLabel: outOfBandInvitation.label, - mediatorId: routing.mediatorId, + mediatorId, autoAcceptConnection: outOfBandRecord.autoAcceptConnection, outOfBandId: outOfBandRecord.id, invitationDid, @@ -107,21 +147,6 @@ export class DidExchangeProtocol { DidExchangeStateMachine.assertCreateMessageState(DidExchangeRequestMessage.type, connectionRecord) - // Create message - const label = params.label ?? agentContext.config.label - const didDocument = await this.createPeerDidDoc(agentContext, this.routingToServices(routing)) - const parentThreadId = outOfBandRecord.outOfBandInvitation.id - - const message = new DidExchangeRequestMessage({ label, parentThreadId, did: didDocument.id, goal, goalCode }) - - // Create sign attachment containing didDoc - if (getNumAlgoFromPeerDid(didDocument.id) === PeerDidNumAlgo.GenesisDoc) { - const didDocAttach = await this.createSignedAttachment(agentContext, didDocument, [ - routing.recipientKey.publicKeyBase58, - ]) - message.didDoc = didDocAttach - } - connectionRecord.did = didDocument.id connectionRecord.threadId = message.id @@ -141,7 +166,7 @@ export class DidExchangeProtocol { messageContext: InboundMessageContext, outOfBandRecord: OutOfBandRecord ): Promise { - this.logger.debug(`Process message ${DidExchangeRequestMessage.type.messageTypeUri} start`, { + this.logger.debug(`Process message ${messageContext.message.type} start`, { message: messageContext.message, }) @@ -150,7 +175,7 @@ export class DidExchangeProtocol { // TODO check there is no connection record for particular oob record - const { message } = messageContext + const { message, agentContext } = messageContext // Check corresponding invitation ID is the request's ~thread.pthid or pthid is a public did // TODO Maybe we can do it in handler, but that actually does not make sense because we try to find oob by parent thread ID there. @@ -165,49 +190,35 @@ export class DidExchangeProtocol { } // If the responder wishes to continue the exchange, they will persist the received information in their wallet. - if (!isDid(message.did, 'peer')) { - throw new DidExchangeProblemReportError( - `Message contains unsupported did ${message.did}. Supported dids are [did:peer]`, - { - problemCode: DidExchangeProblemReportReason.RequestNotAccepted, - } - ) - } - const numAlgo = getNumAlgoFromPeerDid(message.did) - if (numAlgo !== PeerDidNumAlgo.GenesisDoc) { - throw new DidExchangeProblemReportError( - `Unsupported numalgo ${numAlgo}. Supported numalgos are [${PeerDidNumAlgo.GenesisDoc}]`, - { - problemCode: DidExchangeProblemReportReason.RequestNotAccepted, - } - ) - } - - // 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({ - did: message.did, - role: DidDocumentRole.Received, - // It is important to take the did document from the PeerDid class - // as it will have the id property - 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), - }, - }) - this.logger.debug('Saving DID record', { - id: didRecord.id, - did: didRecord.did, - role: didRecord.role, - tags: didRecord.getTags(), - didDocument: 'omitted...', - }) + // Get DID Document either from message (if it is a supported did:peer) or resolve it externally + const didDocument = await this.resolveDidDocument(agentContext, message) + + if (isValidPeerDid(didDocument.id)) { + const didRecord = await this.didRepository.storeReceivedDid(messageContext.agentContext, { + did: didDocument.id, + // It is important to take the did document from the PeerDid class + // as it will have the id property + didDocument: getNumAlgoFromPeerDid(message.did) === PeerDidNumAlgo.GenesisDoc ? 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), + + // For did:peer, store any alternative dids (like short form did:peer:4), + // it may have in order to relate any message referencing it + alternativeDids: getAlternativeDidsForPeerDid(didDocument.id), + }, + }) - await this.didRepository.save(messageContext.agentContext, didRecord) + this.logger.debug('Saved DID record', { + id: didRecord.id, + did: didRecord.did, + role: didRecord.role, + tags: didRecord.getTags(), + didDocument: 'omitted...', + }) + } const connectionRecord = await this.connectionService.createConnection(messageContext.agentContext, { protocol: HandshakeProtocol.DidExchange, @@ -236,12 +247,18 @@ export class DidExchangeProtocol { this.logger.debug(`Create message ${DidExchangeResponseMessage.type.messageTypeUri} start`, connectionRecord) DidExchangeStateMachine.assertCreateMessageState(DidExchangeResponseMessage.type, connectionRecord) - const { threadId } = connectionRecord + const { threadId, theirDid } = connectionRecord + + const config = agentContext.dependencyManager.resolve(ConnectionsModuleConfig) if (!threadId) { throw new AriesFrameworkError('Missing threadId on connection record.') } + if (!theirDid) { + throw new AriesFrameworkError('Missing theirDid on connection record.') + } + let services: ResolvedDidCommService[] = [] if (routing) { services = this.routingToServices(routing) @@ -255,13 +272,32 @@ export class DidExchangeProtocol { })) } - const didDocument = await this.createPeerDidDoc(agentContext, services) + // Use the same num algo for response as received in request + const numAlgo = isValidPeerDid(theirDid) + ? getNumAlgoFromPeerDid(theirDid) + : config.peerNumAlgoForDidExchangeRequests + + const didDocument = await this.createPeerDidDoc(agentContext, services, numAlgo) const message = new DidExchangeResponseMessage({ did: didDocument.id, threadId }) - if (getNumAlgoFromPeerDid(didDocument.id) === PeerDidNumAlgo.GenesisDoc) { - const didDocAttach = await this.createSignedAttachment( + if (numAlgo === PeerDidNumAlgo.GenesisDoc) { + message.didDoc = await this.createSignedAttachment( agentContext, - didDocument, + didDocument.toJSON(), + Array.from( + new Set( + services + .map((s) => s.recipientKeys) + .reduce((acc, curr) => acc.concat(curr), []) + .map((key) => key.publicKeyBase58) + ) + ) + ) + } else { + // We assume any other case is a resolvable did (e.g. did:peer:2 or did:peer:4) + message.didRotate = await this.createSignedAttachment( + agentContext, + didDocument.id, Array.from( new Set( services @@ -271,7 +307,6 @@ export class DidExchangeProtocol { ) ) ) - message.didDoc = didDocAttach } connectionRecord.did = didDocument.id @@ -292,7 +327,7 @@ export class DidExchangeProtocol { message: messageContext.message, }) - const { connection: connectionRecord, message } = messageContext + const { connection: connectionRecord, message, agentContext } = messageContext if (!connectionRecord) { throw new AriesFrameworkError('No connection record in message context.') @@ -306,51 +341,38 @@ export class DidExchangeProtocol { }) } - if (!isDid(message.did, 'peer')) { - throw new DidExchangeProblemReportError( - `Message contains unsupported did ${message.did}. Supported dids are [did:peer]`, - { - problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, - } - ) - } - const numAlgo = getNumAlgoFromPeerDid(message.did) - if (numAlgo !== PeerDidNumAlgo.GenesisDoc) { - throw new DidExchangeProblemReportError( - `Unsupported numalgo ${numAlgo}. Supported numalgos are [${PeerDidNumAlgo.GenesisDoc}]`, - { - problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, - } - ) - } - - const didDocument = await this.extractDidDocument( - messageContext.agentContext, + // Get DID Document either from message (if it is a did:peer) or resolve it externally + const didDocument = await this.resolveDidDocument( + agentContext, message, outOfBandRecord .getTags() .recipientKeyFingerprints.map((fingerprint) => Key.fromFingerprint(fingerprint).publicKeyBase58) ) - const didRecord = new DidRecord({ - did: message.did, - role: DidDocumentRole.Received, - 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), - }, - }) - this.logger.debug('Saving DID record', { - id: didRecord.id, - did: didRecord.did, - role: didRecord.role, - tags: didRecord.getTags(), - didDocument: 'omitted...', - }) + if (isValidPeerDid(didDocument.id)) { + const didRecord = await this.didRepository.storeReceivedDid(messageContext.agentContext, { + did: didDocument.id, + didDocument: getNumAlgoFromPeerDid(message.did) === PeerDidNumAlgo.GenesisDoc ? 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), + + // For did:peer, store any alternative dids (like short form did:peer:4), + // it may have in order to relate any message referencing it + alternativeDids: getAlternativeDidsForPeerDid(didDocument.id), + }, + }) - await this.didRepository.save(messageContext.agentContext, didRecord) + this.logger.debug('Saved DID record', { + id: didRecord.id, + did: didRecord.did, + role: didRecord.role, + tags: didRecord.getTags(), + didDocument: 'omitted...', + }) + } connectionRecord.theirDid = message.did @@ -433,16 +455,22 @@ export class DidExchangeProtocol { return this.connectionService.updateState(agentContext, connectionRecord, nextState) } - private async createPeerDidDoc(agentContext: AgentContext, services: ResolvedDidCommService[]) { + private async createPeerDidDoc( + agentContext: AgentContext, + services: ResolvedDidCommService[], + numAlgo: PeerDidNumAlgo + ) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + // Create did document without the id property const didDocument = createPeerDidDocumentFromServices(services) - // Register did:peer document. This will generate the id property and save it to a did record - const result = await this.didRegistrarService.create(agentContext, { + + const result = await didsApi.create({ method: 'peer', didDocument, options: { - numAlgo: PeerDidNumAlgo.GenesisDoc, + numAlgo, }, }) @@ -458,11 +486,25 @@ export class DidExchangeProtocol { return result.didState.didDocument } - private async createSignedAttachment(agentContext: AgentContext, didDoc: DidDocument, verkeys: string[]) { - const didDocAttach = new Attachment({ - mimeType: 'application/json', + private async getDidDocumentForCreatedDid(agentContext: AgentContext, did: string) { + const didRecord = await this.didRepository.findCreatedDid(agentContext, did) + + if (!didRecord?.didDocument) { + throw new AriesFrameworkError(`Could not get DidDocument for created did ${did}`) + } + return didRecord.didDocument + } + + private async createSignedAttachment( + agentContext: AgentContext, + data: string | Record, + verkeys: string[] + ) { + const signedAttach = new Attachment({ + mimeType: typeof data === 'string' ? undefined : 'application/json', data: new AttachmentData({ - base64: JsonEncoder.toBase64(didDoc), + base64: + typeof data === 'string' ? TypedArrayEncoder.toBase64URL(Buffer.from(data)) : JsonEncoder.toBase64(data), }), }) @@ -470,7 +512,7 @@ export class DidExchangeProtocol { verkeys.map(async (verkey) => { const key = Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) const kid = new DidKey(key).did - const payload = JsonEncoder.toBuffer(didDoc) + const payload = typeof data === 'string' ? TypedArrayEncoder.fromString(data) : JsonEncoder.toBuffer(data) const jws = await this.jwsService.createJws(agentContext, { payload, @@ -483,11 +525,114 @@ export class DidExchangeProtocol { jwk: getJwkFromKey(key), }, }) - didDocAttach.addJws(jws) + signedAttach.addJws(jws) }) ) - return didDocAttach + return signedAttach + } + + /** + * Resolves a did document from a given `request` or `response` message, verifying its signature or did rotate + * signature in case it is taken from message attachment. + * + * @param message DID request or DID response message + * @param invitationKeys array containing keys from connection invitation that could be used for signing of DID document + * @returns verified DID document content from message attachment + */ + + private async resolveDidDocument( + agentContext: AgentContext, + message: DidExchangeRequestMessage | DidExchangeResponseMessage, + invitationKeysBase58: string[] = [] + ) { + // The only supported case where we expect to receive a did-document attachment is did:peer algo 1 + return isDid(message.did, 'peer') && getNumAlgoFromPeerDid(message.did) === PeerDidNumAlgo.GenesisDoc + ? this.extractAttachedDidDocument(agentContext, message, invitationKeysBase58) + : this.extractResolvableDidDocument(agentContext, message, invitationKeysBase58) + } + + /** + * Extracts DID document from message (resolving it externally if required) and verifies did-rotate attachment signature + * if applicable + */ + private async extractResolvableDidDocument( + agentContext: AgentContext, + message: DidExchangeRequestMessage | DidExchangeResponseMessage, + invitationKeysBase58?: string[] + ) { + // Validate did-rotate attachment in case of DID Exchange response + if (message instanceof DidExchangeResponseMessage) { + const didRotateAttachment = message.didRotate + + if (!didRotateAttachment) { + throw new DidExchangeProblemReportError('DID Rotate attachment is missing.', { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + }) + } + + const jws = didRotateAttachment.data.jws + + if (!jws) { + throw new DidExchangeProblemReportError('DID Rotate signature is missing.', { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + }) + } + + if (!didRotateAttachment.data.base64) { + throw new AriesFrameworkError('DID Rotate attachment is missing base64 property for signed did.') + } + + // JWS payload must be base64url encoded + const base64UrlPayload = base64ToBase64URL(didRotateAttachment.data.base64) + const signedDid = TypedArrayEncoder.fromBase64(base64UrlPayload).toString() + + if (signedDid !== message.did) { + throw new AriesFrameworkError( + `DID Rotate attachment's did ${message.did} does not correspond to message did ${message.did}` + ) + } + + const { isValid, signerKeys } = await this.jwsService.verifyJws(agentContext, { + jws: { + ...jws, + payload: base64UrlPayload, + }, + jwkResolver: ({ jws: { header } }) => { + if (typeof header.kid !== 'string' || !isDid(header.kid, 'key')) { + throw new AriesFrameworkError('JWS header kid must be a did:key DID.') + } + + const didKey = DidKey.fromDid(header.kid) + return getJwkFromKey(didKey.key) + }, + }) + + if (!isValid || !signerKeys.every((key) => invitationKeysBase58?.includes(key.publicKeyBase58))) { + throw new DidExchangeProblemReportError( + `DID Rotate signature is invalid. isValid: ${isValid} signerKeys: ${JSON.stringify( + signerKeys + )} invitationKeys:${JSON.stringify(invitationKeysBase58)}`, + { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + } + ) + } + } + + // Now resolve the document related to the did (which can be either a public did or an inline did) + try { + return await agentContext.dependencyManager.resolve(DidsApi).resolveDidDocument(message.did) + } catch (error) { + const problemCode = + message instanceof DidExchangeRequestMessage + ? DidExchangeProblemReportReason.RequestNotAccepted + : DidExchangeProblemReportReason.ResponseNotAccepted + + throw new DidExchangeProblemReportError(error, { + problemCode, + }) + } } /** @@ -497,7 +642,7 @@ export class DidExchangeProtocol { * @param invitationKeys array containing keys from connection invitation that could be used for signing of DID document * @returns verified DID document content from message attachment */ - private async extractDidDocument( + private async extractAttachedDidDocument( agentContext: AgentContext, message: DidExchangeRequestMessage | DidExchangeResponseMessage, invitationKeysBase58: string[] = [] @@ -526,7 +671,6 @@ export class DidExchangeProtocol { // JWS payload must be base64url encoded const base64UrlPayload = base64ToBase64URL(didDocumentAttachment.data.base64) - const json = JsonEncoder.fromBase64(didDocumentAttachment.data.base64) const { isValid, signerKeys } = await this.jwsService.verifyJws(agentContext, { jws: { @@ -543,6 +687,7 @@ export class DidExchangeProtocol { }, }) + const json = JsonEncoder.fromBase64(didDocumentAttachment.data.base64) const didDocument = JsonTransformer.fromJSON(json, DidDocument) const didDocumentKeysBase58 = didDocument.authentication ?.map((authentication) => { diff --git a/packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts b/packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts new file mode 100644 index 0000000000..dc9e9e8107 --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts @@ -0,0 +1,103 @@ +import type { AgentContext } from '../../../agent' +import type { + DidRegistrar, + DidResolver, + DidDocument, + DidCreateOptions, + DidCreateResult, + DidUpdateResult, + DidDeactivateResult, + DidResolutionResult, +} from '../../dids' + +import { DidRecord, DidDocumentRole, DidRepository } from '../../dids' + +export class InMemoryDidRegistry implements DidRegistrar, DidResolver { + public readonly supportedMethods = ['inmemory'] + + private dids: Record = {} + + public async create(agentContext: AgentContext, options: DidCreateOptions): Promise { + const { did, didDocument } = options + + if (!did || !didDocument) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'InMemoryDidRegistrar requires to specify both did and didDocument', + }, + } + } + + this.dids[did] = didDocument + + // Save the did so we know we created it and can use it for didcomm + const didRecord = new DidRecord({ + did: didDocument.id, + role: DidDocumentRole.Created, + 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), + }, + }) + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + await didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: didDocument.id, + didDocument, + }, + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:inmemory not implemented yet`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:inmemory not implemented yet`, + }, + } + } + + public async resolve(agentContext: AgentContext, did: string): Promise { + const didDocument = this.dids[did] + + if (!didDocument) { + return { + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}'`, + }, + } + } + + return { + didDocument, + didDocumentMetadata: {}, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } +} diff --git a/packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts b/packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts new file mode 100644 index 0000000000..0c6c1c657b --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts @@ -0,0 +1,194 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { ConnectionStateChangedEvent } from '../ConnectionEvents' + +import { firstValueFrom } from 'rxjs' +import { filter, first, map, timeout } from 'rxjs/operators' + +import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' +import { setupSubjectTransports } from '../../../../tests' +import { getAgentOptions } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { uuid } from '../../../utils/uuid' +import { DidsModule, PeerDidNumAlgo, createPeerDidDocumentFromServices } from '../../dids' +import { ConnectionEventTypes } from '../ConnectionEvents' +import { ConnectionsModule } from '../ConnectionsModule' +import { DidExchangeState } from '../models' + +import { InMemoryDidRegistry } from './InMemoryDidRegistry' + +function waitForRequest(agent: Agent, theirLabel: string) { + return firstValueFrom( + agent.events.observable(ConnectionEventTypes.ConnectionStateChanged).pipe( + map((event) => event.payload.connectionRecord), + // Wait for request received + filter( + (connectionRecord) => + connectionRecord.state === DidExchangeState.RequestReceived && connectionRecord.theirLabel === theirLabel + ), + first(), + timeout(5000) + ) + ) +} + +function waitForResponse(agent: Agent, connectionId: string) { + return firstValueFrom( + agent.events.observable(ConnectionEventTypes.ConnectionStateChanged).pipe( + // Wait for response received + map((event) => event.payload.connectionRecord), + filter( + (connectionRecord) => + connectionRecord.state === DidExchangeState.ResponseReceived && connectionRecord.id === connectionId + ), + first(), + timeout(5000) + ) + ) +} + +describe('Did Exchange numalgo settings', () => { + test('Connect using default setting (numalgo 1)', async () => { + await didExchangeNumAlgoBaseTest({}) + }) + + test('Connect using default setting for requester and numalgo 2 for responder', async () => { + await didExchangeNumAlgoBaseTest({ responderNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc }) + }) + + test('Connect using numalgo 2 for requester and default setting for responder', async () => { + await didExchangeNumAlgoBaseTest({ requesterNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc }) + }) + + test('Connect using numalgo 2 for both requester and responder', async () => { + await didExchangeNumAlgoBaseTest({ + requesterNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, + responderNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, + }) + }) + + test('Connect using default setting for requester and numalgo 4 for responder', async () => { + await didExchangeNumAlgoBaseTest({ responderNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm }) + }) + + test('Connect using numalgo 4 for requester and default setting for responder', async () => { + await didExchangeNumAlgoBaseTest({ requesterNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm }) + }) + + test.only('Connect using numalgo 4 for both requester and responder', async () => { + await didExchangeNumAlgoBaseTest({ + requesterNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm, + responderNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm, + }) + }) + + test('Connect using an externally defined did for the requested', async () => { + await didExchangeNumAlgoBaseTest({ + createExternalDidForRequester: true, + }) + }) +}) + +async function didExchangeNumAlgoBaseTest(options: { + requesterNumAlgoSetting?: PeerDidNumAlgo + responderNumAlgoSetting?: PeerDidNumAlgo + createExternalDidForRequester?: boolean +}) { + // Make a common in-memory did registry for both agents + const didRegistry = new InMemoryDidRegistry() + + const aliceAgentOptions = getAgentOptions( + 'DID Exchange numalgo settings Alice', + { + label: 'alice', + endpoints: ['rxjs:alice'], + }, + { + ...getIndySdkModules(), + connections: new ConnectionsModule({ + autoAcceptConnections: false, + peerNumAlgoForDidExchangeRequests: options.requesterNumAlgoSetting, + }), + dids: new DidsModule({ registrars: [didRegistry], resolvers: [didRegistry] }), + } + ) + const faberAgentOptions = getAgentOptions( + 'DID Exchange numalgo settings Alice', + { + endpoints: ['rxjs:faber'], + }, + { + ...getIndySdkModules(), + connections: new ConnectionsModule({ + autoAcceptConnections: false, + peerNumAlgoForDidExchangeRequests: options.responderNumAlgoSetting, + }), + dids: new DidsModule({ registrars: [didRegistry], resolvers: [didRegistry] }), + } + ) + + const aliceAgent = new Agent(aliceAgentOptions) + const faberAgent = new Agent(faberAgentOptions) + + setupSubjectTransports([aliceAgent, faberAgent]) + await aliceAgent.initialize() + await faberAgent.initialize() + + const faberOutOfBandRecord = await faberAgent.oob.createInvitation({ + autoAcceptConnection: false, + multiUseInvitation: false, + }) + + const waitForAliceRequest = waitForRequest(faberAgent, 'alice') + + let ourDid, routing + if (options.createExternalDidForRequester) { + // Create did externally + const routing = await aliceAgent.mediationRecipient.getRouting({}) + const ourDid = `did:inmemory:${uuid()}` + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [routing.recipientKey], + routingKeys: routing.routingKeys, + serviceEndpoint: routing.endpoints[0], + }, + ]) + didDocument.id = ourDid + + await aliceAgent.dids.create({ + method: 'inmemory', + did: ourDid, + didDocument, + }) + } + + let { connectionRecord: aliceConnectionRecord } = await aliceAgent.oob.receiveInvitation( + faberOutOfBandRecord.outOfBandInvitation, + { + autoAcceptInvitation: true, + autoAcceptConnection: false, + routing, + ourDid, + } + ) + + let faberAliceConnectionRecord = await waitForAliceRequest + + const waitForAliceResponse = waitForResponse(aliceAgent, aliceConnectionRecord!.id) + + await faberAgent.connections.acceptRequest(faberAliceConnectionRecord.id) + + aliceConnectionRecord = await waitForAliceResponse + await aliceAgent.connections.acceptResponse(aliceConnectionRecord!.id) + + aliceConnectionRecord = await aliceAgent.connections.returnWhenIsConnected(aliceConnectionRecord!.id) + faberAliceConnectionRecord = await faberAgent.connections.returnWhenIsConnected(faberAliceConnectionRecord!.id) + + expect(aliceConnectionRecord).toBeConnectedWith(faberAliceConnectionRecord) + + await aliceAgent.wallet.delete() + await aliceAgent.shutdown() + + await faberAgent.wallet.delete() + await faberAgent.shutdown() +} diff --git a/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts index 3c142e76e8..754f049a71 100644 --- a/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts +++ b/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts @@ -26,5 +26,5 @@ export class DidExchangeCompleteMessage extends AgentMessage { @IsValidMessageType(DidExchangeCompleteMessage.type) public readonly type = DidExchangeCompleteMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/complete') + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/complete') } diff --git a/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts index ec8baf9880..3f948aa768 100644 --- a/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts +++ b/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts @@ -15,5 +15,5 @@ export class DidExchangeProblemReportMessage extends ProblemReportMessage { @IsValidMessageType(DidExchangeProblemReportMessage.type) public readonly type = DidExchangeProblemReportMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/problem-report') + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/problem-report') } diff --git a/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts index c22729a272..353ac079a2 100644 --- a/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts +++ b/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts @@ -42,7 +42,7 @@ export class DidExchangeRequestMessage extends AgentMessage { @IsValidMessageType(DidExchangeRequestMessage.type) public readonly type = DidExchangeRequestMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/request') + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/request') @IsString() public readonly label?: string diff --git a/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts index d0de9b6ec8..fe62a6393a 100644 --- a/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts +++ b/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts @@ -1,5 +1,5 @@ import { Type, Expose } from 'class-transformer' -import { IsString, ValidateNested } from 'class-validator' +import { IsOptional, IsString, ValidateNested } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' import { Attachment } from '../../../decorators/attachment/Attachment' @@ -36,13 +36,20 @@ export class DidExchangeResponseMessage extends AgentMessage { @IsValidMessageType(DidExchangeResponseMessage.type) public readonly type = DidExchangeResponseMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/response') + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/response') @IsString() public readonly did!: string @Expose({ name: 'did_doc~attach' }) + @IsOptional() @Type(() => Attachment) @ValidateNested() public didDoc?: Attachment + + @Expose({ name: 'did_rotate~attach' }) + @IsOptional() + @Type(() => Attachment) + @ValidateNested() + public didRotate?: Attachment } diff --git a/packages/core/src/modules/connections/models/HandshakeProtocol.ts b/packages/core/src/modules/connections/models/HandshakeProtocol.ts index a433bd87f5..cee69c7fcd 100644 --- a/packages/core/src/modules/connections/models/HandshakeProtocol.ts +++ b/packages/core/src/modules/connections/models/HandshakeProtocol.ts @@ -1,4 +1,4 @@ export enum HandshakeProtocol { Connections = 'https://didcomm.org/connections/1.0', - DidExchange = 'https://didcomm.org/didexchange/1.0', + DidExchange = 'https://didcomm.org/didexchange/1.1', } diff --git a/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts index ca1b1a88d4..e51a55c5ef 100644 --- a/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts +++ b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts @@ -29,7 +29,7 @@ export class DidCommDocumentService { // FIXME: we currently retrieve did documents for all didcomm services in the did document, and we don't have caching // yet so this will re-trigger ledger resolves for each one. Should we only resolve the first service, then the second service, etc...? for (const didCommService of didCommServices) { - if (didCommService instanceof IndyAgentService) { + if (didCommService.type === IndyAgentService.type) { // IndyAgentService (DidComm v0) has keys encoded as raw publicKeyBase58 (verkeys) resolvedServices.push({ id: didCommService.id, @@ -37,7 +37,7 @@ export class DidCommDocumentService { routingKeys: didCommService.routingKeys?.map(verkeyToInstanceOfKey) || [], serviceEndpoint: didCommService.serviceEndpoint, }) - } else if (didCommService instanceof DidCommV1Service) { + } else if (didCommService.type === DidCommV1Service.type) { // Resolve dids to DIDDocs to retrieve routingKeys const routingKeys = [] for (const routingKey of didCommService.routingKeys ?? []) { diff --git a/packages/core/src/modules/dids/DidsApi.ts b/packages/core/src/modules/dids/DidsApi.ts index e20ef573e2..4f0cf294bf 100644 --- a/packages/core/src/modules/dids/DidsApi.ts +++ b/packages/core/src/modules/dids/DidsApi.ts @@ -15,6 +15,7 @@ import { injectable } from '../../plugins' import { WalletKeyExistsError } from '../../wallet/error' import { DidsModuleConfig } from './DidsModuleConfig' +import { getAlternativeDidsForPeerDid, isValidPeerDid } from './methods' import { DidRepository } from './repository' import { DidRegistrarService, DidResolverService } from './services' @@ -157,6 +158,7 @@ export class DidsApi { existingDidRecord.didDocument = didDocument existingDidRecord.setTags({ recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(did) : undefined, }) await this.didRepository.update(this.agentContext, existingDidRecord) @@ -169,6 +171,7 @@ export class DidsApi { didDocument, tags: { recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(did) : undefined, }, }) } diff --git a/packages/core/src/modules/dids/__tests__/DidsApi.test.ts b/packages/core/src/modules/dids/__tests__/DidsApi.test.ts index 41993e9d43..8485899723 100644 --- a/packages/core/src/modules/dids/__tests__/DidsApi.test.ts +++ b/packages/core/src/modules/dids/__tests__/DidsApi.test.ts @@ -2,8 +2,16 @@ import { IndySdkModule } from '../../../../../indy-sdk/src' import { indySdk } from '../../../../tests' import { getAgentOptions } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' +import { isLongFormDidPeer4, isShortFormDidPeer4 } from '../methods/peer/peerDidNumAlgo4' -import { DidDocument, DidDocumentService, KeyType, TypedArrayEncoder } from '@aries-framework/core' +import { + DidDocument, + DidDocumentService, + KeyType, + PeerDidNumAlgo, + TypedArrayEncoder, + createPeerDidDocumentFromServices, +} from '@aries-framework/core' const agentOptions = getAgentOptions( 'DidsApi', @@ -227,4 +235,40 @@ describe('DidsApi', () => { ], }) }) + + test('create and resolve did:peer:4 in short and long form', async () => { + const routing = await agent.mediationRecipient.getRouting({}) + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [routing.recipientKey], + routingKeys: routing.routingKeys, + serviceEndpoint: routing.endpoints[0], + }, + ]) + + const result = await agent.dids.create({ + method: 'peer', + didDocument, + options: { + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + }, + }) + + const longFormDid = result.didState.did + const shortFormDid = result.didState.didDocument?.alsoKnownAs + ? result.didState.didDocument?.alsoKnownAs[0] + : undefined + + if (!longFormDid) fail('Long form did not defined') + if (!shortFormDid) fail('Short form did not defined') + + expect(isLongFormDidPeer4(longFormDid)).toBeTruthy() + expect(isShortFormDidPeer4(shortFormDid)).toBeTruthy() + + const didDocumentFromLongFormDid = await agent.dids.resolveDidDocument(longFormDid) + const didDocumentFromShortFormDid = await agent.dids.resolveDidDocument(shortFormDid) + + expect(didDocumentFromLongFormDid).toEqual(didDocumentFromShortFormDid) + }) }) diff --git a/packages/core/src/modules/dids/domain/DidDocument.ts b/packages/core/src/modules/dids/domain/DidDocument.ts index 08963613af..e67dbd6a1b 100644 --- a/packages/core/src/modules/dids/domain/DidDocument.ts +++ b/packages/core/src/modules/dids/domain/DidDocument.ts @@ -186,12 +186,12 @@ export class DidDocument { let recipientKeys: Key[] = [] for (const service of this.didCommServices) { - if (service instanceof IndyAgentService) { + if (service.type === IndyAgentService.type) { recipientKeys = [ ...recipientKeys, ...service.recipientKeys.map((publicKeyBase58) => Key.fromPublicKeyBase58(publicKeyBase58, KeyType.Ed25519)), ] - } else if (service instanceof DidCommV1Service) { + } else if (service.type === DidCommV1Service.type) { recipientKeys = [ ...recipientKeys, ...service.recipientKeys.map((recipientKey) => keyReferenceToKey(this, recipientKey)), diff --git a/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts b/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts index fcae22119e..b1e9172dd9 100644 --- a/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts +++ b/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts @@ -9,19 +9,26 @@ import { DidDocument } from '../../domain' import { DidDocumentRole } from '../../domain/DidDocumentRole' import { DidRepository, DidRecord } from '../../repository' -import { PeerDidNumAlgo } from './didPeer' +import { PeerDidNumAlgo, getAlternativeDidsForPeerDid } from './didPeer' import { keyToNumAlgo0DidDocument } from './peerDidNumAlgo0' import { didDocumentJsonToNumAlgo1Did } from './peerDidNumAlgo1' import { didDocumentToNumAlgo2Did } from './peerDidNumAlgo2' +import { didDocumentToNumAlgo4Did } from './peerDidNumAlgo4' export class PeerDidRegistrar implements DidRegistrar { public readonly supportedMethods = ['peer'] public async create( agentContext: AgentContext, - options: PeerDidNumAlgo0CreateOptions | PeerDidNumAlgo1CreateOptions | PeerDidNumAlgo2CreateOptions + options: + | PeerDidNumAlgo0CreateOptions + | PeerDidNumAlgo1CreateOptions + | PeerDidNumAlgo2CreateOptions + | PeerDidNumAlgo4CreateOptions ): Promise { const didRepository = agentContext.dependencyManager.resolve(DidRepository) + + let did: string let didDocument: DidDocument try { @@ -50,16 +57,27 @@ export class PeerDidRegistrar implements DidRegistrar { // TODO: validate did:peer document didDocument = keyToNumAlgo0DidDocument(key) + did = didDocument.id } else if (isPeerDidNumAlgo1CreateOptions(options)) { const didDocumentJson = options.didDocument.toJSON() - const did = didDocumentJsonToNumAlgo1Did(didDocumentJson) + did = didDocumentJsonToNumAlgo1Did(didDocumentJson) didDocument = JsonTransformer.fromJSON({ ...didDocumentJson, id: did }, DidDocument) } else if (isPeerDidNumAlgo2CreateOptions(options)) { const didDocumentJson = options.didDocument.toJSON() - const did = didDocumentToNumAlgo2Did(options.didDocument) + did = didDocumentToNumAlgo2Did(options.didDocument) didDocument = JsonTransformer.fromJSON({ ...didDocumentJson, id: did }, DidDocument) + } else if (isPeerDidNumAlgo4CreateOptions(options)) { + const didDocumentJson = options.didDocument.toJSON() + + const { longFormDid, shortFormDid } = didDocumentToNumAlgo4Did(options.didDocument) + + did = longFormDid + didDocument = JsonTransformer.fromJSON( + { ...didDocumentJson, id: longFormDid, alsoKnownAs: [shortFormDid] }, + DidDocument + ) } else { return { didDocumentMetadata: {}, @@ -73,13 +91,14 @@ export class PeerDidRegistrar implements DidRegistrar { // Save the did so we know we created it and can use it for didcomm const didRecord = new DidRecord({ - did: didDocument.id, + did, 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), + alternativeDids: getAlternativeDidsForPeerDid(did), }, }) await didRepository.save(agentContext, didRecord) @@ -149,10 +168,15 @@ function isPeerDidNumAlgo2CreateOptions(options: PeerDidCreateOptions): options return options.options.numAlgo === PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc } +function isPeerDidNumAlgo4CreateOptions(options: PeerDidCreateOptions): options is PeerDidNumAlgo4CreateOptions { + return options.options.numAlgo === PeerDidNumAlgo.ShortFormAndLongForm +} + export type PeerDidCreateOptions = | PeerDidNumAlgo0CreateOptions | PeerDidNumAlgo1CreateOptions | PeerDidNumAlgo2CreateOptions + | PeerDidNumAlgo4CreateOptions export interface PeerDidNumAlgo0CreateOptions extends DidCreateOptions { method: 'peer' @@ -188,6 +212,16 @@ export interface PeerDidNumAlgo2CreateOptions extends DidCreateOptions { secret?: undefined } +export interface PeerDidNumAlgo4CreateOptions extends DidCreateOptions { + method: 'peer' + did?: never + didDocument: DidDocument + options: { + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm + } + 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 fa3f2ed9d2..37b0968820 100644 --- a/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts +++ b/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts @@ -9,6 +9,7 @@ import { DidRepository } from '../../repository' import { getNumAlgoFromPeerDid, isValidPeerDid, PeerDidNumAlgo } from './didPeer' import { didToNumAlgo0DidDocument } from './peerDidNumAlgo0' import { didToNumAlgo2DidDocument } from './peerDidNumAlgo2' +import { didToNumAlgo4DidDocument, isShortFormDidPeer4 } from './peerDidNumAlgo4' export class PeerDidResolver implements DidResolver { public readonly supportedMethods = ['peer'] @@ -48,9 +49,22 @@ export class PeerDidResolver implements DidResolver { didDocument = didDocumentRecord.didDocument } // For Method 2, generate from did - else { + else if (numAlgo === PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc) { didDocument = didToNumAlgo2DidDocument(did) } + // For Method 4, if short form is received, attempt to get the didDocument from stored record + else { + if (isShortFormDidPeer4(did)) { + const [didRecord] = await didRepository.findAllByDid(agentContext, did) + + if (!didRecord) { + throw new AriesFrameworkError(`No did record found for peer did ${did}.`) + } + didDocument = didToNumAlgo4DidDocument(didRecord.did) + } else { + didDocument = didToNumAlgo4DidDocument(did) + } + } return { didDocument, 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 index 99716995f5..160860bf03 100644 --- a/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts +++ b/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts @@ -9,10 +9,21 @@ describe('didPeer', () => { 'did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' ) ).toBe(true) + expect( + isValidPeerDid( + 'did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn:z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + ).toBe(true) + expect(isValidPeerDid('did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn')).toBe(true) + expect( + isValidPeerDid( + 'did:peer:4z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + ).toBe(false) expect( isValidPeerDid( - 'did:peer:4.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' + 'did:peer:5.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' ) ).toBe(false) }) @@ -35,6 +46,13 @@ describe('didPeer', () => { 'did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' ) ).toBe(PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc) + + // NumAlgo 4 + expect( + getNumAlgoFromPeerDid( + 'did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn:z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + ).toBe(PeerDidNumAlgo.ShortFormAndLongForm) }) }) }) 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 index 2daa05729b..2c1076f397 100644 --- a/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts +++ b/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts @@ -318,17 +318,110 @@ describe('DidRegistrar', () => { }) }) - 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 - { + describe('did:peer:4', () => { + const key = Key.fromFingerprint('z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz') + const verificationMethod = getEd25519VerificationKey2018({ + key, + 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:4 document from a did document', async () => { + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + didDocument: didDocument, + options: { + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + }, + }) + + const longFormDid = + 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e' + const shortFormDid = 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4' + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: longFormDid, + didDocument: { + '@context': ['https://w3id.org/did/v1'], + id: longFormDid, + alsoKnownAs: [shortFormDid], + 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 longFormDid = + 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e' + const shortFormDid = 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4' + await peerDidRegistrar.create(agentContext, { method: 'peer', + didDocument, options: { - numAlgo: 4, + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + did: longFormDid, + role: DidDocumentRole.Created, + _tags: { + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + alternativeDids: [shortFormDid], }, - } - ) + didDocument: undefined, + }) + }) + }) + + it('should return an error state if an unsupported numAlgo is provided', async () => { + // @ts-expect-error - this is not a valid numAlgo + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + options: { + numAlgo: 5, + }, + }) expect(JsonTransformer.toJSON(result)).toMatchObject({ didDocumentMetadata: {}, diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json new file mode 100644 index 0000000000..79a8c2a0d1 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json @@ -0,0 +1,24 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "verificationMethod": [ + { + "id": "#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE", + "controller": "did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e" + } + ], + "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"], + "id": "did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e", + "alsoKnownAs": ["did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4"] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json new file mode 100644 index 0000000000..fc0529d74e --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json @@ -0,0 +1,39 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/x25519-2020/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "verificationMethod": [ + { + "id": "#6LSqPZfn", + "type": "X25519KeyAgreementKey2020", + "publicKeyMultibase": "z6LSqPZfn9krvgXma2icTMKf2uVcYhKXsudCmPoUzqGYW24U", + "controller": "did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd:z2M1k7h4psgp4CmJcnQn2Ljp7Pz7ktsd7oBhMU3dWY5s4fhFNj17qcRTQ427C7QHNT6cQ7T3XfRh35Q2GhaNFZmWHVFq4vL7F8nm36PA9Y96DvdrUiRUaiCuXnBFrn1o7mxFZAx14JL4t8vUWpuDPwQuddVo1T8myRiVH7wdxuoYbsva5x6idEpCQydJdFjiHGCpNc2UtjzPQ8awSXkctGCnBmgkhrj5gto3D4i3EREXYq4Z8r2cWGBr2UzbSmnxW2BuYddFo9Yfm6mKjtJyLpF74ytqrF5xtf84MnGFg1hMBmh1xVx1JwjZ2BeMJs7mNS8DTZhKC7KH38EgqDtUZzfjhpjmmUfkXg2KFEA3EGbbVm1DPqQXayPYKAsYPS9AyKkcQ3fzWafLPP93UfNhtUPL8JW5pMcSV3P8v6j3vPXqnnGknNyBprD6YGUVtgLiAqDBDUF3LSxFQJCVYYtghMTv8WuSw9h1a1SRFrDQLGHE4UrkgoRvwaGWr64aM87T1eVGkP5Dt4L1AbboeK2ceLArPScrdYGTpi3BpTkLwZCdjdiFSfTy9okL1YNRARqUf2wm8DvkVGUU7u5nQA3ZMaXWJAewk6k1YUxKd7LvofGUK4YEDtoxN5vb6r1Q2godrGqaPkjfL3RoYPpDYymf9XhcgG8Kx3DZaA6cyTs24t45KxYAfeCw4wqUpCH9HbpD78TbEUr9PPAsJgXBvBj2VVsxnr7FKbK4KykGcg1W8M1JPz21Z4Y72LWgGQCmixovrkHktcTX1uNHjAvKBqVD5C7XmVfHgXCHj7djCh3vzLNuVLtEED8J1hhqsB1oCBGiuh3xXr7fZ9wUjJCQ1HYHqxLJKdYKtoCiPmgKM7etVftXkmTFETZmpM19aRyih3bao76LdpQtbw636r7a3qt8v4WfxsXJetSL8c7t24SqQBcAY89FBsbEnFNrQCMK3JEseKHVaU388ctvRD45uQfe5GndFxthj4iSDomk4uRFd1uRbywoP1tRuabHTDX42UxPjz" + }, + { + "id": "#6MkrCD1c", + "type": "Ed25519VerificationKey2020", + "publicKeyMultibase": "z6MkrCD1csqtgdj8sjrsu8jxcbeyP6m7LiK87NzhfWqio5yr", + "controller": "did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd:z2M1k7h4psgp4CmJcnQn2Ljp7Pz7ktsd7oBhMU3dWY5s4fhFNj17qcRTQ427C7QHNT6cQ7T3XfRh35Q2GhaNFZmWHVFq4vL7F8nm36PA9Y96DvdrUiRUaiCuXnBFrn1o7mxFZAx14JL4t8vUWpuDPwQuddVo1T8myRiVH7wdxuoYbsva5x6idEpCQydJdFjiHGCpNc2UtjzPQ8awSXkctGCnBmgkhrj5gto3D4i3EREXYq4Z8r2cWGBr2UzbSmnxW2BuYddFo9Yfm6mKjtJyLpF74ytqrF5xtf84MnGFg1hMBmh1xVx1JwjZ2BeMJs7mNS8DTZhKC7KH38EgqDtUZzfjhpjmmUfkXg2KFEA3EGbbVm1DPqQXayPYKAsYPS9AyKkcQ3fzWafLPP93UfNhtUPL8JW5pMcSV3P8v6j3vPXqnnGknNyBprD6YGUVtgLiAqDBDUF3LSxFQJCVYYtghMTv8WuSw9h1a1SRFrDQLGHE4UrkgoRvwaGWr64aM87T1eVGkP5Dt4L1AbboeK2ceLArPScrdYGTpi3BpTkLwZCdjdiFSfTy9okL1YNRARqUf2wm8DvkVGUU7u5nQA3ZMaXWJAewk6k1YUxKd7LvofGUK4YEDtoxN5vb6r1Q2godrGqaPkjfL3RoYPpDYymf9XhcgG8Kx3DZaA6cyTs24t45KxYAfeCw4wqUpCH9HbpD78TbEUr9PPAsJgXBvBj2VVsxnr7FKbK4KykGcg1W8M1JPz21Z4Y72LWgGQCmixovrkHktcTX1uNHjAvKBqVD5C7XmVfHgXCHj7djCh3vzLNuVLtEED8J1hhqsB1oCBGiuh3xXr7fZ9wUjJCQ1HYHqxLJKdYKtoCiPmgKM7etVftXkmTFETZmpM19aRyih3bao76LdpQtbw636r7a3qt8v4WfxsXJetSL8c7t24SqQBcAY89FBsbEnFNrQCMK3JEseKHVaU388ctvRD45uQfe5GndFxthj4iSDomk4uRFd1uRbywoP1tRuabHTDX42UxPjz" + } + ], + "service": [ + { + "id": "#didcommmessaging-0", + "type": "DIDCommMessaging", + "serviceEndpoint": { + "uri": "didcomm:transport/queue", + "accept": ["didcomm/v2"], + "routingKeys": [] + } + } + ], + "authentication": ["#6MkrCD1c"], + "keyAgreement": ["#6LSqPZfn"], + "assertionMethod": ["#6MkrCD1c"], + "capabilityDelegation": ["#6MkrCD1c"], + "capabilityInvocation": ["#6MkrCD1c"], + "alsoKnownAs": ["did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd"], + "id": "did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd:z2M1k7h4psgp4CmJcnQn2Ljp7Pz7ktsd7oBhMU3dWY5s4fhFNj17qcRTQ427C7QHNT6cQ7T3XfRh35Q2GhaNFZmWHVFq4vL7F8nm36PA9Y96DvdrUiRUaiCuXnBFrn1o7mxFZAx14JL4t8vUWpuDPwQuddVo1T8myRiVH7wdxuoYbsva5x6idEpCQydJdFjiHGCpNc2UtjzPQ8awSXkctGCnBmgkhrj5gto3D4i3EREXYq4Z8r2cWGBr2UzbSmnxW2BuYddFo9Yfm6mKjtJyLpF74ytqrF5xtf84MnGFg1hMBmh1xVx1JwjZ2BeMJs7mNS8DTZhKC7KH38EgqDtUZzfjhpjmmUfkXg2KFEA3EGbbVm1DPqQXayPYKAsYPS9AyKkcQ3fzWafLPP93UfNhtUPL8JW5pMcSV3P8v6j3vPXqnnGknNyBprD6YGUVtgLiAqDBDUF3LSxFQJCVYYtghMTv8WuSw9h1a1SRFrDQLGHE4UrkgoRvwaGWr64aM87T1eVGkP5Dt4L1AbboeK2ceLArPScrdYGTpi3BpTkLwZCdjdiFSfTy9okL1YNRARqUf2wm8DvkVGUU7u5nQA3ZMaXWJAewk6k1YUxKd7LvofGUK4YEDtoxN5vb6r1Q2godrGqaPkjfL3RoYPpDYymf9XhcgG8Kx3DZaA6cyTs24t45KxYAfeCw4wqUpCH9HbpD78TbEUr9PPAsJgXBvBj2VVsxnr7FKbK4KykGcg1W8M1JPz21Z4Y72LWgGQCmixovrkHktcTX1uNHjAvKBqVD5C7XmVfHgXCHj7djCh3vzLNuVLtEED8J1hhqsB1oCBGiuh3xXr7fZ9wUjJCQ1HYHqxLJKdYKtoCiPmgKM7etVftXkmTFETZmpM19aRyih3bao76LdpQtbw636r7a3qt8v4WfxsXJetSL8c7t24SqQBcAY89FBsbEnFNrQCMK3JEseKHVaU388ctvRD45uQfe5GndFxthj4iSDomk4uRFd1uRbywoP1tRuabHTDX42UxPjz" +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts new file mode 100644 index 0000000000..60af15b4f2 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts @@ -0,0 +1,45 @@ +import { JsonTransformer } from '../../../../../utils' +import { OutOfBandDidCommService } from '../../../../oob/domain/OutOfBandDidCommService' +import { DidDocument } from '../../../domain' +import { didDocumentToNumAlgo4Did, didToNumAlgo4DidDocument, outOfBandServiceToNumAlgo4Did } from '../peerDidNumAlgo4' + +import didPeer4zQmUJdJ from './__fixtures__/didPeer4zQmUJdJ.json' +import didPeer4zQmd8Cp from './__fixtures__/didPeer4zQmd8Cp.json' + +describe('peerDidNumAlgo4', () => { + describe('didDocumentToNumAlgo4Did', () => { + test('transforms method 4 peer did to a did document', async () => { + expect(didToNumAlgo4DidDocument(didPeer4zQmd8Cp.id).toJSON()).toMatchObject(didPeer4zQmd8Cp) + }) + }) + + describe('didDocumentToNumAlgo4Did', () => { + test('transforms method 4 peer did document to a did', async () => { + const longFormDid = didPeer4zQmUJdJ.id + const shortFormDid = didPeer4zQmUJdJ.alsoKnownAs[0] + + const didDocument = JsonTransformer.fromJSON(didPeer4zQmUJdJ, DidDocument) + + expect(didDocumentToNumAlgo4Did(didDocument)).toEqual({ longFormDid, shortFormDid }) + }) + }) + + describe('outOfBandServiceToNumAlgo4Did', () => { + test('transforms a did comm service into a valid method 4 did', () => { + const service = new OutOfBandDidCommService({ + id: '#service-0', + serviceEndpoint: 'https://example.com/endpoint', + recipientKeys: ['did:key:z6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V'], + routingKeys: ['did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'], + accept: ['didcomm/v2', 'didcomm/aip2;env=rfc587'], + }) + const { longFormDid } = outOfBandServiceToNumAlgo4Did(service) + const peerDidDocument = didToNumAlgo4DidDocument(longFormDid) + + expect(longFormDid).toBe( + 'did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn:z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + expect(longFormDid).toBe(peerDidDocument.id) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts b/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts index 4e23c98d3d..7a194d4c4c 100644 --- a/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts +++ b/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts @@ -5,7 +5,6 @@ import { convertPublicKeyToX25519 } from '@stablelib/ed25519' import { Key } from '../../../../crypto/Key' import { KeyType } from '../../../../crypto/KeyType' import { AriesFrameworkError } from '../../../../error' -import { uuid } from '../../../../utils/uuid' import { getEd25519VerificationKey2018, getX25519KeyAgreementKey2019 } from '../../domain' import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' import { DidCommV1Service } from '../../domain/service/DidCommV1Service' @@ -30,13 +29,14 @@ export function createPeerDidDocumentFromServices(services: ResolvedDidCommServi } const x25519Key = Key.fromPublicKey(convertPublicKeyToX25519(recipientKey.publicKey), KeyType.X25519) + // Remove prefix from id as it is not included in did peer identifiers const ed25519VerificationMethod = getEd25519VerificationKey2018({ - id: `#${uuid()}`, + id: `#${recipientKey.fingerprint.substring(1)}`, key: recipientKey, controller: '#id', }) const x25519VerificationMethod = getX25519KeyAgreementKey2019({ - id: `#${uuid()}`, + id: `#${x25519Key.fingerprint.substring(1)}`, key: x25519Key, controller: '#id', }) diff --git a/packages/core/src/modules/dids/methods/peer/didPeer.ts b/packages/core/src/modules/dids/methods/peer/didPeer.ts index 9aaf294ba5..622b5b6a9c 100644 --- a/packages/core/src/modules/dids/methods/peer/didPeer.ts +++ b/packages/core/src/modules/dids/methods/peer/didPeer.ts @@ -1,7 +1,9 @@ import { AriesFrameworkError } from '../../../../error' +import { getAlternativeDidsForNumAlgo4Did } from './peerDidNumAlgo4' + const PEER_DID_REGEX = new RegExp( - '^did:peer:(([01](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))|(2((.[AEVID](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))+(.(S)[0-9a-zA-Z=]*)?)))$' + '^did:peer:(([01](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))|(2((.[AEVID](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))+(.(S)[0-9a-zA-Z=]*)?))|([4](z[1-9a-km-zA-HJ-NP-Z]{46})(:z[1-9a-km-zA-HJ-NP-Z]{6,}){0,1}))$' ) export function isValidPeerDid(did: string): boolean { @@ -14,6 +16,7 @@ export enum PeerDidNumAlgo { InceptionKeyWithoutDoc = 0, GenesisDoc = 1, MultipleInceptionKeyWithoutDoc = 2, + ShortFormAndLongForm = 4, } export function getNumAlgoFromPeerDid(did: string) { @@ -22,10 +25,23 @@ export function getNumAlgoFromPeerDid(did: string) { if ( numAlgo !== PeerDidNumAlgo.InceptionKeyWithoutDoc && numAlgo !== PeerDidNumAlgo.GenesisDoc && - numAlgo !== PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc + numAlgo !== PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc && + numAlgo !== PeerDidNumAlgo.ShortFormAndLongForm ) { throw new AriesFrameworkError(`Invalid peer did numAlgo: ${numAlgo}`) } return numAlgo as PeerDidNumAlgo } + +/** + * Given a peer did, returns any alternative forms equivalent to it. + * + * @param did + * @returns array of alternative dids or undefined if not applicable + */ +export function getAlternativeDidsForPeerDid(did: string) { + if (getNumAlgoFromPeerDid(did) === PeerDidNumAlgo.ShortFormAndLongForm) { + return getAlternativeDidsForNumAlgo4Did(did) + } +} diff --git a/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts new file mode 100644 index 0000000000..cb8d61598d --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts @@ -0,0 +1,138 @@ +import type { OutOfBandDidCommService } from '../../../oob/domain/OutOfBandDidCommService' + +import { AriesFrameworkError } from '../../../../error' +import { + JsonEncoder, + JsonTransformer, + MultiBaseEncoder, + MultiHashEncoder, + TypedArrayEncoder, + VarintEncoder, +} from '../../../../utils' +import { Buffer } from '../../../../utils/buffer' +import { DidDocument, DidCommV1Service } from '../../domain' +import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' +import { parseDid } from '../../domain/parse' +import { DidKey } from '../key' + +const LONG_RE = new RegExp(`^did:peer:4(z[1-9a-km-zA-HJ-NP-Z]{46}):(z[1-9a-km-zA-HJ-NP-Z]{6,})$`) +const SHORT_RE = new RegExp(`^did:peer:4(z[1-9a-km-zA-HJ-NP-Z]{46})$`) +const JSON_MULTICODEC_VARINT = 0x0200 + +export const isShortFormDidPeer4 = (did: string) => SHORT_RE.test(did) +export const isLongFormDidPeer4 = (did: string) => LONG_RE.test(did) + +const hashEncodedDocument = (encodedDocument: string) => + MultiBaseEncoder.encode( + MultiHashEncoder.encode(TypedArrayEncoder.fromString(encodedDocument), 'sha2-256'), + 'base58btc' + ) + +export function getAlternativeDidsForNumAlgo4Did(did: string) { + const match = did.match(LONG_RE) + if (!match) return + const [, hash] = match + return [`did:peer:4${hash}`] +} + +export function didToNumAlgo4DidDocument(did: string) { + const parsed = parseDid(did) + + const match = parsed.did.match(LONG_RE) + if (!match) { + throw new AriesFrameworkError(`Invalid long form algo 4 did:peer: ${parsed.did}`) + } + const [, hash, encodedDocument] = match + if (hash !== hashEncodedDocument(encodedDocument)) { + throw new AriesFrameworkError(`Hash is invalid for did: ${did}`) + } + + const { data } = MultiBaseEncoder.decode(encodedDocument) + const [multiCodecValue] = VarintEncoder.decode(data.subarray(0, 2)) + if (multiCodecValue !== JSON_MULTICODEC_VARINT) { + throw new AriesFrameworkError(`Not a JSON multicodec data`) + } + const didDocumentJson = JsonEncoder.fromBuffer(data.subarray(2)) + + didDocumentJson.id = parsed.did + didDocumentJson.alsoKnownAs = [parsed.did.slice(0, did.lastIndexOf(':'))] + + // Populate all verification methods without controller + const addControllerIfNotPresent = (item: unknown) => { + if (Array.isArray(item)) item.forEach(addControllerIfNotPresent) + + if (item && typeof item === 'object' && (item as Record).controller === undefined) { + ;(item as Record).controller = parsed.did + } + } + + addControllerIfNotPresent(didDocumentJson.verificationMethod) + addControllerIfNotPresent(didDocumentJson.authentication) + addControllerIfNotPresent(didDocumentJson.assertionMethod) + addControllerIfNotPresent(didDocumentJson.keyAgreement) + addControllerIfNotPresent(didDocumentJson.capabilityDelegation) + addControllerIfNotPresent(didDocumentJson.capabilityInvocation) + + const didDocument = JsonTransformer.fromJSON(didDocumentJson, DidDocument) + return didDocument +} + +export function didDocumentToNumAlgo4Did(didDocument: DidDocument) { + const didDocumentJson = didDocument.toJSON() + + // Build input document based on did document, without any + // reference to controller + const deleteControllerIfPresent = (item: unknown) => { + if (Array.isArray(item)) { + item.forEach((method: { controller?: string }) => { + if (method.controller === '#id' || method.controller === didDocument.id) delete method.controller + }) + } + } + delete didDocumentJson.id + delete didDocumentJson.alsoKnownAs + deleteControllerIfPresent(didDocumentJson.verificationMethod) + deleteControllerIfPresent(didDocumentJson.authentication) + deleteControllerIfPresent(didDocumentJson.assertionMethod) + deleteControllerIfPresent(didDocumentJson.keyAgreement) + deleteControllerIfPresent(didDocumentJson.capabilityDelegation) + deleteControllerIfPresent(didDocumentJson.capabilityInvocation) + + // Construct encoded document by prefixing did document with multicodec prefix for JSON + const buffer = Buffer.concat([ + VarintEncoder.encode(JSON_MULTICODEC_VARINT), + Buffer.from(JSON.stringify(didDocumentJson)), + ]) + + const encodedDocument = MultiBaseEncoder.encode(buffer, 'base58btc') + + const shortFormDid = `did:peer:4${hashEncodedDocument(encodedDocument)}` + const longFormDid = `${shortFormDid}:${encodedDocument}` + + return { shortFormDid, longFormDid } +} + +export function outOfBandServiceToNumAlgo4Did(service: OutOfBandDidCommService) { + // FIXME: add the key entries for the recipientKeys to the did document. + const didDocument = new DidDocumentBuilder('') + .addService( + new DidCommV1Service({ + id: service.id, + serviceEndpoint: service.serviceEndpoint, + accept: service.accept, + // FIXME: this should actually be local key references, not did:key:123#456 references + recipientKeys: service.recipientKeys.map((recipientKey) => { + const did = DidKey.fromDid(recipientKey) + return `${did.did}#${did.key.fingerprint}` + }), + // Map did:key:xxx to actual did:key:xxx#123 + routingKeys: service.routingKeys?.map((routingKey) => { + const did = DidKey.fromDid(routingKey) + return `${did.did}#${did.key.fingerprint}` + }), + }) + ) + .build() + + return didDocumentToNumAlgo4Did(didDocument) +} diff --git a/packages/core/src/modules/dids/repository/DidRecord.ts b/packages/core/src/modules/dids/repository/DidRecord.ts index 5f8b9cc375..3f22751648 100644 --- a/packages/core/src/modules/dids/repository/DidRecord.ts +++ b/packages/core/src/modules/dids/repository/DidRecord.ts @@ -23,6 +23,10 @@ export interface DidRecordProps { export interface CustomDidTags extends TagsBase { recipientKeyFingerprints?: string[] + + // Alternative forms of the did, allowed to be queried by them. + // Relationship must be verified both ways before setting this tag. + alternativeDids?: string[] } type DefaultDidTags = { diff --git a/packages/core/src/modules/dids/repository/DidRepository.ts b/packages/core/src/modules/dids/repository/DidRepository.ts index 11a6c60b9a..0851390d87 100644 --- a/packages/core/src/modules/dids/repository/DidRepository.ts +++ b/packages/core/src/modules/dids/repository/DidRepository.ts @@ -48,22 +48,28 @@ export class DidRepository extends Repository { } public findAllByDid(agentContext: AgentContext, did: string) { - return this.findByQuery(agentContext, { did }) + return this.findByQuery(agentContext, { $or: [{ alternativeDids: [did] }, { did }] }) } public findReceivedDid(agentContext: AgentContext, receivedDid: string) { - return this.findSingleByQuery(agentContext, { did: receivedDid, role: DidDocumentRole.Received }) + return this.findSingleByQuery(agentContext, { + $or: [{ alternativeDids: [receivedDid] }, { did: receivedDid }], + role: DidDocumentRole.Received, + }) } public findCreatedDid(agentContext: AgentContext, createdDid: string) { - return this.findSingleByQuery(agentContext, { did: createdDid, role: DidDocumentRole.Created }) + return this.findSingleByQuery(agentContext, { + $or: [{ alternativeDids: [createdDid] }, { did: createdDid }], + role: DidDocumentRole.Created, + }) } public getCreatedDids(agentContext: AgentContext, { method, did }: { method?: string; did?: string }) { return this.findByQuery(agentContext, { role: DidDocumentRole.Created, method, - did, + $or: did ? [{ alternativeDids: [did] }, { did }] : undefined, }) } diff --git a/packages/core/src/modules/oob/OutOfBandApi.ts b/packages/core/src/modules/oob/OutOfBandApi.ts index b6f48b97e2..c4e65ccd57 100644 --- a/packages/core/src/modules/oob/OutOfBandApi.ts +++ b/packages/core/src/modules/oob/OutOfBandApi.ts @@ -77,6 +77,7 @@ interface BaseReceiveOutOfBandInvitationConfig { routing?: Routing acceptInvitationTimeoutMs?: number isImplicit?: boolean + ourDid?: string } export type ReceiveOutOfBandInvitationConfig = Omit @@ -479,6 +480,7 @@ export class OutOfBandApi { reuseConnection, routing, timeoutMs: config.acceptInvitationTimeoutMs, + ourDid: config.ourDid, }) } @@ -514,12 +516,13 @@ export class OutOfBandApi { */ routing?: Routing timeoutMs?: number + ourDid?: string } ) { const outOfBandRecord = await this.outOfBandService.getById(this.agentContext, outOfBandId) const { outOfBandInvitation } = outOfBandRecord - const { label, alias, imageUrl, autoAcceptConnection, reuseConnection } = config + const { label, alias, imageUrl, autoAcceptConnection, reuseConnection, ourDid } = config const services = outOfBandInvitation.getServices() const messages = outOfBandInvitation.getRequests() const timeoutMs = config.timeoutMs ?? 20000 @@ -585,6 +588,7 @@ export class OutOfBandApi { autoAcceptConnection, protocol: handshakeProtocol, routing, + ourDid, }) } diff --git a/packages/core/tests/oob.test.ts b/packages/core/tests/oob.test.ts index 2911d9d406..fddbb253ba 100644 --- a/packages/core/tests/oob.test.ts +++ b/packages/core/tests/oob.test.ts @@ -692,7 +692,7 @@ describe('out of band', () => { await expect(aliceAgent.oob.receiveInvitation(outOfBandInvitation, receiveInvitationConfig)).rejects.toEqual( new AriesFrameworkError( - `Handshake protocols [${unsupportedProtocol}] are not supported. Supported protocols are [https://didcomm.org/didexchange/1.0,https://didcomm.org/connections/1.0]` + `Handshake protocols [${unsupportedProtocol}] are not supported. Supported protocols are [https://didcomm.org/didexchange/1.1,https://didcomm.org/connections/1.0]` ) ) })