Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: 0.2.0 connection migration script #731

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
9539a95
Create OOB invitation
jakubkoci Oct 20, 2021
598ad08
Receive OOB connection invitaion
jakubkoci Oct 20, 2021
f4dce71
Send and recevie request via OOB invitation
jakubkoci Nov 4, 2021
255bebe
Separate OOB invitation and OOB message handling
jakubkoci Nov 5, 2021
4cd8dd9
Refactor and get rid of unnecessary dependencies to dispatcher and me…
jakubkoci Nov 8, 2021
81d51df
Allow undefined handshake protocols and request attach attributes
jakubkoci Nov 8, 2021
6b76b70
Remove unused code and logs
jakubkoci Nov 8, 2021
b9e4816
Rename oob tests wallets
jakubkoci Nov 8, 2021
bd8ec83
Add small changes based on review
jakubkoci Nov 9, 2021
d38e480
Extract getting supported handshake protocols into discover service
jakubkoci Nov 9, 2021
f68b11a
Pass all oob message props via constructor
jakubkoci Nov 10, 2021
077be04
Unify create and receive oob message methods
jakubkoci Nov 10, 2021
0e0c1c5
Create OOB message with both handshake and requests
jakubkoci Nov 12, 2021
d49821a
Accept OOB message with both handshake and requests
jakubkoci Nov 12, 2021
4360f90
Check if handshake protocols are supported
jakubkoci Nov 17, 2021
d1abc7c
Wait until the connection is made before processing requests
jakubkoci Nov 17, 2021
b3343b4
Reuse connection when it exists
jakubkoci Nov 18, 2021
7f0c24e
Refactor logic when connection already exists
jakubkoci Nov 18, 2021
941b9b7
Refactor finding of existing connection
jakubkoci Nov 19, 2021
83396d3
Omit version from handshake protocol list
jakubkoci Nov 19, 2021
b326310
Update public API and reorganize folder structure
jakubkoci Nov 19, 2021
bc05c3d
Add oob invitation encoding and decoding
jakubkoci Nov 22, 2021
ffd6b78
Use filter method for handshake protocols from dispatcher
jakubkoci Nov 24, 2021
ac36ff4
Create a new connection instead of reusing an existing one
jakubkoci Nov 26, 2021
8038ea9
Update according to main branch API changes
jakubkoci Dec 16, 2021
f402b84
Use connections module instead of service
jakubkoci Dec 17, 2021
29a244d
Add support for old url encoded connection invitation
jakubkoci Dec 17, 2021
8a595a2
Iterate over all services and reciepient keys to find a connection
jakubkoci Dec 17, 2021
661c5cd
Replace custom event handler with async method provided by framework
jakubkoci Dec 28, 2021
111e658
Update types
jakubkoci Dec 28, 2021
a396dcb
Pass connection label to old invitation message
jakubkoci Dec 29, 2021
e864792
Rename unpack to plaintext
jakubkoci Dec 29, 2021
e9f56fc
Add thread to handshake reuse message
jakubkoci Dec 30, 2021
376f86e
Code review updates mainly about API and docs
jakubkoci Jan 8, 2022
e0ee539
Allow dids in services attribute of oob message
jakubkoci Jan 11, 2022
4ad262d
Process only first supported requests message
jakubkoci Jan 12, 2022
caed198
Throw error when there are no services in connection record
jakubkoci Jan 13, 2022
a435fa0
Refactor get requests method
jakubkoci Jan 13, 2022
2fe1f2b
Log error insted of commented code
jakubkoci Jan 13, 2022
5c220b3
Update api after rebase
jakubkoci Jan 18, 2022
64e0bf4
Throw error instead of resolving did from services attribute
jakubkoci Jan 18, 2022
b47560a
Update structure of oob tests
jakubkoci Jan 18, 2022
f917ef5
Add connection record to received message event
jakubkoci Jan 18, 2022
ce61a45
Update test of oob with requests witg parsing of encoded invitation
jakubkoci Jan 19, 2022
f10d3a8
Make label mandatory
jakubkoci Jan 19, 2022
6f0739d
Update receive message method signature
jakubkoci Jan 19, 2022
ba21f83
Updates after rebase
jakubkoci Jan 28, 2022
877514d
feat(core): did-exchange protocol (#615)
jakubkoci Feb 12, 2022
37a28f1
feat(core): add OOB record (#655)
jakubkoci Mar 5, 2022
b317d8c
feat: Replace old oob invitation with the new one (#676)
jakubkoci Mar 23, 2022
f819960
test: reuse connection create and process request tests (#680)
jakubkoci Mar 27, 2022
783824e
refactor: remove verkeys and did docs from connection record (#687)
jakubkoci Apr 5, 2022
9399a71
feat: find existing connection based on invitation did (#698)
jakubkoci Apr 13, 2022
2b73215
fix: use new oob instead of connections
jakubkoci Apr 15, 2022
fe463af
feat: use did keys for oob and did exchange protocols (#700)
jakubkoci Apr 21, 2022
337d6a2
test: remove all inline loggers
TimoGlastra Apr 30, 2022
f18c731
feat: 0.2.0 migration script for connections
TimoGlastra Apr 30, 2022
d4a5176
test: mock uuid for consistent snapshots
TimoGlastra Apr 30, 2022
9d18de4
fix: default metadata type
TimoGlastra Apr 30, 2022
8b4349f
docs: add connection migration docs
TimoGlastra Apr 30, 2022
428da75
fix: add logs remove fixmes
TimoGlastra Apr 30, 2022
a799116
test: add test for oob already exists
TimoGlastra Apr 30, 2022
51ccb05
feat: connection state and role migration script
TimoGlastra Apr 30, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions demo/src/Alice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,19 @@ export class Alice extends BaseAgent {
}

private async printConnectionInvite() {
const invite = await this.agent.connections.createConnection()
this.connectionRecordFaberId = invite.connectionRecord.id
const outOfBand = await this.agent.oob.createInvitation()
const connectionRecord = await this.agent.connections.findByOutOfBandId(outOfBand.id)
if (!connectionRecord) {
throw new Error(redText(Output.NoConnectionRecordFromOutOfBand))
}
this.connectionRecordFaberId = connectionRecord.id

console.log(Output.ConnectionLink, invite.invitation.toUrl({ domain: `http://localhost:${this.port}` }), '\n')
return invite.connectionRecord
console.log(
Output.ConnectionLink,
outOfBand.outOfBandMessage.toUrl({ domain: `http://localhost:${this.port}` }),
'\n'
)
return connectionRecord
}

private async waitForConnection() {
Expand Down
6 changes: 5 additions & 1 deletion demo/src/Faber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ export class Faber extends BaseAgent {
}

private async receiveConnectionRequest(invitationUrl: string) {
return await this.agent.connections.receiveInvitationFromUrl(invitationUrl)
const { connectionRecord } = await this.agent.oob.receiveInvitationFromUrl(invitationUrl)
if (!connectionRecord) {
throw new Error(redText(Output.NoConnectionRecordFromOutOfBand))
}
return connectionRecord
}

private async waitForConnection(connectionRecord: ConnectionRecord) {
Expand Down
1 change: 1 addition & 0 deletions demo/src/OutputClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum Color {
}

export enum Output {
NoConnectionRecordFromOutOfBand = `\nNo connectionRecord has been created from invitation\n`,
ConnectionEstablished = `\nConnection established!`,
MissingConnectionRecord = `\nNo connectionRecord ID has been set yet\n`,
ConnectionLink = `\nRun 'Receive connection invitation' in Faber and paste this invitation link:\n\n`,
Expand Down
82 changes: 82 additions & 0 deletions docs/migration/0.1-to-0.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,85 @@ Because it's not always possible detect whether the role should actually be medi
- `doNotChange`: The role is not changed

Most agents only act as either the role of mediator or recipient, in which case the `allMediator` or `allRecipient` configuration is the most appropriate. If your agent acts as both a recipient and mediator, the `recipientIfEndpoint` configuration is the most appropriate. The `doNotChange` options is not recommended and can lead to errors if the role is not set correctly.

### Extracting Did Documents to Did Repository

The connection record previously stored both did documents from a connection in the connection record itself. Version 0.2.0 added a generic did storage that can be used for numerous usages, one of which is the storage of did documents for connection records.

The migration script extracts the did documents from the `didDoc` and `theirDidDoc` properties from the connection record, updates them to did documents compliant with the did core spec, and stores them in the did repository. By doing so it also updates the unqualified dids in the `did` and `theirDid` fields generated by the indy-sdk to fully qualified `did:peer` dids compliant with the [Peer DID Method Specification](https://identity.foundation/peer-did-method-spec/).

To account for the fact that the mechanism to migrate legacy did document to peer did documents is not defined yet, the legacy did and did document are stored in the did record metadata. This will be deleted later if we can be certain the did doc conversion to a `did:peer` did document is correct.

The following 0.1.0 connection record structure (unrelated keys omitted):

```json
{
"did": "BBPoJqRKatdcfLEAFL7exC",
"theirDid": "N8NQHLtCKfPmWMgCSdfa7h",
"didDoc": <legacyDidDoc>,
"theirDidDoc": <legacyTheirDidDoc>,
}
```

Will be transformed into the following 0.2.0 structure (unrelated keys omitted):

```json
{
"did": "did:peer:1zQmXUaPPhPCbUVZ3hGYmQmGxWTwyDfhqESXCpMFhKaF9Y2A",
"theirDid": "did:peer:1zQmZMygzYqNwU6Uhmewx5Xepf2VLp5S4HLSwwgf2aiKZuwa"
}
```

### Migrating to the Out of Band Record

With the addition of the out of band protocol, invitations are now stored in the `OutOfBandRecord`. In addition a new field `invitationDid` is added to the connection record that is generated based on the invitation service or did. This allows to reuse existing connections.

The migration script extracts the invitation and other relevant data into a separate `OutOfBandRecord`. By doing so it converts the old connection protocol invitation into the new Out of band invitation message. Based on the service or did of the invitation, the `invitationDid` is populated.

The following 0.1.0 connection record structure (unrelated keys omitted):

```json
{
"invitation": {
"@type": "https://didcomm.org/connections/1.0/invitation",
"@id": "04a2c382-999e-4de9-a1d2-9dec0b2fa5e4",
"recipientKeys": ["E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu"],
"serviceEndpoint": "https://example.com",
"label": "test"
}
}
```

Will be transformed into the following 0.2.0 structure (unrelated keys omitted):

```json
{
"invitationDid": "did:peer:2.Ez6MksYU4MHtfmNhNm1uGMvANr9j4CBv2FymjiJtRgA36bSVH.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSJ9",
"outOfBandId": "04a2c382-999e-4de9-a1d2-9dec0b2fa5e4"
}
```

### Unifying Connection States and Roles

With the addition of the did exchange protocol there are now two states and roles related to the connection record; for the did exchange protocol and for the connection protocol. To keep it easy to work with the connection record, all state and role values are updated to those of the `DidExchangeRole` and `DidExchangeState` enums.

The migration script transforms all connection record state and role values to their respective values of the `DidExchangeRole` and `DidExchangeState` enums. For convenience a getter
property `rfc0160ConnectionState` is added to the connection record which returns the `ConnectionState` value.

The following 0.1.0 connection record structure (unrelated keys omitted):

```json
{
"state": "invited",
"role": "inviter"
}
```

Will be transformed into the following 0.2.0 structure (unrelated keys omitted):

```json
{
"state": "invitation-sent",
"role": "responder"
}
```
40 changes: 37 additions & 3 deletions packages/core/src/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { CredentialsModule } from '../modules/credentials/CredentialsModule'
import { DidsModule } from '../modules/dids/DidsModule'
import { DiscoverFeaturesModule } from '../modules/discover-features'
import { LedgerModule } from '../modules/ledger/LedgerModule'
import { OutOfBandModule } from '../modules/oob/OutOfBandModule'
import { ProofsModule } from '../modules/proofs/ProofsModule'
import { MediatorModule } from '../modules/routing/MediatorModule'
import { RecipientModule } from '../modules/routing/RecipientModule'
Expand Down Expand Up @@ -61,6 +62,7 @@ export class Agent {
public readonly discovery: DiscoverFeaturesModule
public readonly dids: DidsModule
public readonly wallet: WalletModule
public readonly oob!: OutOfBandModule

public constructor(
initialConfig: InitConfig,
Expand Down Expand Up @@ -123,13 +125,14 @@ export class Agent {
this.discovery = this.container.resolve(DiscoverFeaturesModule)
this.dids = this.container.resolve(DidsModule)
this.wallet = this.container.resolve(WalletModule)
this.oob = this.container.resolve(OutOfBandModule)

// Listen for new messages (either from transports or somewhere else in the framework / extensions)
this.messageSubscription = this.eventEmitter
.observable<AgentMessageReceivedEvent>(AgentEventTypes.AgentMessageReceived)
.pipe(
takeUntil(this.agentConfig.stop$),
concatMap((e) => this.messageReceiver.receiveMessage(e.payload.message))
concatMap((e) => this.messageReceiver.receiveMessage(e.payload.message, { connection: e.payload.connection }))
)
.subscribe()
}
Expand Down Expand Up @@ -224,7 +227,9 @@ export class Agent {
// Also requests mediation ans sets as default mediator
// Because this requires the connections module, we do this in the agent constructor
if (mediatorConnectionsInvite) {
await this.mediationRecipient.provision(mediatorConnectionsInvite)
this.logger.debug('Provision mediation with invitation', { mediatorConnectionsInvite })
const mediatonConnection = await this.getMediationConnection(mediatorConnectionsInvite)
await this.mediationRecipient.provision(mediatonConnection)
}

await this.mediationRecipient.initialize()
Expand Down Expand Up @@ -254,7 +259,7 @@ export class Agent {
}

public async receiveMessage(inboundMessage: unknown, session?: TransportSession) {
return await this.messageReceiver.receiveMessage(inboundMessage, session)
return await this.messageReceiver.receiveMessage(inboundMessage, { session })
}

public get injectionContainer() {
Expand All @@ -264,4 +269,33 @@ export class Agent {
public get config() {
return this.agentConfig
}

private async getMediationConnection(mediatorInvitationUrl: string) {
const outOfBandMessage = await this.oob.parseInvitation(mediatorInvitationUrl)
const outOfBandRecord = await this.oob.findByMessageId(outOfBandMessage.id)
const connection = outOfBandRecord && (await this.connections.findByOutOfBandId(outOfBandRecord.id))

if (!connection) {
this.logger.debug('Mediation connection does not exist, creating connection')
// We don't want to use the current default mediator when connecting to another mediator
const routing = await this.mediationRecipient.getRouting({ useDefaultMediator: false })

this.logger.debug('Routing created', routing)
const { connectionRecord: newConnection } = await this.oob.receiveInvitation(outOfBandMessage, {
routing,
})
this.logger.debug(`Mediation invitation processed`, { outOfBandMessage })

if (!newConnection) {
throw new AriesFrameworkError('No connection record to provision mediation.')
}

return this.connections.returnWhenIsConnected(newConnection.id)
}

if (!connection.isReady) {
return this.connections.returnWhenIsConnected(connection.id)
}
return connection
}
}
4 changes: 2 additions & 2 deletions packages/core/src/agent/Dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ class Dispatcher {
this.logger.error(`Error handling message with type ${message.type}`, {
message: message.toJSON(),
error,
senderVerkey: messageContext.senderVerkey,
recipientVerkey: messageContext.recipientVerkey,
senderKey: messageContext.senderKey,
recipientKey: messageContext.recipientKey,
connectionId: messageContext.connection?.id,
})

Expand Down
42 changes: 28 additions & 14 deletions packages/core/src/agent/EnvelopeService.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import type { Logger } from '../logger'
import type { DecryptedMessageContext, EncryptedMessage } from '../types'
import type { EncryptedMessage, PlaintextMessage } from '../types'
import type { AgentMessage } from './AgentMessage'

import { inject, scoped, Lifecycle } from 'tsyringe'

import { InjectionSymbols } from '../constants'
import { KeyType } from '../crypto'
import { Key } from '../modules/dids'
import { ForwardMessage } from '../modules/routing/messages'
import { Wallet } from '../wallet/Wallet'

import { AgentConfig } from './AgentConfig'

export interface EnvelopeKeys {
recipientKeys: string[]
routingKeys: string[]
senderKey: string | null
recipientKeys: Key[]
routingKeys: Key[]
senderKey: Key | null
}

@scoped(Lifecycle.ContainerScoped)
class EnvelopeService {
export class EnvelopeService {
private wallet: Wallet
private logger: Logger
private config: AgentConfig
Expand All @@ -29,38 +31,50 @@ class EnvelopeService {
}

public async packMessage(payload: AgentMessage, keys: EnvelopeKeys): Promise<EncryptedMessage> {
const { routingKeys, senderKey } = keys
let recipientKeys = keys.recipientKeys
const { recipientKeys, routingKeys, senderKey } = keys
let recipientKeysBase58 = recipientKeys.map((key) => key.publicKeyBase58)
const routingKeysBase58 = routingKeys.map((key) => key.publicKeyBase58)
const senderKeyBase58 = senderKey && senderKey.publicKeyBase58

// pass whether we want to use legacy did sov prefix
const message = payload.toJSON({ useLegacyDidSovPrefix: this.config.useLegacyDidSovPrefix })

this.logger.debug(`Pack outbound message ${message['@type']}`)

let encryptedMessage = await this.wallet.pack(message, recipientKeys, senderKey ?? undefined)
let encryptedMessage = await this.wallet.pack(message, recipientKeysBase58, senderKeyBase58 ?? undefined)

// If the message has routing keys (mediator) pack for each mediator
for (const routingKey of routingKeys) {
for (const routingKeyBase58 of routingKeysBase58) {
const forwardMessage = new ForwardMessage({
// Forward to first recipient key
to: recipientKeys[0],
to: recipientKeysBase58[0],
message: encryptedMessage,
})
recipientKeys = [routingKey]
recipientKeysBase58 = [routingKeyBase58]
this.logger.debug('Forward message created', forwardMessage)

const forwardJson = forwardMessage.toJSON({ useLegacyDidSovPrefix: this.config.useLegacyDidSovPrefix })

// Forward messages are anon packed
encryptedMessage = await this.wallet.pack(forwardJson, [routingKey], undefined)
encryptedMessage = await this.wallet.pack(forwardJson, [routingKeyBase58], undefined)
}

return encryptedMessage
}

public async unpackMessage(encryptedMessage: EncryptedMessage): Promise<DecryptedMessageContext> {
return this.wallet.unpack(encryptedMessage)
const decryptedMessage = await this.wallet.unpack(encryptedMessage)
const { recipientKey, senderKey, plaintextMessage } = decryptedMessage
return {
recipientKey: recipientKey ? Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519) : undefined,
senderKey: senderKey ? Key.fromPublicKeyBase58(senderKey, KeyType.Ed25519) : undefined,
plaintextMessage,
}
}
}

export { EnvelopeService }
export interface DecryptedMessageContext {
plaintextMessage: PlaintextMessage
senderKey?: Key
recipientKey?: Key
}
1 change: 1 addition & 0 deletions packages/core/src/agent/Events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface AgentMessageReceivedEvent extends BaseEvent {
type: typeof AgentEventTypes.AgentMessageReceived
payload: {
message: unknown
connection?: ConnectionRecord
}
}

Expand Down
Loading