Skip to content

Commit

Permalink
Support connection-less credential exchange and proof exchange. (#112)
Browse files Browse the repository at this point in the history
Signed-off-by: kukgini <kukgini@gmail.com>
  • Loading branch information
kukgini authored Jun 12, 2024
1 parent f3a19ee commit b2d63db
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 68 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ jobs:
- name: Build
run: swift build
- name: Run tests
run: swift test --skip AgentTest --skip CredentialsTest --skip LedgerServiceTest --skip OobTest --skip ProofsTest --skip RevocationTest --skip testCredentialDeclinedProblemReport --skip testProofDeclinedProblemReport | xcpretty && exit ${PIPESTATUS[0]}
run: swift test --skip AgentTest --skip CredentialsTest --skip LedgerServiceTest --skip OobTest --skip ProofsTest --skip RevocationTest --skip testCredentialDeclinedProblemReport --skip testProofDeclinedProblemReport --skip ConnectionlessExchangeTest | xcpretty && exit ${PIPESTATUS[0]}
52 changes: 51 additions & 1 deletion Sources/AriesFramework/agent/MessageReceiver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public class MessageReceiver {
func receiveMessage(_ encryptedMessage: EncryptedMessage) async throws {
do {
let decryptedMessage = try await agent.wallet.unpack(encryptedMessage: encryptedMessage)
let connection = try await findConnectionByMessageKeys(decryptedMessage: decryptedMessage)
let message = try MessageReceiver.decodeAgentMessage(plaintextMessage: decryptedMessage.plaintextMessage)
let connection = try await findConnection(decryptedMessage: decryptedMessage, message: message)
let messageContext = InboundMessageContext(message: message,
plaintextMessage: decryptedMessage.plaintextMessage,
connection: connection,
Expand All @@ -40,12 +40,62 @@ public class MessageReceiver {
}
}

func findConnection(decryptedMessage: DecryptedMessageContext, message: AgentMessage) async throws -> ConnectionRecord? {
var connection = try await findConnectionByMessageKeys(decryptedMessage: decryptedMessage)
if connection == nil {
connection = try await findConnectionByMessageThreadId(message: message)
if connection != nil {
// If a connection is found by the message thread id, then this message is
// a connection-less exchange and recipient is the oob inviter.
// To be able to respond to the sender, sender's information should be updated
// based on incomming message because the oob inviter created a fake connection.
try await updateConnectionTheirDidDoc(&connection!, senderKey: decryptedMessage.senderKey)
}
}
return connection
}

func findConnectionByMessageKeys(decryptedMessage: DecryptedMessageContext) async throws -> ConnectionRecord? {
let connectionRecord = try await agent.connectionService.findByKeys(senderKey: decryptedMessage.senderKey ?? "",
recipientKey: decryptedMessage.recipientKey ?? "")
return connectionRecord
}

func findConnectionByMessageThreadId(message: AgentMessage) async throws -> ConnectionRecord? {
guard let pthId = message.thread?.parentThreadId else {
return nil
}
guard let oobRecord = try await agent.outOfBandService.findByInvitationId(pthId) else {
return nil
}
guard let invitationKey = try oobRecord.outOfBandInvitation.invitationKey() else {
return nil
}
let connection = await agent.connectionService.findByInvitationKey(invitationKey)
return connection
}

func updateConnectionTheirDidDoc(_ connection: inout ConnectionRecord, senderKey: String?) async throws {
// TODO: get this from the message's service decorator if exists.
guard let senderKey = senderKey else {
return
}
let service = DidCommService(
id: "\(connection.id)#1",
serviceEndpoint: DID_COMM_TRANSPORT_QUEUE,
recipientKeys: [senderKey]
).asDidDocService()

var theirDidDoc = DidDoc(
id: senderKey,
publicKey: [],
service: [service],
authentication: []
)
connection.theirDidDoc = theirDidDoc
try await agent.connectionRepository.update(connection)
}

static func decodeAgentMessage(plaintextMessage: String) throws -> AgentMessage {
let data = plaintextMessage.data(using: .utf8)!
let decoder = JSONDecoder()
Expand Down
33 changes: 21 additions & 12 deletions Sources/AriesFramework/connection/ConnectionCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,31 @@ public class ConnectionCommand {

func acceptOutOfBandInvitation(
outOfBandRecord: OutOfBandRecord,
handshakeProtocol: HandshakeProtocol,
handshakeProtocol: HandshakeProtocol?,
config: ReceiveOutOfBandInvitationConfig?) async throws -> ConnectionRecord {
let connection = try await receiveInvitation(outOfBandInvitation: outOfBandRecord.outOfBandInvitation,
var connection = try await receiveInvitation(outOfBandInvitation: outOfBandRecord.outOfBandInvitation,
autoAcceptConnection: false, alias: config?.alias)
let message: OutboundMessage
if handshakeProtocol == .Connections {
message = try await agent.connectionService.createRequest(connectionId: connection.id,
label: config?.label,
imageUrl: config?.imageUrl,
autoAcceptConnection: config?.autoAcceptConnection)
if let handshakeProtocol = handshakeProtocol {
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
} else {
message = try await agent.didExchangeService.createRequest(connectionId: connection.id,
label: config?.label,
autoAcceptConnection: config?.autoAcceptConnection)
let didDocServices = try outOfBandRecord.outOfBandInvitation.services.compactMap { try $0.asDidDocService() }
let theirDidDoc = try connection.theirDidDoc ?? DidDoc(from: didDocServices)
connection.theirDidDoc = theirDidDoc
try await agent.connectionService.updateState(connectionRecord: &connection, newState: .Complete)
return connection

}
try await agent.messageSender.send(message: message)
return message.connection
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ public struct DidCommService: Codable {
var accept: [String]?
var priority: Int? = 0
}

public extension DidCommService {
func asDidDocService() -> DidDocService {
return DidDocService.didComm(self)
}
}
15 changes: 15 additions & 0 deletions Sources/AriesFramework/connection/models/didauth/DidDoc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,18 @@ extension DidDoc {
}
}
}

extension DidDoc {
public init(from: [DidDocService]) throws {
guard let service = from.first else {
throw AriesFrameworkError.frameworkError("new DidDoc from a DidDocService failed. services is empty.")
}
guard let key = service.recipientKeys.first else {
throw AriesFrameworkError.frameworkError("new DidDoc from a DidDocService failed. service has no recipientKeys.")
}
self.id = key
self.service = from
self.publicKey = []
self.authentication = []
}
}
114 changes: 64 additions & 50 deletions Sources/AriesFramework/oob/OutOfBandCommand.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// swiftlint:disable cyclomatic_complexity

import Foundation
import os
Expand Down Expand Up @@ -87,9 +86,25 @@ public class OutOfBandCommand {
try messages.forEach { message in
try outOfBandInvitation.addRequest(message: message)
}
if handshake == false {
// For connection-less exchange. Create a fake connection in inviter side.
let connectionRecord = try await agent.connectionService.createConnection(
role: .Inviter,
state: .Complete,
outOfBandInvitation: outOfBandInvitation,
alias: nil,
routing: routing,
theirLabel: nil,
autoAcceptConnection: true,
multiUseInvitation: false,
tags: nil,
imageUrl: nil,
threadId: nil)
try await agent.connectionRepository.save(connectionRecord)
}
}

let outOfBandRecord = OutOfBandRecord(
var outOfBandRecord = OutOfBandRecord(
id: OutOfBandRecord.generateId(),
createdAt: Date(),
outOfBandInvitation: outOfBandInvitation,
Expand Down Expand Up @@ -231,63 +246,59 @@ public class OutOfBandCommand {

let messages = try outOfBandRecord.outOfBandInvitation.getRequests()
let handshakeProtocols = outOfBandRecord.outOfBandInvitation.handshakeProtocols ?? []
if handshakeProtocols.count > 0 {
var connectionRecord: ConnectionRecord?
if existingConnection != nil && config?.reuseConnection ?? true {
if messages.count > 0 {
logger.debug("Skip handshake and reuse existing connection \(existingConnection!.id)")

var connectionRecord: ConnectionRecord?
if existingConnection != nil && config?.reuseConnection ?? true {
if messages.count > 0 {
logger.debug("Skip handshake and reuse existing connection \(existingConnection!.id)")
connectionRecord = existingConnection
} else {
logger.debug("Start handshake to reuse connection.")
let isHandshakeReuseSuccessful = try await handleHandshakeReuse(outOfBandRecord: outOfBandRecord, connectionRecord: existingConnection!)
if isHandshakeReuseSuccessful {
connectionRecord = existingConnection
} else {
logger.debug("Start handshake to reuse connection.")
let isHandshakeReuseSuccessful = try await handleHandshakeReuse(outOfBandRecord: outOfBandRecord, connectionRecord: existingConnection!)
if isHandshakeReuseSuccessful {
connectionRecord = existingConnection
} else {
logger.warning("Handshake reuse failed. Not using existing connection \(existingConnection!.id)")
}
logger.warning("Handshake reuse failed. Not using existing connection \(existingConnection!.id)")
}
}
}

let handshakeProtocol = try selectHandshakeProtocol(handshakeProtocols)
if connectionRecord == nil {
logger.debug("Creating new connection.")
connectionRecord = try await agent.connections.acceptOutOfBandInvitation(
outOfBandRecord: outOfBandRecord,
handshakeProtocol: handshakeProtocol,
config: config)
}
let handshakeProtocol = try selectHandshakeProtocol(handshakeProtocols)
if connectionRecord == nil {
logger.debug("Creating new connection.")
connectionRecord = try await agent.connections.acceptOutOfBandInvitation(
outOfBandRecord: outOfBandRecord,
handshakeProtocol: handshakeProtocol,
config: config)
}

if try await agent.connectionService.fetchState(connectionRecord: connectionRecord!) != .Complete {
var result = false
if handshakeProtocol == .Connections {
result = try await agent.connectionService.waitForConnection()
} else {
result = try await agent.didExchangeService.waitForConnection()
}
if !result {
throw AriesFrameworkError.frameworkError("Connection timed out.")
}
}
connectionRecord = try await agent.connectionRepository.getById(connectionRecord!.id)
if !outOfBandRecord.reusable {
try await agent.outOfBandService.updateState(outOfBandRecord: &outOfBandRecord, newState: .Done)
}
if handshakeProtocol != nil {
try await waitForConnection(connection: connectionRecord!, handshakeProtocol: handshakeProtocol!)
}
connectionRecord = try await agent.connectionRepository.getById(connectionRecord!.id)
if !outOfBandRecord.reusable {
try await agent.outOfBandService.updateState(outOfBandRecord: &outOfBandRecord, newState: .Done)
}

if messages.count > 0 {
try await processMessages(messages, connectionRecord: connectionRecord!)
if messages.count > 0 {
try await processMessages(messages, connectionRecord: connectionRecord!)
}
return (outOfBandRecord, connectionRecord)
}

private func waitForConnection(connection: ConnectionRecord, handshakeProtocol: HandshakeProtocol) async throws {
if try await agent.connectionService.fetchState(connectionRecord: connection) != .Complete {
var result = false
switch handshakeProtocol {
case .Connections:
result = try await agent.connectionService.waitForConnection()
case .DidExchange10, .DidExchange11:
result = try await agent.didExchangeService.waitForConnection()
}
return (outOfBandRecord, connectionRecord)
} else if messages.count > 0 {
logger.debug("Out of band message contains only request messages.")
if existingConnection != nil {
try await processMessages(messages, connectionRecord: existingConnection!)
} else {
// TODO: send message to the service endpoint
throw AriesFrameworkError.frameworkError("Cannot process request messages. No connection found.")
if !result {
throw AriesFrameworkError.frameworkError("Connection timed out.")
}
}

return (outOfBandRecord, nil)
}

private func processMessages(_ messages: [String], connectionRecord: ConnectionRecord) async throws {
Expand Down Expand Up @@ -351,7 +362,10 @@ public class OutOfBandCommand {
})
}

private func selectHandshakeProtocol(_ handshakeProtocols: [HandshakeProtocol]) throws -> HandshakeProtocol {
private func selectHandshakeProtocol(_ handshakeProtocols: [HandshakeProtocol]) throws -> HandshakeProtocol? {
if handshakeProtocols.isEmpty {
return nil
}
let supportedProtocols = getSupportedHandshakeProtocols()
if handshakeProtocols.contains(agent.agentConfig.preferredHandshakeProtocol) &&
supportedProtocols.contains(agent.agentConfig.preferredHandshakeProtocol) {
Expand Down
8 changes: 4 additions & 4 deletions Sources/AriesFramework/proofs/ProofService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@ public class ProofService {
*/
public func createRequest(
proofRequest: ProofRequest,
connectionRecord: ConnectionRecord,
connectionRecord: ConnectionRecord? = nil,
comment: String? = nil,
autoAcceptProof: AutoAcceptProof? = nil
) async throws -> (message: RequestPresentationMessage, record: ProofExchangeRecord) {
try connectionRecord.assertReady()
try connectionRecord?.assertReady()

let proofRequestJson = try JSONEncoder().encode(proofRequest)
let attachment = Attachment.fromData(proofRequestJson, id: RequestPresentationMessage.INDY_PROOF_REQUEST_ATTACHMENT_ID)
let message = RequestPresentationMessage(comment: comment, requestPresentationAttachments: [attachment])

let proofRecord = ProofExchangeRecord(
connectionId: connectionRecord.id,
connectionId: connectionRecord?.id ?? "proof-request",
threadId: message.threadId,
state: .RequestSent,
autoAcceptProof: autoAcceptProof)
Expand Down Expand Up @@ -136,7 +136,7 @@ public class ProofService {

var proofRecord = try await agent.proofRepository.getByThreadAndConnectionId(
threadId: presentationMessage.threadId,
connectionId: connection.id)
connectionId: nil)
try proofRecord.assertState(.RequestSent)

let indyProofJson = try presentationMessage.indyProof()
Expand Down
Loading

0 comments on commit b2d63db

Please sign in to comment.