Skip to content

Commit

Permalink
Implement DID Exchange Protocol (#102)
Browse files Browse the repository at this point in the history
Signed-off-by: conanoc <conanoc@gmail.com>
  • Loading branch information
conanoc authored Apr 18, 2024
1 parent 3caebe0 commit a86d268
Show file tree
Hide file tree
Showing 28 changed files with 821 additions and 45 deletions.
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
excluded:
- .build
- Sample
- Tests/javascript
disabled_rules:
- line_length
- function_body_length
Expand Down
5 changes: 5 additions & 0 deletions DEVELOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,8 @@ $ yarn faber
Then, get the invitation urls from the mediator and faber agent.
Run `testDemoFaber()` with these urls and operate the faber agent to issue a credential.
You can also use the Sample app to interact with the faber agent.

You can see debug messages of faber agent by adding the following option to the agent config in `BaseAgent.ts`:
```javascript
logger: new ConsoleLogger(LogLevel.debug),
```
36 changes: 36 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,24 @@
"version" : "1.1.0"
}
},
{
"identity" : "didcore-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/beatt83/didcore-swift.git",
"state" : {
"revision" : "2503b1690e11b16a0cdc8f492e370bbd9dcbe08b",
"version" : "2.0.0"
}
},
{
"identity" : "peerdid-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/beatt83/peerdid-swift",
"state" : {
"revision" : "4e82ff42aa2b53b2d361f482b607763d79ccfdbb",
"version" : "3.0.0"
}
},
{
"identity" : "semaphore",
"kind" : "remoteSourceControl",
Expand All @@ -80,6 +98,24 @@
"revision" : "e325083424688055363bbfcb7f1a440d7d7a1bae",
"version" : "1.2.2"
}
},
{
"identity" : "swift-bases",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-libp2p/swift-bases.git",
"state" : {
"revision" : "3cf27cf95d70248b0a1d99eee06cdf8b235241a8",
"version" : "0.0.3"
}
},
{
"identity" : "swift-multibase",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-libp2p/swift-multibase.git",
"state" : {
"revision" : "45f3cf2844477b9d211e1d3e793d0853134fd942",
"version" : "0.0.2"
}
}
],
"version" : 2
Expand Down
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ let package = Package(
.package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit", exact: "0.2.0"),
.package(url: "https://github.com/keefertaylor/Base58Swift", exact: "2.1.7"),
.package(url: "https://github.com/thecatalinstan/Criollo", exact: "1.1.0"),
.package(url: "https://github.com/groue/Semaphore", exact: "0.0.8")
.package(url: "https://github.com/groue/Semaphore", exact: "0.0.8"),
.package(url: "https://github.com/beatt83/peerdid-swift", exact: "3.0.0")
],
targets: [
.target(
Expand All @@ -28,6 +29,7 @@ let package = Package(
.product(name: "Askar", package: "aries-uniffi-wrappers"),
.product(name: "IndyVdr", package: "aries-uniffi-wrappers"),
.product(name: "WebSockets", package: "concurrent-ws"),
.product(name: "PeerDID", package: "peerdid-swift"),
"CollectionConcurrencyKit",
"Base58Swift",
"Semaphore"
Expand Down
4 changes: 4 additions & 0 deletions Sources/AriesFramework/agent/Agent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public class Agent {
var mediationRecipient: MediationRecipient!
public var connectionRepository: ConnectionRepository!
public var connectionService: ConnectionService!
public var didExchangeService: DidExchangeService!
public var peerDIDService: PeerDIDService!
var messageSender: MessageSender!
var messageReceiver: MessageReceiver!
public var dispatcher: Dispatcher!
Expand Down Expand Up @@ -42,6 +44,8 @@ public class Agent {
self.wallet = Wallet(agent: self)
self.connectionRepository = ConnectionRepository(agent: self)
self.connectionService = ConnectionService(agent: self)
self.didExchangeService = DidExchangeService(agent: self)
self.peerDIDService = PeerDIDService(agent: self)
self.messageSender = MessageSender(agent: self)
self.messageReceiver = MessageReceiver(agent: self)
self.dispatcher = Dispatcher(agent: self)
Expand Down
4 changes: 4 additions & 0 deletions Sources/AriesFramework/agent/AgentConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public struct AgentConfig: Codable {
ignoreRevocationCheck: Bool = false,
useLedgerService: Bool = true,
useLegacyDidSovPrefix: Bool = true,
preferredHandshakeProtocol: HandshakeProtocol = .Connections,
publicDidSeed: String? = nil,
agentEndpoints: [String]? = nil) {

Expand All @@ -38,6 +39,7 @@ public struct AgentConfig: Codable {
self.ignoreRevocationCheck = ignoreRevocationCheck
self.useLedgerService = useLedgerService
self.useLegacyDidSovPrefix = useLegacyDidSovPrefix
self.preferredHandshakeProtocol = preferredHandshakeProtocol
self.publicDidSeed = publicDidSeed
self.agentEndpoints = agentEndpoints
}
Expand Down Expand Up @@ -75,6 +77,8 @@ public struct AgentConfig: Codable {
public var useLedgerService: Bool
/// Whether to use the legacy did sov prefix. Default is true.
public var useLegacyDidSovPrefix: Bool
/// The preferred handshake protocol to use. Default is `.Connections`.
public var preferredHandshakeProtocol: HandshakeProtocol

// For testing

Expand Down
23 changes: 18 additions & 5 deletions Sources/AriesFramework/connection/ConnectionCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ public class ConnectionCommand {
dispatcher.registerHandler(handler: ConnectionRequestHandler(agent: agent))
dispatcher.registerHandler(handler: ConnectionResponseHandler(agent: agent))
dispatcher.registerHandler(handler: TrustPingMessageHandler(agent: agent))
dispatcher.registerHandler(handler: DidExchangeRequestHandler(agent: agent))
dispatcher.registerHandler(handler: DidExchangeResponseHandler(agent: agent))
dispatcher.registerHandler(handler: DidExchangeCompleteHandler(agent: agent))
}

/**
Expand Down Expand Up @@ -112,13 +115,23 @@ public class ConnectionCommand {
return message.connection
}

func acceptOutOfBandInvitation(outOfBandRecord: OutOfBandRecord, config: ReceiveOutOfBandInvitationConfig?) async throws -> ConnectionRecord {
func acceptOutOfBandInvitation(
outOfBandRecord: OutOfBandRecord,
handshakeProtocol: HandshakeProtocol,
config: ReceiveOutOfBandInvitationConfig?) async throws -> ConnectionRecord {
let connection = try await receiveInvitation(outOfBandInvitation: outOfBandRecord.outOfBandInvitation,
autoAcceptConnection: false, alias: config?.alias)
let message = try await agent.connectionService.createRequest(connectionId: connection.id,
label: config?.label,
imageUrl: config?.imageUrl,
autoAcceptConnection: config?.autoAcceptConnection)
let message: OutboundMessage
if handshakeProtocol == .Connections {
message = try await agent.connectionService.createRequest(connectionId: connection.id,
label: config?.label,
imageUrl: config?.imageUrl,
autoAcceptConnection: config?.autoAcceptConnection)
} else {
message = try await agent.didExchangeService.createRequest(connectionId: connection.id,
label: config?.label,
autoAcceptConnection: config?.autoAcceptConnection)
}
try await agent.messageSender.send(message: message)
return message.connection
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/AriesFramework/connection/ConnectionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public class ConnectionService {
return connectionRecord
}

private func createConnection(
func createConnection(
role: ConnectionRole,
state: ConnectionState,
invitation: ConnectionInvitationMessage? = nil,
Expand Down
201 changes: 201 additions & 0 deletions Sources/AriesFramework/connection/DidExchangeService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@

import Foundation
import os

public class DidExchangeService {
let agent: Agent
let connectionRepository: ConnectionRepository
let connectionWaiter = AsyncWaiter()
let logger = Logger(subsystem: "AriesFramework", category: "DidExchangeService")

init(agent: Agent) {
self.agent = agent
self.connectionRepository = agent.connectionRepository
}

/**
Create a DID exchange request message for the connection with the specified connection id.
- Parameters:
- connectionId: the id of the connection for which to create a DID exchange request.
- label: the label to use for the DID exchange request.
- autoAcceptConnection: whether to automatically accept the DID exchange response.
- Returns: outbound message containing DID exchange request.
*/
public func createRequest(
connectionId: String,
label: String? = nil,
autoAcceptConnection: Bool? = nil) async throws -> OutboundMessage {

var connectionRecord = try await self.connectionRepository.getById(connectionId)
assert(connectionRecord.state == ConnectionState.Invited)
assert(connectionRecord.role == ConnectionRole.Invitee)

let peerDid = try await agent.peerDIDService.createPeerDID(verkey: connectionRecord.verkey)
logger.debug("Created peer DID for a RequestMessage: \(peerDid)")
let message = DidExchangeRequestMessage(label: label ?? agent.agentConfig.label, did: peerDid)

if autoAcceptConnection != nil {
connectionRecord.autoAcceptConnection = autoAcceptConnection
}
connectionRecord.threadId = message.id
connectionRecord.did = peerDid
try await updateState(connectionRecord: &connectionRecord, newState: ConnectionState.Requested)

return OutboundMessage(payload: message, connection: connectionRecord)
}

/**
Process a received DID exchange request message. This will not accept the DID exchange request
or send a DID exchange response message. It will only update the existing connection record
with all the new information from the DID exchange request message. Use ``createResponse(connectionId:)``
after calling this function to create a DID exchange response.
- Parameter messageContext: the message context containing the DID exchange request message.
- Returns: updated connection record.
*/
public func processRequest(messageContext: InboundMessageContext) async throws -> ConnectionRecord {
let decoder = JSONDecoder()
let message = try decoder.decode(DidExchangeRequestMessage.self, from: Data(messageContext.plaintextMessage.utf8))

guard let recipientKey = messageContext.recipientVerkey else {
throw AriesFrameworkError.frameworkError("Unable to process connection request without recipientVerkey")
}

var outOfBandRecord: OutOfBandRecord?
let outOfBandRecords = await agent.outOfBandService.findAllByInvitationKey(recipientKey)
if outOfBandRecords.isEmpty {
throw AriesFrameworkError.frameworkError("No out-of-band record or connection record found for invitation key: \(recipientKey)")
} else {
outOfBandRecord = outOfBandRecords[0]
}

let didDoc = try agent.peerDIDService.parsePeerDID(message.did)
var connectionRecord = try await agent.connectionService.createConnection(
role: .Inviter,
state: .Invited,
invitation: nil,
outOfBandInvitation: outOfBandRecord!.outOfBandInvitation,
alias: nil,
routing: agent.mediationRecipient.getRouting(),
theirLabel: message.label,
autoAcceptConnection: outOfBandRecord!.autoAcceptConnection,
multiUseInvitation: true,
imageUrl: nil,
threadId: message.threadId)

try await self.connectionRepository.save(connectionRecord)

connectionRecord.theirDidDoc = didDoc
connectionRecord.theirLabel = message.label
connectionRecord.threadId = message.id
connectionRecord.theirDid = didDoc.id

if connectionRecord.theirKey() == nil {
throw AriesFrameworkError.frameworkError("Connection with id \(connectionRecord.id) has no recipient keys.")
}

try await updateState(connectionRecord: &connectionRecord, newState: .Requested)
return connectionRecord
}

/**
Create a DID exchange response message for the connection with the specified connection id.
- Parameter connectionId: the id of the connection for which to create a DID exchange response.
- Returns: outbound message containing DID exchange response.
*/
public func createResponse(connectionId: String) async throws -> OutboundMessage {
var connectionRecord = try await connectionRepository.getById(connectionId)
assert(connectionRecord.state == ConnectionState.Requested)
assert(connectionRecord.role == ConnectionRole.Inviter)
guard let threadId = connectionRecord.threadId else {
throw AriesFrameworkError.frameworkError("Connection record with id \(connectionRecord.id) has no thread id.")
}

let peerDid = try await agent.peerDIDService.createPeerDID(verkey: connectionRecord.verkey)
connectionRecord.did = peerDid

let message = DidExchangeResponseMessage(threadId: threadId, did: peerDid)
message.thread = ThreadDecorator(threadId: threadId)

try await updateState(connectionRecord: &connectionRecord, newState: .Responded)

return OutboundMessage(payload: message, connection: connectionRecord)
}

/**
Process a received DID exchange response message. This will not accept the DID exchange response
or send a DID exchange complete message. It will only update the existing connection record
with all the new information from the DID exchange response message. Use ``createComplete(connectionId:)``
after calling this function to create a DID exchange complete message.
- Parameter messageContext: the message context containing a DID exchange response message.
- Returns: updated connection record.
*/
public func processResponse(messageContext: InboundMessageContext) async throws -> ConnectionRecord {
let decoder = JSONDecoder()
let message = try decoder.decode(DidExchangeResponseMessage.self, from: Data(messageContext.plaintextMessage.utf8))

var connectionRecord: ConnectionRecord!
do {
connectionRecord = try await agent.connectionService.getByThreadId(message.threadId)
} catch {
throw AriesFrameworkError.frameworkError("Unable to process DID exchange response: connection for threadId: \(message.threadId) not found")
}
assert(connectionRecord.state == ConnectionState.Requested)
assert(connectionRecord.role == ConnectionRole.Invitee)

if message.threadId != connectionRecord.threadId {
throw AriesFrameworkError.frameworkError("Invalid or missing thread ID")
}

let didDoc = try agent.peerDIDService.parsePeerDID(message.did)
connectionRecord.theirDid = didDoc.id
connectionRecord.theirDidDoc = didDoc
// TODO: Verify the signature of message.didRotate attachment

try await updateState(connectionRecord: &connectionRecord, newState: ConnectionState.Responded)
return connectionRecord
}

/**
Create a DID exchange complete message for the connection with the specified connection id.
- Parameter connectionId: the id of the connection for which to create a DID exchange complete message.
- Returns: outbound message containing a DID exchange complete message.
*/
public func createComplete(connectionId: String) async throws -> OutboundMessage {
var connectionRecord = try await self.connectionRepository.getById(connectionId)
assert(connectionRecord.state == ConnectionState.Responded)

guard let threadId = connectionRecord.threadId else {
throw AriesFrameworkError.frameworkError("Connection record with id \(connectionRecord.id) has no thread id.")
}
guard let parentThreadId = connectionRecord.outOfBandInvitation?.id else {
throw AriesFrameworkError.frameworkError("Connection record with id \(connectionRecord.id) has no parent thread id.")
}

let message = DidExchangeCompleteMessage(threadId: threadId, parentThreadId: parentThreadId)
try await updateState(connectionRecord: &connectionRecord, newState: ConnectionState.Complete)

return OutboundMessage(payload: message, connection: connectionRecord)
}

func updateState(connectionRecord: inout ConnectionRecord, newState: ConnectionState) async throws {
connectionRecord.state = newState
try await self.connectionRepository.update(connectionRecord)
if newState == ConnectionState.Complete {
finishConnectionWaiter()
}
agent.agentDelegate?.onConnectionStateChanged(connectionRecord: connectionRecord)
}

func waitForConnection() async throws -> Bool {
return try await connectionWaiter.wait()
}

private func finishConnectionWaiter() {
connectionWaiter.finish()
}
}
Loading

0 comments on commit a86d268

Please sign in to comment.