Skip to content

Commit ebf6bc3

Browse files
authored
Support connection-less exchange (#31)
Signed-off-by: conanoc <conanoc@gmail.com>
1 parent 7a8b436 commit ebf6bc3

File tree

9 files changed

+292
-59
lines changed

9 files changed

+292
-59
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package org.hyperledger.ariesframework.connectionless
2+
3+
import androidx.test.filters.LargeTest
4+
import androidx.test.platform.app.InstrumentationRegistry
5+
import kotlinx.coroutines.test.runTest
6+
import org.hyperledger.ariesframework.TestHelper
7+
import org.hyperledger.ariesframework.agent.Agent
8+
import org.hyperledger.ariesframework.agent.SubjectOutboundTransport
9+
import org.hyperledger.ariesframework.connection.models.ConnectionState
10+
import org.hyperledger.ariesframework.credentials.models.AutoAcceptCredential
11+
import org.hyperledger.ariesframework.credentials.models.CreateOfferOptions
12+
import org.hyperledger.ariesframework.credentials.models.CredentialPreview
13+
import org.hyperledger.ariesframework.credentials.models.CredentialState
14+
import org.hyperledger.ariesframework.oob.models.CreateOutOfBandInvitationConfig
15+
import org.hyperledger.ariesframework.proofs.ProofService
16+
import org.hyperledger.ariesframework.proofs.models.AttributeFilter
17+
import org.hyperledger.ariesframework.proofs.models.AutoAcceptProof
18+
import org.hyperledger.ariesframework.proofs.models.PredicateType
19+
import org.hyperledger.ariesframework.proofs.models.ProofAttributeInfo
20+
import org.hyperledger.ariesframework.proofs.models.ProofPredicateInfo
21+
import org.hyperledger.ariesframework.proofs.models.ProofRequest
22+
import org.hyperledger.ariesframework.proofs.models.ProofState
23+
import org.junit.After
24+
import org.junit.Assert.assertEquals
25+
import org.junit.Assert.assertNotNull
26+
import org.junit.Before
27+
import org.junit.Test
28+
import kotlin.time.Duration.Companion.seconds
29+
30+
class ConnectionlessExchangeTest {
31+
lateinit var issuerAgent: Agent
32+
lateinit var holderAgent: Agent
33+
lateinit var verifierAgent: Agent
34+
35+
lateinit var credDefId: String
36+
37+
val credentialPreview = CredentialPreview.fromDictionary(mapOf("name" to "John", "age" to "99"))
38+
val context = InstrumentationRegistry.getInstrumentation().targetContext
39+
40+
@Before
41+
fun setUp() = runTest(timeout = 30.seconds) {
42+
val issuerConfig = TestHelper.getBaseConfig("issuer", true)
43+
issuerConfig.autoAcceptCredential = AutoAcceptCredential.Always
44+
issuerAgent = Agent(context, issuerConfig)
45+
46+
val holderConfig = TestHelper.getBaseConfig("holder", true)
47+
holderConfig.autoAcceptCredential = AutoAcceptCredential.Always
48+
holderConfig.autoAcceptProof = AutoAcceptProof.Always
49+
holderAgent = Agent(context, holderConfig)
50+
51+
val verifierConfig = TestHelper.getBaseConfig("verifier", true)
52+
verifierConfig.autoAcceptProof = AutoAcceptProof.Always
53+
verifierAgent = Agent(context, verifierConfig)
54+
55+
issuerAgent.initialize()
56+
holderAgent.initialize()
57+
verifierAgent.initialize()
58+
59+
credDefId = TestHelper.prepareForIssuance(issuerAgent, listOf("name", "age"))
60+
}
61+
62+
@After
63+
fun tearDown() = runTest {
64+
issuerAgent.reset()
65+
holderAgent.reset()
66+
verifierAgent.reset()
67+
}
68+
69+
@Test @LargeTest
70+
fun testConnectionlessExchange() = runTest {
71+
issuerAgent.setOutboundTransport(SubjectOutboundTransport(holderAgent))
72+
holderAgent.setOutboundTransport(SubjectOutboundTransport(issuerAgent))
73+
74+
val offerOptions = CreateOfferOptions(
75+
connection = null,
76+
credentialDefinitionId = credDefId,
77+
attributes = credentialPreview.attributes,
78+
comment = "credential-offer for test",
79+
)
80+
val (message, record) = issuerAgent.credentialService.createOffer(offerOptions)
81+
validateState(issuerAgent, record.threadId, CredentialState.OfferSent)
82+
83+
val oobConfig = CreateOutOfBandInvitationConfig(
84+
label = "issuer-to-holder-invitation",
85+
alias = "issuer-to-holder-invitation",
86+
handshake = false,
87+
messages = listOf(message),
88+
multiUseInvitation = false,
89+
autoAcceptConnection = true,
90+
)
91+
val oobInvitation = issuerAgent.oob.createInvitation(oobConfig)
92+
93+
val (oob, connection) = holderAgent.oob.receiveInvitation(oobInvitation.outOfBandInvitation)
94+
assertNotNull(connection)
95+
assertEquals(connection?.state, ConnectionState.Complete)
96+
assertNotNull(oob)
97+
98+
validateState(holderAgent, record.threadId, CredentialState.Done)
99+
validateState(issuerAgent, record.threadId, CredentialState.Done)
100+
101+
// credential exchange done.
102+
103+
holderAgent.setOutboundTransport(SubjectOutboundTransport(verifierAgent))
104+
verifierAgent.setOutboundTransport(SubjectOutboundTransport(holderAgent))
105+
106+
val proofRequest = getProofRequest()
107+
val (proofRequestMessage, proofExchangeRecord) = verifierAgent.proofService.createRequest(
108+
proofRequest,
109+
)
110+
validateState(verifierAgent, proofExchangeRecord.threadId, ProofState.RequestSent)
111+
112+
val oobConfigForProofExchange = CreateOutOfBandInvitationConfig(
113+
label = "verifier-to-holder-invitation",
114+
alias = "verifier-to-holder-invitation",
115+
handshake = false,
116+
messages = listOf(proofRequestMessage),
117+
multiUseInvitation = false,
118+
autoAcceptConnection = true,
119+
)
120+
val oobInvitationForProofExchange =
121+
verifierAgent.oob.createInvitation(oobConfigForProofExchange)
122+
123+
val (oobForProofExchange, connectionForProofExchange) = holderAgent.oob.receiveInvitation(
124+
oobInvitationForProofExchange.outOfBandInvitation,
125+
)
126+
assertNotNull(connectionForProofExchange)
127+
assertEquals(connectionForProofExchange?.state, ConnectionState.Complete)
128+
assertNotNull(oobForProofExchange)
129+
130+
validateState(holderAgent, proofExchangeRecord.threadId, ProofState.Done)
131+
validateState(verifierAgent, proofExchangeRecord.threadId, ProofState.Done)
132+
}
133+
134+
private suspend fun validateState(agent: Agent, threadId: String, state: CredentialState) {
135+
val record = agent.credentialExchangeRepository.getByThreadAndConnectionId(threadId, null)
136+
assertEquals(record.state, state)
137+
}
138+
139+
private suspend fun validateState(agent: Agent, threadId: String, state: ProofState) {
140+
val record = agent.proofRepository.getByThreadAndConnectionId(threadId, null)
141+
assertEquals(record.state, state)
142+
}
143+
144+
private suspend fun getProofRequest(): ProofRequest {
145+
val attributes = mapOf(
146+
"name" to ProofAttributeInfo(
147+
name = "name",
148+
restrictions = listOf(AttributeFilter(credentialDefinitionId = credDefId)),
149+
),
150+
)
151+
val predicates = mapOf(
152+
"age" to ProofPredicateInfo(
153+
name = "age",
154+
predicateType = PredicateType.GreaterThanOrEqualTo,
155+
predicateValue = 50,
156+
restrictions = listOf(AttributeFilter(credentialDefinitionId = credDefId)),
157+
),
158+
)
159+
val nonce = ProofService.generateProofRequestNonce()
160+
return ProofRequest(nonce = nonce, requestedAttributes = attributes, requestedPredicates = predicates)
161+
}
162+
}

ariesframework/src/main/java/org/hyperledger/ariesframework/agent/AgentConfig.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable
55
import org.hyperledger.ariesframework.credentials.models.AutoAcceptCredential
66
import org.hyperledger.ariesframework.oob.models.HandshakeProtocol
77
import org.hyperledger.ariesframework.proofs.models.AutoAcceptProof
8+
import org.hyperledger.ariesframework.routing.Routing
89

910
@Serializable
1011
enum class MediatorPickupStrategy {
@@ -63,5 +64,5 @@ data class AgentConfig(
6364
var preferredHandshakeProtocol: HandshakeProtocol = HandshakeProtocol.Connections,
6465
) {
6566
val endpoints: List<String>
66-
get() = agentEndpoints ?: listOf("didcomm:transport/queue")
67+
get() = agentEndpoints ?: listOf(Routing.DID_COMM_TRANSPORT_QUEUE)
6768
}

ariesframework/src/main/java/org/hyperledger/ariesframework/agent/MessageReceiver.kt

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package org.hyperledger.ariesframework.agent
33
import org.hyperledger.ariesframework.DecryptedMessageContext
44
import org.hyperledger.ariesframework.EncryptedMessage
55
import org.hyperledger.ariesframework.InboundMessageContext
6+
import org.hyperledger.ariesframework.connection.models.didauth.DidCommService
7+
import org.hyperledger.ariesframework.connection.models.didauth.DidDoc
68
import org.hyperledger.ariesframework.connection.repository.ConnectionRecord
9+
import org.hyperledger.ariesframework.routing.Routing
710
import org.slf4j.LoggerFactory
811

912
class MessageReceiver(val agent: Agent) {
@@ -12,8 +15,8 @@ class MessageReceiver(val agent: Agent) {
1215
suspend fun receiveMessage(encryptedMessage: EncryptedMessage) {
1316
try {
1417
val decryptedMessage = agent.wallet.unpack(encryptedMessage)
15-
val connection = findConnectionByMessageKeys(decryptedMessage)
1618
val message = MessageSerializer.decodeFromString(decryptedMessage.plaintextMessage)
19+
val connection = findConnection(decryptedMessage, message)
1720
val messageContext = InboundMessageContext(
1821
message,
1922
decryptedMessage.plaintextMessage,
@@ -43,6 +46,44 @@ class MessageReceiver(val agent: Agent) {
4346
}
4447
}
4548

49+
private suspend fun findConnection(decryptedMessage: DecryptedMessageContext, message: AgentMessage): ConnectionRecord? {
50+
var connection = findConnectionByMessageKeys(decryptedMessage)
51+
if (connection == null) {
52+
connection = findConnectionByMessageThreadId(message)
53+
if (connection != null) {
54+
updateConnectionTheirDidDoc(connection, decryptedMessage.senderKey)
55+
}
56+
}
57+
return connection
58+
}
59+
60+
private suspend fun findConnectionByMessageThreadId(message: AgentMessage): ConnectionRecord? {
61+
val pthId = message.thread?.parentThreadId ?: ""
62+
val oobRecord = agent.outOfBandService.findByInvitationId(pthId)
63+
val invitationKey = oobRecord?.outOfBandInvitation?.invitationKey() ?: ""
64+
return agent.connectionService.findByInvitationKey(invitationKey)
65+
}
66+
67+
private suspend fun updateConnectionTheirDidDoc(connection: ConnectionRecord, senderKey: String?) {
68+
if (senderKey == null) {
69+
return
70+
}
71+
val service = DidCommService(
72+
id = "${connection.id}#1",
73+
serviceEndpoint = Routing.DID_COMM_TRANSPORT_QUEUE,
74+
recipientKeys = listOf(senderKey),
75+
)
76+
77+
val theirDidDoc = DidDoc(
78+
id = senderKey,
79+
publicKey = emptyList(),
80+
service = listOf(service),
81+
authentication = emptyList(),
82+
)
83+
connection.theirDidDoc = theirDidDoc
84+
agent.connectionRepository.update(connection)
85+
}
86+
4687
private suspend fun findConnectionByMessageKeys(decryptedMessage: DecryptedMessageContext): ConnectionRecord? {
4788
return agent.connectionService.findByKeys(
4889
decryptedMessage.senderKey ?: "",

ariesframework/src/main/java/org/hyperledger/ariesframework/connection/ConnectionCommand.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import org.hyperledger.ariesframework.connection.messages.DidExchangeCompleteMes
1717
import org.hyperledger.ariesframework.connection.messages.DidExchangeRequestMessage
1818
import org.hyperledger.ariesframework.connection.messages.DidExchangeResponseMessage
1919
import org.hyperledger.ariesframework.connection.messages.TrustPingMessage
20+
import org.hyperledger.ariesframework.connection.models.ConnectionState
21+
import org.hyperledger.ariesframework.connection.models.didauth.DidDoc
2022
import org.hyperledger.ariesframework.connection.repository.ConnectionRecord
2123
import org.hyperledger.ariesframework.oob.messages.OutOfBandInvitation
2224
import org.hyperledger.ariesframework.oob.models.HandshakeProtocol
@@ -150,7 +152,7 @@ class ConnectionCommand(val agent: Agent, private val dispatcher: Dispatcher) {
150152
*/
151153
suspend fun acceptOutOfBandInvitation(
152154
outOfBandRecord: OutOfBandRecord,
153-
handshakeProtocol: HandshakeProtocol,
155+
handshakeProtocol: HandshakeProtocol? = null,
154156
config: ReceiveOutOfBandInvitationConfig? = null,
155157
): ConnectionRecord {
156158
val connection = receiveInvitation(
@@ -159,6 +161,14 @@ class ConnectionCommand(val agent: Agent, private val dispatcher: Dispatcher) {
159161
false,
160162
config?.alias,
161163
)
164+
165+
if (handshakeProtocol == null) {
166+
val didDocServices = outOfBandRecord.outOfBandInvitation.services.mapNotNull { it.asDidCommService() }
167+
connection.theirDidDoc = connection.theirDidDoc ?: DidDoc(didDocServices)
168+
agent.connectionService.updateState(connection, ConnectionState.Complete)
169+
return connection
170+
}
171+
162172
val message = if (handshakeProtocol == HandshakeProtocol.Connections) {
163173
agent.connectionService.createRequest(
164174
connection.id,

ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDoc.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,13 @@ class DidDoc(
7373
}
7474
}
7575
}
76+
77+
constructor(services: List<DidCommService>) : this(id = "") {
78+
val service = services.firstOrNull()
79+
?: throw Exception("Creating a DidDoc from DidCommServices failed. services is empty.")
80+
val key = service.recipientKeys.firstOrNull()
81+
?: throw Exception("Creating a DidDoc from DidCommServices failed. recipientKeys is empty.")
82+
this.id = key
83+
this.service = services
84+
}
7685
}

ariesframework/src/main/java/org/hyperledger/ariesframework/credentials/CredentialService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ class CredentialService(val agent: Agent) {
238238

239239
var credentialRecord = credentialExchangeRepository.getByThreadAndConnectionId(
240240
requestMessage.threadId,
241-
messageContext.connection?.id,
241+
null,
242242
)
243243

244244
// The credential offer may have been a connectionless-offer.

0 commit comments

Comments
 (0)