Skip to content

Commit

Permalink
Support connection-less exchange (#31)
Browse files Browse the repository at this point in the history
Signed-off-by: conanoc <conanoc@gmail.com>
  • Loading branch information
conanoc authored Jun 18, 2024
1 parent 7a8b436 commit ebf6bc3
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package org.hyperledger.ariesframework.connectionless

import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.test.runTest
import org.hyperledger.ariesframework.TestHelper
import org.hyperledger.ariesframework.agent.Agent
import org.hyperledger.ariesframework.agent.SubjectOutboundTransport
import org.hyperledger.ariesframework.connection.models.ConnectionState
import org.hyperledger.ariesframework.credentials.models.AutoAcceptCredential
import org.hyperledger.ariesframework.credentials.models.CreateOfferOptions
import org.hyperledger.ariesframework.credentials.models.CredentialPreview
import org.hyperledger.ariesframework.credentials.models.CredentialState
import org.hyperledger.ariesframework.oob.models.CreateOutOfBandInvitationConfig
import org.hyperledger.ariesframework.proofs.ProofService
import org.hyperledger.ariesframework.proofs.models.AttributeFilter
import org.hyperledger.ariesframework.proofs.models.AutoAcceptProof
import org.hyperledger.ariesframework.proofs.models.PredicateType
import org.hyperledger.ariesframework.proofs.models.ProofAttributeInfo
import org.hyperledger.ariesframework.proofs.models.ProofPredicateInfo
import org.hyperledger.ariesframework.proofs.models.ProofRequest
import org.hyperledger.ariesframework.proofs.models.ProofState
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import kotlin.time.Duration.Companion.seconds

class ConnectionlessExchangeTest {
lateinit var issuerAgent: Agent
lateinit var holderAgent: Agent
lateinit var verifierAgent: Agent

lateinit var credDefId: String

val credentialPreview = CredentialPreview.fromDictionary(mapOf("name" to "John", "age" to "99"))
val context = InstrumentationRegistry.getInstrumentation().targetContext

@Before
fun setUp() = runTest(timeout = 30.seconds) {
val issuerConfig = TestHelper.getBaseConfig("issuer", true)
issuerConfig.autoAcceptCredential = AutoAcceptCredential.Always
issuerAgent = Agent(context, issuerConfig)

val holderConfig = TestHelper.getBaseConfig("holder", true)
holderConfig.autoAcceptCredential = AutoAcceptCredential.Always
holderConfig.autoAcceptProof = AutoAcceptProof.Always
holderAgent = Agent(context, holderConfig)

val verifierConfig = TestHelper.getBaseConfig("verifier", true)
verifierConfig.autoAcceptProof = AutoAcceptProof.Always
verifierAgent = Agent(context, verifierConfig)

issuerAgent.initialize()
holderAgent.initialize()
verifierAgent.initialize()

credDefId = TestHelper.prepareForIssuance(issuerAgent, listOf("name", "age"))
}

@After
fun tearDown() = runTest {
issuerAgent.reset()
holderAgent.reset()
verifierAgent.reset()
}

@Test @LargeTest
fun testConnectionlessExchange() = runTest {
issuerAgent.setOutboundTransport(SubjectOutboundTransport(holderAgent))
holderAgent.setOutboundTransport(SubjectOutboundTransport(issuerAgent))

val offerOptions = CreateOfferOptions(
connection = null,
credentialDefinitionId = credDefId,
attributes = credentialPreview.attributes,
comment = "credential-offer for test",
)
val (message, record) = issuerAgent.credentialService.createOffer(offerOptions)
validateState(issuerAgent, record.threadId, CredentialState.OfferSent)

val oobConfig = CreateOutOfBandInvitationConfig(
label = "issuer-to-holder-invitation",
alias = "issuer-to-holder-invitation",
handshake = false,
messages = listOf(message),
multiUseInvitation = false,
autoAcceptConnection = true,
)
val oobInvitation = issuerAgent.oob.createInvitation(oobConfig)

val (oob, connection) = holderAgent.oob.receiveInvitation(oobInvitation.outOfBandInvitation)
assertNotNull(connection)
assertEquals(connection?.state, ConnectionState.Complete)
assertNotNull(oob)

validateState(holderAgent, record.threadId, CredentialState.Done)
validateState(issuerAgent, record.threadId, CredentialState.Done)

// credential exchange done.

holderAgent.setOutboundTransport(SubjectOutboundTransport(verifierAgent))
verifierAgent.setOutboundTransport(SubjectOutboundTransport(holderAgent))

val proofRequest = getProofRequest()
val (proofRequestMessage, proofExchangeRecord) = verifierAgent.proofService.createRequest(
proofRequest,
)
validateState(verifierAgent, proofExchangeRecord.threadId, ProofState.RequestSent)

val oobConfigForProofExchange = CreateOutOfBandInvitationConfig(
label = "verifier-to-holder-invitation",
alias = "verifier-to-holder-invitation",
handshake = false,
messages = listOf(proofRequestMessage),
multiUseInvitation = false,
autoAcceptConnection = true,
)
val oobInvitationForProofExchange =
verifierAgent.oob.createInvitation(oobConfigForProofExchange)

val (oobForProofExchange, connectionForProofExchange) = holderAgent.oob.receiveInvitation(
oobInvitationForProofExchange.outOfBandInvitation,
)
assertNotNull(connectionForProofExchange)
assertEquals(connectionForProofExchange?.state, ConnectionState.Complete)
assertNotNull(oobForProofExchange)

validateState(holderAgent, proofExchangeRecord.threadId, ProofState.Done)
validateState(verifierAgent, proofExchangeRecord.threadId, ProofState.Done)
}

private suspend fun validateState(agent: Agent, threadId: String, state: CredentialState) {
val record = agent.credentialExchangeRepository.getByThreadAndConnectionId(threadId, null)
assertEquals(record.state, state)
}

private suspend fun validateState(agent: Agent, threadId: String, state: ProofState) {
val record = agent.proofRepository.getByThreadAndConnectionId(threadId, null)
assertEquals(record.state, state)
}

private suspend fun getProofRequest(): ProofRequest {
val attributes = mapOf(
"name" to ProofAttributeInfo(
name = "name",
restrictions = listOf(AttributeFilter(credentialDefinitionId = credDefId)),
),
)
val predicates = mapOf(
"age" to ProofPredicateInfo(
name = "age",
predicateType = PredicateType.GreaterThanOrEqualTo,
predicateValue = 50,
restrictions = listOf(AttributeFilter(credentialDefinitionId = credDefId)),
),
)
val nonce = ProofService.generateProofRequestNonce()
return ProofRequest(nonce = nonce, requestedAttributes = attributes, requestedPredicates = predicates)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable
import org.hyperledger.ariesframework.credentials.models.AutoAcceptCredential
import org.hyperledger.ariesframework.oob.models.HandshakeProtocol
import org.hyperledger.ariesframework.proofs.models.AutoAcceptProof
import org.hyperledger.ariesframework.routing.Routing

@Serializable
enum class MediatorPickupStrategy {
Expand Down Expand Up @@ -63,5 +64,5 @@ data class AgentConfig(
var preferredHandshakeProtocol: HandshakeProtocol = HandshakeProtocol.Connections,
) {
val endpoints: List<String>
get() = agentEndpoints ?: listOf("didcomm:transport/queue")
get() = agentEndpoints ?: listOf(Routing.DID_COMM_TRANSPORT_QUEUE)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package org.hyperledger.ariesframework.agent
import org.hyperledger.ariesframework.DecryptedMessageContext
import org.hyperledger.ariesframework.EncryptedMessage
import org.hyperledger.ariesframework.InboundMessageContext
import org.hyperledger.ariesframework.connection.models.didauth.DidCommService
import org.hyperledger.ariesframework.connection.models.didauth.DidDoc
import org.hyperledger.ariesframework.connection.repository.ConnectionRecord
import org.hyperledger.ariesframework.routing.Routing
import org.slf4j.LoggerFactory

class MessageReceiver(val agent: Agent) {
Expand All @@ -12,8 +15,8 @@ class MessageReceiver(val agent: Agent) {
suspend fun receiveMessage(encryptedMessage: EncryptedMessage) {
try {
val decryptedMessage = agent.wallet.unpack(encryptedMessage)
val connection = findConnectionByMessageKeys(decryptedMessage)
val message = MessageSerializer.decodeFromString(decryptedMessage.plaintextMessage)
val connection = findConnection(decryptedMessage, message)
val messageContext = InboundMessageContext(
message,
decryptedMessage.plaintextMessage,
Expand Down Expand Up @@ -43,6 +46,44 @@ class MessageReceiver(val agent: Agent) {
}
}

private suspend fun findConnection(decryptedMessage: DecryptedMessageContext, message: AgentMessage): ConnectionRecord? {
var connection = findConnectionByMessageKeys(decryptedMessage)
if (connection == null) {
connection = findConnectionByMessageThreadId(message)
if (connection != null) {
updateConnectionTheirDidDoc(connection, decryptedMessage.senderKey)
}
}
return connection
}

private suspend fun findConnectionByMessageThreadId(message: AgentMessage): ConnectionRecord? {
val pthId = message.thread?.parentThreadId ?: ""
val oobRecord = agent.outOfBandService.findByInvitationId(pthId)
val invitationKey = oobRecord?.outOfBandInvitation?.invitationKey() ?: ""
return agent.connectionService.findByInvitationKey(invitationKey)
}

private suspend fun updateConnectionTheirDidDoc(connection: ConnectionRecord, senderKey: String?) {
if (senderKey == null) {
return
}
val service = DidCommService(
id = "${connection.id}#1",
serviceEndpoint = Routing.DID_COMM_TRANSPORT_QUEUE,
recipientKeys = listOf(senderKey),
)

val theirDidDoc = DidDoc(
id = senderKey,
publicKey = emptyList(),
service = listOf(service),
authentication = emptyList(),
)
connection.theirDidDoc = theirDidDoc
agent.connectionRepository.update(connection)
}

private suspend fun findConnectionByMessageKeys(decryptedMessage: DecryptedMessageContext): ConnectionRecord? {
return agent.connectionService.findByKeys(
decryptedMessage.senderKey ?: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import org.hyperledger.ariesframework.connection.messages.DidExchangeCompleteMes
import org.hyperledger.ariesframework.connection.messages.DidExchangeRequestMessage
import org.hyperledger.ariesframework.connection.messages.DidExchangeResponseMessage
import org.hyperledger.ariesframework.connection.messages.TrustPingMessage
import org.hyperledger.ariesframework.connection.models.ConnectionState
import org.hyperledger.ariesframework.connection.models.didauth.DidDoc
import org.hyperledger.ariesframework.connection.repository.ConnectionRecord
import org.hyperledger.ariesframework.oob.messages.OutOfBandInvitation
import org.hyperledger.ariesframework.oob.models.HandshakeProtocol
Expand Down Expand Up @@ -150,7 +152,7 @@ class ConnectionCommand(val agent: Agent, private val dispatcher: Dispatcher) {
*/
suspend fun acceptOutOfBandInvitation(
outOfBandRecord: OutOfBandRecord,
handshakeProtocol: HandshakeProtocol,
handshakeProtocol: HandshakeProtocol? = null,
config: ReceiveOutOfBandInvitationConfig? = null,
): ConnectionRecord {
val connection = receiveInvitation(
Expand All @@ -159,6 +161,14 @@ class ConnectionCommand(val agent: Agent, private val dispatcher: Dispatcher) {
false,
config?.alias,
)

if (handshakeProtocol == null) {
val didDocServices = outOfBandRecord.outOfBandInvitation.services.mapNotNull { it.asDidCommService() }
connection.theirDidDoc = connection.theirDidDoc ?: DidDoc(didDocServices)
agent.connectionService.updateState(connection, ConnectionState.Complete)
return connection
}

val message = if (handshakeProtocol == HandshakeProtocol.Connections) {
agent.connectionService.createRequest(
connection.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,13 @@ class DidDoc(
}
}
}

constructor(services: List<DidCommService>) : this(id = "") {
val service = services.firstOrNull()
?: throw Exception("Creating a DidDoc from DidCommServices failed. services is empty.")
val key = service.recipientKeys.firstOrNull()
?: throw Exception("Creating a DidDoc from DidCommServices failed. recipientKeys is empty.")
this.id = key
this.service = services
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ class CredentialService(val agent: Agent) {

var credentialRecord = credentialExchangeRepository.getByThreadAndConnectionId(
requestMessage.threadId,
messageContext.connection?.id,
null,
)

// The credential offer may have been a connectionless-offer.
Expand Down
Loading

0 comments on commit ebf6bc3

Please sign in to comment.