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: use did:key flag #1029

Merged
merged 7 commits into from
Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions packages/core/src/agent/AgentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ export class AgentConfig {
return this.initConfig.maximumMediatorReconnectionIntervalMs ?? Number.POSITIVE_INFINITY
}

/**
* Encode keys in did:key format instead of 'naked' keys, as stated in Aries RFC 0360.
*
* This setting will not be taken into account if the other party has previously used naked keys
* in a given protocol (i.e. it does not support Aries RFC 0360).
*/
public get useDidKeyInProtocols() {
return this.initConfig.useDidKeyInProtocols ?? false
}

public get endpoints(): [string, ...string[]] {
// if endpoints is not set, return queue endpoint
// https://github.com/hyperledger/aries-rfcs/issues/405#issuecomment-582612875
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export enum ConnectionMetadataKeys {
UseDidKeysForProtocol = '_internal/useDidKeysForProtocol',
}

export type ConnectionMetadata = {
[ConnectionMetadataKeys.UseDidKeysForProtocol]: {
[protocolUri: string]: boolean
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specific reason for making this a key/value object instead of an array with string uri's?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of this metadata key is to override agent settings, based on what we learn from the connection. So if our agent is set to always use did:key by default and we learn that the other side is not using it for a certain protocol, we'll set the key for that protocol to false.

And we could also have the opposite case: we don't use did:key but it happens that the other side is using them, so we set the key for that protocol to true. Also we might change our agent settings afterwards, and it will still take into account the metadata key for the given connection.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I thought we would only use it to say a protocol does support did:key, but there is also the case where we explicitly store a protocol doesn't store it.

Is this also updated autmoatically when this changes? E.g. we have stored false so we keep sending base58 keys, but then the other agent sends a did:key, do we update it automatically? Or is it set once and kept like this forever?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this also updated autmoatically when this changes? E.g. we have stored false so we keep sending base58 keys, but then the other agent sends a did:key, do we update it automatically? Or is it set once and kept like this forever?

I think it depends on each protocol/service but generally the best would be to update it automatically. As of current AFJ support for mediator role, it does not add so much value because we only support Mediation Grant and Keylist Update request/response. However, considering a future support of Keylist queries and responses it's better to handle it right from the start.

So I'm adding automatic handling for both recipient and mediator in the messages we currently support. Hopefully in 0.3.x we'll support all of them 😄

}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { TagsBase } from '../../../storage/BaseRecord'
import type { HandshakeProtocol } from '../models'
import type { ConnectionMetadata } from './ConnectionMetadataTypes'

import { AriesFrameworkError } from '../../../error'
import { BaseRecord } from '../../../storage/BaseRecord'
Expand Down Expand Up @@ -39,7 +40,7 @@ export type DefaultConnectionTags = {
}

export class ConnectionRecord
extends BaseRecord<DefaultConnectionTags, CustomConnectionTags>
extends BaseRecord<DefaultConnectionTags, CustomConnectionTags, ConnectionMetadata>
implements ConnectionRecordProps
{
public state!: DidExchangeState
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/modules/dids/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ import { KeyType } from '../../crypto'
import { Key } from './domain/Key'
import { DidKey } from './methods/key'

export function isDidKey(key: string) {
return key.startsWith('did:key')
}

export function didKeyToVerkey(key: string) {
if (key.startsWith('did:key')) {
if (isDidKey(key)) {
const publicKeyBase58 = DidKey.fromDid(key).key.publicKeyBase58
return publicKeyBase58
}
return key
}

export function verkeyToDidKey(key: string) {
if (key.startsWith('did:key')) {
if (isDidKey(key)) {
return key
}
const publicKeyBase58 = key
Expand Down
44 changes: 31 additions & 13 deletions packages/core/src/modules/routing/__tests__/mediation.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { SubjectMessage } from '../../../../../../tests/transport/SubjectInboundTransport'
import type { InitConfig } from '../../../types'

import { Subject } from 'rxjs'

Expand Down Expand Up @@ -39,14 +40,7 @@ describe('mediator establishment', () => {
await senderAgent?.wallet.delete()
})

test(`Mediation end-to-end flow
1. Start mediator agent and create invitation
2. Start recipient agent with mediatorConnectionsInvite from mediator
3. Assert mediator and recipient are connected and mediation state is Granted
4. Start sender agent and create connection with recipient
5. Assert endpoint in recipient invitation for sender is mediator endpoint
6. Send basic message from sender to recipient and assert it is received on the recipient side
`, async () => {
const e2eMediationTest = async (mediatorAgentConfig: InitConfig, recipientAgentConfig: InitConfig) => {
const mediatorMessages = new Subject<SubjectMessage>()
const recipientMessages = new Subject<SubjectMessage>()
const senderMessages = new Subject<SubjectMessage>()
Expand All @@ -56,8 +50,8 @@ describe('mediator establishment', () => {
'rxjs:sender': senderMessages,
}

// Initialize mediatorReceived message
mediatorAgent = new Agent(mediatorConfig.config, recipientConfig.agentDependencies)
// Initialize mediator
mediatorAgent = new Agent(mediatorAgentConfig, mediatorConfig.agentDependencies)
mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap))
mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages))
await mediatorAgent.initialize()
Expand All @@ -66,17 +60,16 @@ describe('mediator establishment', () => {
const mediatorOutOfBandRecord = await mediatorAgent.oob.createInvitation({
label: 'mediator invitation',
handshake: true,
handshakeProtocols: [HandshakeProtocol.DidExchange],
handshakeProtocols: [HandshakeProtocol.Connections],
})

// Initialize recipient with mediation connections invitation
recipientAgent = new Agent(
{
...recipientConfig.config,
...recipientAgentConfig,
mediatorConnectionsInvite: mediatorOutOfBandRecord.outOfBandInvitation.toUrl({
domain: 'https://example.com/ssi',
}),
mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1,
},
recipientConfig.agentDependencies
)
Expand Down Expand Up @@ -136,6 +129,31 @@ describe('mediator establishment', () => {
})

expect(basicMessage.content).toBe(message)
}

test(`Mediation end-to-end flow
1. Start mediator agent and create invitation
2. Start recipient agent with mediatorConnectionsInvite from mediator
3. Assert mediator and recipient are connected and mediation state is Granted
4. Start sender agent and create connection with recipient
5. Assert endpoint in recipient invitation for sender is mediator endpoint
6. Send basic message from sender to recipient and assert it is received on the recipient side
`, async () => {
await e2eMediationTest(mediatorConfig.config, {
...recipientConfig.config,
mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1,
})
})

test('Mediation end-to-end flow (use did:key in both sides)', async () => {
await e2eMediationTest(
{ ...mediatorConfig.config, useDidKeyInProtocols: true },
{
...recipientConfig.config,
mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1,
useDidKeyInProtocols: true,
}
)
})

test('restart recipient agent and create connection through mediator after recipient agent is restarted', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Expose, Type } from 'class-transformer'
import { IsArray, ValidateNested, IsString, IsEnum, IsInstance } from 'class-validator'
import { Verkey } from 'indy-sdk'

import { AgentMessage } from '../../../agent/AgentMessage'
import { IsValidMessageType, parseMessageType } from '../../../utils/messageType'
Expand All @@ -11,7 +10,7 @@ export enum KeylistUpdateAction {
}

export class KeylistUpdate {
public constructor(options: { recipientKey: Verkey; action: KeylistUpdateAction }) {
public constructor(options: { recipientKey: string; action: KeylistUpdateAction }) {
if (options) {
this.recipientKey = options.recipientKey
this.action = options.action
Expand All @@ -20,7 +19,7 @@ export class KeylistUpdate {

@IsString()
@Expose({ name: 'recipient_key' })
public recipientKey!: Verkey
public recipientKey!: string

@IsEnum(KeylistUpdateAction)
public action!: KeylistUpdateAction
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Expose, Type } from 'class-transformer'
import { IsArray, IsEnum, IsInstance, IsString, ValidateNested } from 'class-validator'
import { Verkey } from 'indy-sdk'

import { AgentMessage } from '../../../agent/AgentMessage'
import { IsValidMessageType, parseMessageType } from '../../../utils/messageType'
Expand All @@ -15,7 +14,7 @@ export enum KeylistUpdateResult {
}

export class KeylistUpdated {
public constructor(options: { recipientKey: Verkey; action: KeylistUpdateAction; result: KeylistUpdateResult }) {
public constructor(options: { recipientKey: string; action: KeylistUpdateAction; result: KeylistUpdateResult }) {
if (options) {
this.recipientKey = options.recipientKey
this.action = options.action
Expand All @@ -25,7 +24,7 @@ export class KeylistUpdated {

@IsString()
@Expose({ name: 'recipient_key' })
public recipientKey!: Verkey
public recipientKey!: string

@IsEnum(KeylistUpdateAction)
public action!: KeylistUpdateAction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { EncryptedMessage } from '../../../types'
import type { ConnectionRecord } from '../../connections'
import type { Routing } from '../../connections/services/ConnectionService'
import type { MediationStateChangedEvent, KeylistUpdatedEvent } from '../RoutingEvents'
import type { KeylistUpdateResponseMessage, MediationDenyMessage, MediationGrantMessage } from '../messages'
import type { MediationDenyMessage } from '../messages'
import type { StatusMessage, MessageDeliveryMessage } from '../protocol'
import type { GetRoutingOptions } from './RoutingService'

Expand All @@ -21,13 +21,19 @@ import { KeyType } from '../../../crypto'
import { AriesFrameworkError } from '../../../error'
import { injectable } from '../../../plugins'
import { JsonTransformer } from '../../../utils'
import { ConnectionMetadataKeys } from '../../connections/repository/ConnectionMetadataTypes'
import { ConnectionService } from '../../connections/services/ConnectionService'
import { Key } from '../../dids'
import { didKeyToVerkey } from '../../dids/helpers'
import { didKeyToVerkey, isDidKey, verkeyToDidKey } from '../../dids/helpers'
import { ProblemReportError } from '../../problem-reports'
import { RoutingEventTypes } from '../RoutingEvents'
import { RoutingProblemReportReason } from '../error'
import { KeylistUpdateAction, MediationRequestMessage } from '../messages'
import {
KeylistUpdateAction,
KeylistUpdateResponseMessage,
MediationRequestMessage,
MediationGrantMessage,
} from '../messages'
import { KeylistUpdate, KeylistUpdateMessage } from '../messages/KeylistUpdateMessage'
import { MediationRole, MediationState } from '../models'
import { DeliveryRequestMessage, MessagesReceivedMessage, StatusRequestMessage } from '../protocol/pickup/v2/messages'
Expand Down Expand Up @@ -104,6 +110,10 @@ export class MediationRecipientService {
// Update record
mediationRecord.endpoint = messageContext.message.endpoint

// Update connection metadata to use their key format in further protocol messages
const connectionUsesDidKey = messageContext.message.routingKeys.some(isDidKey)
await this.updateUseDidKeysFlag(connection, MediationGrantMessage.type.protocolUri, connectionUsesDidKey)

// According to RFC 0211 keys should be a did key, but base58 encoded verkey was used before
// RFC was accepted. This converts the key to a public key base58 if it is a did key.
mediationRecord.routingKeys = messageContext.message.routingKeys.map(didKeyToVerkey)
Expand All @@ -122,12 +132,16 @@ export class MediationRecipientService {

const keylist = messageContext.message.updated

// Update connection metadata to use their key format in further protocol messages
const connectionUsesDidKey = keylist.some((key) => isDidKey(key.recipientKey))
await this.updateUseDidKeysFlag(connection, KeylistUpdateResponseMessage.type.protocolUri, connectionUsesDidKey)

// update keylist in mediationRecord
for (const update of keylist) {
if (update.action === KeylistUpdateAction.add) {
mediationRecord.addRecipientKey(update.recipientKey)
mediationRecord.addRecipientKey(didKeyToVerkey(update.recipientKey))
} else if (update.action === KeylistUpdateAction.remove) {
mediationRecord.removeRecipientKey(update.recipientKey)
mediationRecord.removeRecipientKey(didKeyToVerkey(update.recipientKey))
}
}

Expand All @@ -146,9 +160,18 @@ export class MediationRecipientService {
verKey: string,
timeoutMs = 15000 // TODO: this should be a configurable value in agent config
): Promise<MediationRecord> {
const message = this.createKeylistUpdateMessage(verKey)
const connection = await this.connectionService.getById(mediationRecord.connectionId)

// Use our useDidKey configuration unless we know the key formatting other party is using
let useDidKey = this.config.useDidKeyInProtocols

const useDidKeysConnectionMetadata = connection.metadata.get(ConnectionMetadataKeys.UseDidKeysForProtocol)
if (useDidKeysConnectionMetadata) {
useDidKey = useDidKeysConnectionMetadata[KeylistUpdateMessage.type.protocolUri] ?? useDidKey
}

const message = this.createKeylistUpdateMessage(useDidKey ? verkeyToDidKey(verKey) : verKey)

mediationRecord.assertReady()
mediationRecord.assertRole(MediationRole.Recipient)

Expand Down Expand Up @@ -381,6 +404,13 @@ export class MediationRecipientService {
await this.mediationRepository.update(mediationRecord)
}
}

private async updateUseDidKeysFlag(connection: ConnectionRecord, protocolUri: string, connectionUsesDidKey: boolean) {
const useDidKeysForProtocol = connection.metadata.get(ConnectionMetadataKeys.UseDidKeysForProtocol) ?? {}
useDidKeysForProtocol[protocolUri] = connectionUsesDidKey
connection.metadata.set(ConnectionMetadataKeys.UseDidKeysForProtocol, useDidKeysForProtocol)
await this.connectionService.update(connection)
}
}

export interface MediationProtocolMsgReturnType<MessageType extends AgentMessage> {
Expand Down
29 changes: 25 additions & 4 deletions packages/core/src/modules/routing/services/MediatorService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext'
import type { EncryptedMessage } from '../../../types'
import type { ConnectionRecord } from '../../connections'
import type { MediationStateChangedEvent } from '../RoutingEvents'
import type { ForwardMessage, KeylistUpdateMessage, MediationRequestMessage } from '../messages'
import type { ForwardMessage, MediationRequestMessage } from '../messages'

import { AgentConfig } from '../../../agent/AgentConfig'
import { EventEmitter } from '../../../agent/EventEmitter'
Expand All @@ -10,9 +11,12 @@ import { AriesFrameworkError } from '../../../error'
import { inject, injectable } from '../../../plugins'
import { JsonTransformer } from '../../../utils/JsonTransformer'
import { Wallet } from '../../../wallet/Wallet'
import { didKeyToVerkey } from '../../dids/helpers'
import { ConnectionService } from '../../connections'
import { ConnectionMetadataKeys } from '../../connections/repository/ConnectionMetadataTypes'
import { didKeyToVerkey, isDidKey, verkeyToDidKey } from '../../dids/helpers'
import { RoutingEventTypes } from '../RoutingEvents'
import {
KeylistUpdateMessage,
KeylistUpdateAction,
KeylistUpdateResult,
KeylistUpdated,
Expand All @@ -33,20 +37,23 @@ export class MediatorService {
private mediatorRoutingRepository: MediatorRoutingRepository
private wallet: Wallet
private eventEmitter: EventEmitter
private connectionService: ConnectionService
private _mediatorRoutingRecord?: MediatorRoutingRecord

public constructor(
mediationRepository: MediationRepository,
mediatorRoutingRepository: MediatorRoutingRepository,
agentConfig: AgentConfig,
@inject(InjectionSymbols.Wallet) wallet: Wallet,
eventEmitter: EventEmitter
eventEmitter: EventEmitter,
connectionService: ConnectionService
) {
this.mediationRepository = mediationRepository
this.mediatorRoutingRepository = mediatorRoutingRepository
this.agentConfig = agentConfig
this.wallet = wallet
this.eventEmitter = eventEmitter
this.connectionService = connectionService
}

public async initialize() {
Expand Down Expand Up @@ -114,6 +121,10 @@ export class MediatorService {
mediationRecord.assertReady()
mediationRecord.assertRole(MediationRole.Mediator)

// Update connection metadata to use their key format in further protocol messages
const connectionUsesDidKey = message.updates.some((update) => isDidKey(update.recipientKey))
await this.updateUseDidKeysFlag(connection, KeylistUpdateMessage.type.protocolUri, connectionUsesDidKey)

for (const update of message.updates) {
const updated = new KeylistUpdated({
action: update.action,
Expand Down Expand Up @@ -149,9 +160,12 @@ export class MediatorService {

await this.updateState(mediationRecord, MediationState.Granted)

// Use our useDidKey configuration, as this is the first interaction for this protocol
const useDidKey = this.agentConfig.useDidKeyInProtocols

const message = new MediationGrantMessage({
endpoint: this.agentConfig.endpoints[0],
routingKeys: this.getRoutingKeys(),
routingKeys: useDidKey ? this.getRoutingKeys().map(verkeyToDidKey) : this.getRoutingKeys(),
threadId: mediationRecord.threadId,
})

Expand Down Expand Up @@ -207,4 +221,11 @@ export class MediatorService {
},
})
}

private async updateUseDidKeysFlag(connection: ConnectionRecord, protocolUri: string, connectionUsesDidKey: boolean) {
const useDidKeysForProtocol = connection.metadata.get(ConnectionMetadataKeys.UseDidKeysForProtocol) ?? {}
useDidKeysForProtocol[protocolUri] = connectionUsesDidKey
connection.metadata.set(ConnectionMetadataKeys.UseDidKeysForProtocol, useDidKeysForProtocol)
await this.connectionService.update(connection)
}
}
Loading