Skip to content

Commit

Permalink
Implement report-problem protocol (#78)
Browse files Browse the repository at this point in the history
Signed-off-by: conanoc <conanoc@gmail.com>
  • Loading branch information
conanoc authored Dec 4, 2023
1 parent bfaf6a7 commit 3db307c
Show file tree
Hide file tree
Showing 14 changed files with 290 additions and 5 deletions.
3 changes: 2 additions & 1 deletion BasicTests.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
"LedgerServiceTest",
"OobTest\/testCreateWithOfferCredentialMessage()",
"OobTest\/testCredentialOffer()",
"ProblemReportsTest",
"ProofsTest",
"RevocationTest",
"RevocationTest"
],
"target" : {
"containerPath" : "container:",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ Aries Framework Swift supports most of [AIP 1.0](https://github.com/hyperledger/
- Does not implement alternate begining (Prover begins with proposal)
- ✅ HTTP & WebSocket Transport
- ✅ ([RFC 0434](https://github.com/hyperledger/aries-rfcs/blob/main/features/0434-outofband/README.md)) Out of Band Protocol (AIP 2.0)
- ✅ ([RFC 0035](https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md)) Report Problem Protocol

### Not supported yet
- ❌ ([RFC 0023](https://github.com/hyperledger/aries-rfcs/tree/main/features/0023-did-exchange)) DID Exchange Protocol (AIP 2.0)
- ❌ ([RFC 0035](https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md)) Report Problem Protocol
- ❌ ([RFC 0056](https://github.com/hyperledger/aries-rfcs/blob/main/features/0056-service-decorator/README.md)) Service Decorator

## Requirements & Installation
Expand Down
4 changes: 4 additions & 0 deletions Sources/AriesFramework/agent/AgentDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public protocol AgentDelegate {
func onOutOfBandStateChanged(outOfBandRecord: OutOfBandRecord)
func onCredentialStateChanged(credentialRecord: CredentialExchangeRecord)
func onProofStateChanged(proofRecord: ProofExchangeRecord)
func onProblemReportReceived(message: BaseProblemReportMessage)
}

// Default implementation of AgentDelegate
Expand All @@ -25,4 +26,7 @@ public extension AgentDelegate {

func onProofStateChanged(proofRecord: ProofExchangeRecord) {
}

func onProblemReportReceived(message: BaseProblemReportMessage) {
}
}
7 changes: 7 additions & 0 deletions Sources/AriesFramework/agent/Dispatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ public class Dispatcher {

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

public func registerHandler(handler: MessageHandler) {
handlers[handler.messageType] = handler
handlers[Dispatcher.replaceNewDidCommPrefixWithLegacyDidSov(messageType: handler.messageType)] = handler
}

private func registerProblemReportHandlers() {
registerHandler(handler: ProblemReportHandler(agent: agent, messageType: PresentationProblemReportMessage.type))
registerHandler(handler: ProblemReportHandler(agent: agent, messageType: CredentialProblemReportMessage.type))
registerHandler(handler: ProblemReportHandler(agent: agent, messageType: MediationProblemReportMessage.type))
}

func dispatch(messageContext: InboundMessageContext) async throws {
logger.debug("Dispatching message of type: \(messageContext.message.type)")
guard let handler = handlers[messageContext.message.type] else {
Expand Down
17 changes: 17 additions & 0 deletions Sources/AriesFramework/credentials/CredentialService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,23 @@ public class CredentialService {
return credentialRecord
}

/**
Create a ``ProblemReportMessage`` as response to a received credential offer.
- Parameter credentialRecordId: credential record that was declined
- Returns: credential-problem-report message.
*/
public func createOfferDeclinedProblemReport(credentialRecordId: String) async throws -> CredentialProblemReportMessage {
var credentialRecord = try await credentialExchangeRepository.getById(credentialRecordId)
try credentialRecord.assertProtocolVersion("v1")
try credentialRecord.assertState(CredentialState.OfferReceived)

let message = CredentialProblemReportMessage(threadId: credentialRecord.threadId)
try await updateState(credentialRecord: &credentialRecord, newState: .Declined)

return message
}

func getHolderDid(credentialRecord: CredentialExchangeRecord) async throws -> String {
let connection = try await agent.connectionRepository.getById(credentialRecord.connectionId)
return connection.did
Expand Down
7 changes: 4 additions & 3 deletions Sources/AriesFramework/credentials/CredentialsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ public class CredentialsCommand {
- Returns: credential record that was declined.
*/
public func declineOffer(credentialRecordId: String) async throws -> CredentialExchangeRecord {
var credentialRecord = try await agent.credentialExchangeRepository.getById(credentialRecordId)
try credentialRecord.assertState(CredentialState.OfferReceived)
try await agent.credentialService.updateState(credentialRecord: &credentialRecord, newState: .Declined)
let message = try await agent.credentialService.createOfferDeclinedProblemReport(credentialRecordId: credentialRecordId)
let credentialRecord = try await agent.credentialExchangeRepository.getById(credentialRecordId)
let connection = try await agent.connectionRepository.getById(credentialRecord.connectionId)
try await agent.messageSender.send(message: OutboundMessage(payload: message, connection: connection))

return credentialRecord
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

import Foundation
import os

class ProblemReportHandler: MessageHandler {
let agent: Agent
let messageType: String
let logger = Logger(subsystem: "AriesFramework", category: "ProblemReportHandler")

init(agent: Agent, messageType: String) {
self.agent = agent
self.messageType = messageType
}

func handle(messageContext: InboundMessageContext) async throws -> OutboundMessage? {
let message = try JSONDecoder().decode(BaseProblemReportMessage.self, from: Data(messageContext.plaintextMessage.utf8))
logger.debug("Received problem report: \(message.description.en)")
agent.agentDelegate?.onProblemReportReceived(message: message)
return nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation

public struct DescriptionOptions: Codable {
let en: String
let code: String
}

public struct FixHintOptions: Codable {
let en: String
}

public class BaseProblemReportMessage: AgentMessage {
var description: DescriptionOptions
var fixHint: FixHintOptions?

private enum CodingKeys: String, CodingKey {
case description, fixHint = "fix_hint"
}

public init(description: DescriptionOptions, fixHint: FixHintOptions? = nil, type: String) {
self.description = description
self.fixHint = fixHint
super.init(id: UUID().uuidString, type: type)
}

public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
description = try values.decode(DescriptionOptions.self, forKey: .description)
fixHint = try values.decodeIfPresent(FixHintOptions.self, forKey: .fixHint)
try super.init(from: decoder)
}

public override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(description, forKey: .description)
try container.encodeIfPresent(fixHint, forKey: .fixHint)
try super.encode(to: encoder)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

import Foundation

public class CredentialProblemReportMessage: BaseProblemReportMessage {
public static var type: String = "https://didcomm.org/issue-credential/1.0/problem-report"

public init(threadId: String) {
super.init(description: DescriptionOptions(en: "Issuance abandoned", code: "issuance-abandoned"), type: CredentialProblemReportMessage.type)
thread = ThreadDecorator(threadId: threadId)
}

public required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

import Foundation

public class MediationProblemReportMessage: BaseProblemReportMessage {
public static var type: String = "https://didcomm.org/coordinate-mediation/1.0/problem-report"

public required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

import Foundation

public class PresentationProblemReportMessage: BaseProblemReportMessage {
public static var type: String = "https://didcomm.org/present-proof/1.0/problem-report"

public init(threadId: String) {
super.init(description: DescriptionOptions(en: "Proof abandoned", code: "abandoned"), type: PresentationProblemReportMessage.type)
thread = ThreadDecorator(threadId: threadId)
}

public required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
}
16 changes: 16 additions & 0 deletions Sources/AriesFramework/proofs/ProofCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ public class ProofCommand {
return proofRecord
}

/**
Decline a presentation request as prover (by sending a presentation problem report message) to the connection
associated with the proof record.
- Parameter proofRecordId: the id of the proof record for which to decline the request.
- Returns: proof record associated with the sent presentation problem report message.
*/
public func declineRequest(proofRecordId: String) async throws -> ProofExchangeRecord {
let record = try await agent.proofRepository.getById(proofRecordId)
let (message, proofRecord) = try await agent.proofService.createPresentationDeclinedProblemReport(proofRecord: record)

let connection = try await agent.connectionRepository.getById(record.connectionId)
try await agent.messageSender.send(message: OutboundMessage(payload: message, connection: connection))
return proofRecord
}

/**
Accept a presentation as verifier (by sending a presentation acknowledgement message) to the connection
associated with the proof record.
Expand Down
15 changes: 15 additions & 0 deletions Sources/AriesFramework/proofs/ProofService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,21 @@ public class ProofService {
return proofRecord
}

/**
Create a ``PresentationProblemReportMessage`` as response to a received presentation request.
- Parameters proofRecord: the proof record for which to create the presentation problem report.
- Returns: the presentation problem report message and an associated proof record.
*/
public func createPresentationDeclinedProblemReport(proofRecord: ProofExchangeRecord) async throws -> (message: PresentationProblemReportMessage, record: ProofExchangeRecord) {
var proofRecord = proofRecord
try proofRecord.assertState(.RequestReceived)
let message = PresentationProblemReportMessage(threadId: proofRecord.threadId)
try await updateState(proofRecord: &proofRecord, newState: .Declined)

return (message, proofRecord)
}

/**
Create a ``PresentationAckMessage`` as response to a received presentation.
Expand Down
124 changes: 124 additions & 0 deletions Tests/AriesFrameworkTests/problemreports/ProblemReportsTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@

import XCTest
@testable import AriesFramework

class ProblemReportsTest: XCTestCase {
var faberAgent: Agent!
var aliceAgent: Agent!
var credDefId: String!
var faberConnection: ConnectionRecord!
var aliceConnection: ConnectionRecord!

let credentialPreview = CredentialPreview.fromDictionary([
"name": "John",
"sex": "Male",
"age": "99"
])

class TestDelegate: AgentDelegate {
let expectation: TestHelper.XCTestExpectation
let threadId: String
init(expectation: TestHelper.XCTestExpectation, threadId: String) {
self.expectation = expectation
self.threadId = threadId
}
func onProblemReportReceived(message: BaseProblemReportMessage) {
XCTAssertEqual(message.threadId, threadId)
expectation.fulfill()
}
}

override func setUp() async throws {
try await super.setUp()

(faberAgent, aliceAgent, faberConnection, aliceConnection) = try await TestHelper.setupCredentialTests()
credDefId = try await TestHelper.prepareForIssuance(faberAgent, ["name", "sex", "age"])
}

override func tearDown() async throws {
try await faberAgent?.reset()
try await aliceAgent?.reset()
try await super.tearDown()
}

func getCredentialRecord(for agent: Agent, threadId: String) async throws -> CredentialExchangeRecord {
let credentialRecord = try await agent.credentialExchangeRepository.getByThreadAndConnectionId(threadId: threadId, connectionId: nil)
return credentialRecord
}

func getProofRecord(for agent: Agent, threadId: String) async throws -> ProofExchangeRecord {
let proofRecord = try await agent.proofRepository.getByThreadAndConnectionId(threadId: threadId, connectionId: nil)
return proofRecord
}

func issueCredential() async throws {
aliceAgent.agentConfig.autoAcceptCredential = .always
faberAgent.agentConfig.autoAcceptCredential = .always

var faberCredentialRecord = try await faberAgent.credentials.offerCredential(
options: CreateOfferOptions(
connection: faberConnection,
credentialDefinitionId: credDefId,
attributes: credentialPreview.attributes,
comment: "Offer to Alice"))
try await Task.sleep(nanoseconds: UInt64(1 * SECOND)) // Need enough time to finish exchange a credential.

let threadId = faberCredentialRecord.threadId
let aliceCredentialRecord = try await getCredentialRecord(for: aliceAgent, threadId: threadId)
faberCredentialRecord = try await getCredentialRecord(for: faberAgent, threadId: threadId)

XCTAssertEqual(aliceCredentialRecord.state, .Done)
XCTAssertEqual(faberCredentialRecord.state, .Done)
}

func getProofRequest() async throws -> ProofRequest {
let attributes = ["name": ProofAttributeInfo(
name: "name", names: nil, nonRevoked: nil,
restrictions: [AttributeFilter(credentialDefinitionId: credDefId)])]
let predicates = ["age": ProofPredicateInfo(
name: "age", nonRevoked: nil, predicateType: .GreaterThanOrEqualTo, predicateValue: 50,
restrictions: [AttributeFilter(credentialDefinitionId: credDefId)])]

let nonce = try ProofService.generateProofRequestNonce()
return ProofRequest(nonce: nonce, requestedAttributes: attributes, requestedPredicates: predicates)
}

func testCredentialDeclinedProblemReport() async throws {
let faberCredentialRecord = try await faberAgent.credentials.offerCredential(
options: CreateOfferOptions(
connection: faberConnection,
credentialDefinitionId: credDefId,
attributes: credentialPreview.attributes,
comment: "Offer to Alice"))

let threadId = faberCredentialRecord.threadId
let aliceCredentialRecord = try await getCredentialRecord(for: aliceAgent, threadId: threadId)
XCTAssertEqual(aliceCredentialRecord.state, .OfferReceived)

let expectation = TestHelper.expectation(description: "Problem report received")
faberAgent.agentDelegate = TestDelegate(expectation: expectation, threadId: threadId)

_ = try await aliceAgent.credentials.declineOffer(credentialRecordId: aliceCredentialRecord.id)
try await TestHelper.wait(for: expectation, timeout: 5)
}

func testProofDeclinedProblemReport() async throws {
try await issueCredential()

let proofRequest = try await getProofRequest()
let faberProofRecord = try await faberAgent.proofs.requestProof(
connectionId: faberConnection.id,
proofRequest: proofRequest,
comment: "Request from Alice")

let threadId = faberProofRecord.threadId
let aliceProofRecord = try await getProofRecord(for: aliceAgent, threadId: threadId)
XCTAssertEqual(aliceProofRecord.state, .RequestReceived)

let expectation = TestHelper.expectation(description: "Problem report received")
faberAgent.agentDelegate = TestDelegate(expectation: expectation, threadId: threadId)

_ = try await aliceAgent.proofs.declineRequest(proofRecordId: aliceProofRecord.id)
try await TestHelper.wait(for: expectation, timeout: 5)
}
}

0 comments on commit 3db307c

Please sign in to comment.