Skip to content

Commit

Permalink
feat(oob): implicit invitations (#1348)
Browse files Browse the repository at this point in the history
Signed-off-by: Ariel Gentile <[email protected]>
  • Loading branch information
genaris authored Mar 2, 2023
1 parent 18abb18 commit fd13bb8
Show file tree
Hide file tree
Showing 12 changed files with 448 additions and 36 deletions.
15 changes: 10 additions & 5 deletions packages/core/src/modules/connections/DidExchangeProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
PeerDidNumAlgo,
} 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 { OutOfBandRole } from '../oob/domain/OutOfBandRole'
Expand Down Expand Up @@ -104,7 +105,7 @@ export class DidExchangeProtocol {
// Create message
const label = params.label ?? agentContext.config.label
const didDocument = await this.createPeerDidDoc(agentContext, this.routingToServices(routing))
const parentThreadId = outOfBandInvitation.id
const parentThreadId = outOfBandRecord.outOfBandInvitation.id

const message = new DidExchangeRequestMessage({ label, parentThreadId, did: didDocument.id, goal, goalCode })

Expand Down Expand Up @@ -146,9 +147,13 @@ export class DidExchangeProtocol {

const { message } = messageContext

// Check corresponding invitation ID is the request's ~thread.pthid
// 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.
if (!message.thread?.parentThreadId || message.thread?.parentThreadId !== outOfBandRecord.getTags().invitationId) {
const parentThreadId = message.thread?.parentThreadId
if (
!parentThreadId ||
(!tryParseDid(parentThreadId) && parentThreadId !== outOfBandRecord.getTags().invitationId)
) {
throw new DidExchangeProblemReportError('Missing reference to invitation.', {
problemCode: DidExchangeProblemReportReason.RequestNotAccepted,
})
Expand Down Expand Up @@ -401,8 +406,8 @@ export class DidExchangeProtocol {
problemCode: DidExchangeProblemReportReason.CompleteRejected,
})
}

if (!message.thread?.parentThreadId || message.thread?.parentThreadId !== outOfBandRecord.getTags().invitationId) {
const pthid = message.thread?.parentThreadId
if (!pthid || pthid !== outOfBandRecord.outOfBandInvitation.id) {
throw new DidExchangeProblemReportError('Invalid or missing parent thread ID referencing to the invitation.', {
problemCode: DidExchangeProblemReportReason.CompleteRejected,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import type { ConnectionService } from '../services/ConnectionService'

import { OutboundMessageContext } from '../../../agent/models'
import { AriesFrameworkError } from '../../../error/AriesFrameworkError'
import { tryParseDid } from '../../dids/domain/parse'
import { ConnectionRequestMessage } from '../messages'
import { HandshakeProtocol } from '../models'

export class ConnectionRequestHandler implements MessageHandler {
private connectionService: ConnectionService
Expand All @@ -32,16 +34,23 @@ export class ConnectionRequestHandler implements MessageHandler {
}

public async handle(messageContext: MessageHandlerInboundMessage<ConnectionRequestHandler>) {
const { connection, recipientKey, senderKey } = messageContext
const { agentContext, connection, recipientKey, senderKey, message } = messageContext

if (!recipientKey || !senderKey) {
throw new AriesFrameworkError('Unable to process connection request without senderVerkey or recipientKey')
}

const outOfBandRecord = await this.outOfBandService.findCreatedByRecipientKey(
messageContext.agentContext,
recipientKey
)
const parentThreadId = message.thread?.parentThreadId

const outOfBandRecord =
parentThreadId && tryParseDid(parentThreadId)
? await this.outOfBandService.createFromImplicitInvitation(agentContext, {
did: parentThreadId,
threadId: message.threadId,
recipientKey,
handshakeProtocols: [HandshakeProtocol.Connections],
})
: await this.outOfBandService.findCreatedByRecipientKey(agentContext, recipientKey)

if (!outOfBandRecord) {
throw new AriesFrameworkError(`Out-of-band record for recipient key ${recipientKey.fingerprint} was not found.`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { OutOfBandService } from '../../oob/OutOfBandService'
import type { DidExchangeProtocol } from '../DidExchangeProtocol'

import { AriesFrameworkError } from '../../../error'
import { tryParseDid } from '../../dids/domain/parse'
import { OutOfBandState } from '../../oob/domain/OutOfBandState'
import { DidExchangeCompleteMessage } from '../messages'
import { HandshakeProtocol } from '../models'
Expand Down Expand Up @@ -32,12 +33,14 @@ export class DidExchangeCompleteHandler implements MessageHandler {
}

const { message } = messageContext
if (!message.thread?.parentThreadId) {
const parentThreadId = message.thread?.parentThreadId
if (!parentThreadId) {
throw new AriesFrameworkError(`Message does not contain pthid attribute`)
}
const outOfBandRecord = await this.outOfBandService.findByCreatedInvitationId(
messageContext.agentContext,
message.thread?.parentThreadId
parentThreadId,
tryParseDid(parentThreadId) ? message.threadId : undefined
)

if (!outOfBandRecord) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import type { DidExchangeProtocol } from '../DidExchangeProtocol'

import { OutboundMessageContext } from '../../../agent/models'
import { AriesFrameworkError } from '../../../error/AriesFrameworkError'
import { tryParseDid } from '../../dids/domain/parse'
import { OutOfBandState } from '../../oob/domain/OutOfBandState'
import { DidExchangeRequestMessage } from '../messages'
import { HandshakeProtocol } from '../models'

export class DidExchangeRequestHandler implements MessageHandler {
private didExchangeProtocol: DidExchangeProtocol
Expand All @@ -33,22 +35,28 @@ export class DidExchangeRequestHandler implements MessageHandler {
}

public async handle(messageContext: MessageHandlerInboundMessage<DidExchangeRequestHandler>) {
const { recipientKey, senderKey, message, connection } = messageContext
const { agentContext, recipientKey, senderKey, message, connection } = messageContext

if (!recipientKey || !senderKey) {
throw new AriesFrameworkError('Unable to process connection request without senderKey or recipientKey')
}

if (!message.thread?.parentThreadId) {
const parentThreadId = message.thread?.parentThreadId

if (!parentThreadId) {
throw new AriesFrameworkError(`Message does not contain 'pthid' attribute`)
}
const outOfBandRecord = await this.outOfBandService.findByCreatedInvitationId(
messageContext.agentContext,
message.thread.parentThreadId
)

const outOfBandRecord = tryParseDid(parentThreadId)
? await this.outOfBandService.createFromImplicitInvitation(agentContext, {
did: parentThreadId,
threadId: message.threadId,
recipientKey,
handshakeProtocols: [HandshakeProtocol.DidExchange],
})
: await this.outOfBandService.findByCreatedInvitationId(agentContext, parentThreadId)
if (!outOfBandRecord) {
throw new AriesFrameworkError(`OutOfBand record for message ID ${message.thread?.parentThreadId} not found!`)
throw new AriesFrameworkError(`OutOfBand record for message ID ${parentThreadId} not found!`)
}

if (connection && !outOfBandRecord.reusable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export class ConnectionService {

connectionRequest.setThread({
threadId: connectionRequest.threadId,
parentThreadId: outOfBandInvitation.id,
parentThreadId: outOfBandRecord.outOfBandInvitation.id,
})

const connectionRecord = await this.createConnection(agentContext, {
Expand Down
76 changes: 64 additions & 12 deletions packages/core/src/modules/oob/OutOfBandApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export interface CreateLegacyInvitationConfig {
routing?: Routing
}

export interface ReceiveOutOfBandInvitationConfig {
interface BaseReceiveOutOfBandInvitationConfig {
label?: string
alias?: string
imageUrl?: string
Expand All @@ -76,6 +76,15 @@ export interface ReceiveOutOfBandInvitationConfig {
reuseConnection?: boolean
routing?: Routing
acceptInvitationTimeoutMs?: number
isImplicit?: boolean
}

export type ReceiveOutOfBandInvitationConfig = Omit<BaseReceiveOutOfBandInvitationConfig, 'isImplicit'>

export interface ReceiveOutOfBandImplicitInvitationConfig
extends Omit<BaseReceiveOutOfBandInvitationConfig, 'isImplicit' | 'reuseConnection'> {
did: string
handshakeProtocols?: HandshakeProtocol[]
}

@injectable()
Expand Down Expand Up @@ -321,6 +330,44 @@ export class OutOfBandApi {
public async receiveInvitation(
invitation: OutOfBandInvitation | ConnectionInvitationMessage,
config: ReceiveOutOfBandInvitationConfig = {}
): Promise<{ outOfBandRecord: OutOfBandRecord; connectionRecord?: ConnectionRecord }> {
return this._receiveInvitation(invitation, config)
}

/**
* Creates inbound out-of-band record from an implicit invitation, given as a public DID the agent
* should be capable of resolving. It automatically passes out-of-band invitation for further
* processing to `acceptInvitation` method. If you don't want to do that you can set
* `autoAcceptInvitation` attribute in `config` parameter to `false` and accept the message later by
* calling `acceptInvitation`.
*
* It supports both OOB (Aries RFC 0434: Out-of-Band Protocol 1.1) and Connection Invitation
* (0160: Connection Protocol). Handshake protocol to be used depends on handshakeProtocols
* (DID Exchange by default)
*
* Agent role: receiver (invitee)
*
* @param config config for creating and handling invitation
*
* @returns out-of-band record and connection record if one has been created.
*/
public async receiveImplicitInvitation(config: ReceiveOutOfBandImplicitInvitationConfig) {
const invitation = new OutOfBandInvitation({
id: config.did,
label: config.label ?? '',
services: [config.did],
handshakeProtocols: config.handshakeProtocols ?? [HandshakeProtocol.DidExchange],
})

return this._receiveInvitation(invitation, { ...config, isImplicit: true })
}

/**
* Internal receive invitation method, for both explicit and implicit OOB invitations
*/
private async _receiveInvitation(
invitation: OutOfBandInvitation | ConnectionInvitationMessage,
config: BaseReceiveOutOfBandInvitationConfig = {}
): Promise<{ outOfBandRecord: OutOfBandRecord; connectionRecord?: ConnectionRecord }> {
// Convert to out of band invitation if needed
const outOfBandInvitation =
Expand All @@ -344,15 +391,19 @@ export class OutOfBandApi {
)
}

// Make sure we haven't received this invitation before. (it's fine if we created it, that means we're connecting with ourselves
let [outOfBandRecord] = await this.outOfBandService.findAllByQuery(this.agentContext, {
invitationId: outOfBandInvitation.id,
role: OutOfBandRole.Receiver,
})
if (outOfBandRecord) {
throw new AriesFrameworkError(
`An out of band record with invitation ${outOfBandInvitation.id} has already been received. Invitations should have a unique id.`
)
// Make sure we haven't received this invitation before
// It's fine if we created it (means that we are connnecting to ourselves) or if it's an implicit
// invitation (it allows to connect multiple times to the same public did)
if (!config.isImplicit) {
const existingOobRecordsFromThisId = await this.outOfBandService.findAllByQuery(this.agentContext, {
invitationId: outOfBandInvitation.id,
role: OutOfBandRole.Receiver,
})
if (existingOobRecordsFromThisId.length > 0) {
throw new AriesFrameworkError(
`An out of band record with invitation ${outOfBandInvitation.id} has already been received. Invitations should have a unique id.`
)
}
}

const recipientKeyFingerprints: string[] = []
Expand All @@ -374,7 +425,7 @@ export class OutOfBandApi {
}
}

outOfBandRecord = new OutOfBandRecord({
const outOfBandRecord = new OutOfBandRecord({
role: OutOfBandRole.Receiver,
state: OutOfBandState.Initial,
outOfBandInvitation: outOfBandInvitation,
Expand Down Expand Up @@ -430,11 +481,12 @@ export class OutOfBandApi {

const { outOfBandInvitation } = outOfBandRecord
const { label, alias, imageUrl, autoAcceptConnection, reuseConnection, routing } = config
const { handshakeProtocols } = outOfBandInvitation
const services = outOfBandInvitation.getServices()
const messages = outOfBandInvitation.getRequests()
const timeoutMs = config.timeoutMs ?? 20000

const { handshakeProtocols } = outOfBandInvitation

const existingConnection = await this.findExistingConnection(outOfBandInvitation)

await this.outOfBandService.updateState(this.agentContext, outOfBandRecord, OutOfBandState.PrepareResponse)
Expand Down
64 changes: 60 additions & 4 deletions packages/core/src/modules/oob/OutOfBandService.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
import type { HandshakeReusedEvent, OutOfBandStateChangedEvent } from './domain/OutOfBandEvents'
import type { OutOfBandRecord } from './repository'
import type { AgentContext } from '../../agent'
import type { InboundMessageContext } from '../../agent/models/InboundMessageContext'
import type { Key } from '../../crypto'
import type { Query } from '../../storage/StorageService'
import type { ConnectionRecord } from '../connections'
import type { HandshakeProtocol } from '../connections/models'

import { EventEmitter } from '../../agent/EventEmitter'
import { AriesFrameworkError } from '../../error'
import { injectable } from '../../plugins'
import { JsonTransformer } from '../../utils'
import { DidsApi } from '../dids'
import { parseDid } from '../dids/domain/parse'

import { OutOfBandEventTypes } from './domain/OutOfBandEvents'
import { OutOfBandRole } from './domain/OutOfBandRole'
import { OutOfBandState } from './domain/OutOfBandState'
import { HandshakeReuseMessage } from './messages'
import { HandshakeReuseMessage, OutOfBandInvitation } from './messages'
import { HandshakeReuseAcceptedMessage } from './messages/HandshakeReuseAcceptedMessage'
import { OutOfBandRepository } from './repository'
import { OutOfBandRecord, OutOfBandRepository } from './repository'

export interface CreateFromImplicitInvitationConfig {
did: string
threadId: string
handshakeProtocols: HandshakeProtocol[]
autoAcceptConnection?: boolean
recipientKey: Key
}

@injectable()
export class OutOfBandService {
Expand All @@ -28,6 +38,51 @@ export class OutOfBandService {
this.eventEmitter = eventEmitter
}

/**
* Creates an Out of Band record from a Connection/DIDExchange request started by using
* a publicly resolvable DID this agent can control
*/
public async createFromImplicitInvitation(
agentContext: AgentContext,
config: CreateFromImplicitInvitationConfig
): Promise<OutOfBandRecord> {
const { did, threadId, handshakeProtocols, autoAcceptConnection, recipientKey } = config

// Verify it is a valid did and it is present in the wallet
const publicDid = parseDid(did)
const didsApi = agentContext.dependencyManager.resolve(DidsApi)
const [createdDid] = await didsApi.getCreatedDids({ did: publicDid.did })
if (!createdDid) {
throw new AriesFrameworkError(`Referenced public did ${did} not found.`)
}

// Recreate an 'implicit invitation' matching the parameters used by the invitee when
// initiating the flow
const outOfBandInvitation = new OutOfBandInvitation({
id: did,
label: '',
services: [did],
handshakeProtocols,
})

outOfBandInvitation.setThread({ threadId })

const outOfBandRecord = new OutOfBandRecord({
role: OutOfBandRole.Sender,
state: OutOfBandState.AwaitResponse,
reusable: true,
autoAcceptConnection: autoAcceptConnection ?? false,
outOfBandInvitation,
tags: {
recipientKeyFingerprints: [recipientKey.fingerprint],
},
})

await this.save(agentContext, outOfBandRecord)
this.emitStateChangedEvent(agentContext, outOfBandRecord, null)
return outOfBandRecord
}

public async processHandshakeReuse(messageContext: InboundMessageContext<HandshakeReuseMessage>) {
const reuseMessage = messageContext.message
const parentThreadId = reuseMessage.thread?.parentThreadId
Expand Down Expand Up @@ -172,10 +227,11 @@ export class OutOfBandService {
})
}

public async findByCreatedInvitationId(agentContext: AgentContext, createdInvitationId: string) {
public async findByCreatedInvitationId(agentContext: AgentContext, createdInvitationId: string, threadId?: string) {
return this.outOfBandRepository.findSingleByQuery(agentContext, {
invitationId: createdInvitationId,
role: OutOfBandRole.Sender,
threadId,
})
}

Expand Down
Loading

0 comments on commit fd13bb8

Please sign in to comment.