Skip to content

Commit

Permalink
Implement DID Exchange Protocol (#28)
Browse files Browse the repository at this point in the history
Signed-off-by: conanoc <[email protected]>
  • Loading branch information
conanoc authored May 9, 2024
1 parent 72c218c commit 5e01569
Show file tree
Hide file tree
Showing 30 changed files with 852 additions and 20 deletions.
2 changes: 2 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions DEVELOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ Then, get the invitation urls from faber agent.
Run `testDemoFaber()` with this url and operate the faber agent to issue a credential.
Aliasing `10.0.2.2` as `lo0` is needed to allow the local mediator and the local faber can communicate with each other with the IP `10.0.2.2`.

You can see the debug messages of faber agent by adding the following option to the agent config in `BaseAgent.ts`:
```javascript
logger: new ConsoleLogger(LogLevel.debug),
```

### Testing using the sample app

You can run the sample app in `/app` directory. This sample app uses [Indicio Public Mediator](https://indicio-tech.github.io/mediator/) and connects to other agents by receiving invitions by scanning QR codes or by entering invitation urls. You can use the sample app to test the credential exchange flow and the proof exchange flow.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ Aries Framework Kotlin 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 0023](https://github.com/hyperledger/aries-rfcs/tree/main/features/0023-did-exchange)) DID Exchange Protocol (AIP 2.0)

### 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

Expand All @@ -44,6 +44,7 @@ allprojects {
password = "your github token for read:packages"
}
}
maven { url 'https://jitpack.io' }
}
}
```
Expand Down
13 changes: 7 additions & 6 deletions ariesframework/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,27 @@ ktlint {
}

dependencies {
implementation("org.hyperledger:anoncreds_uniffi:0.2.0-wrapper.1")
implementation("org.hyperledger:indy_vdr_uniffi:0.2.1-wrapper.2")
implementation("org.hyperledger:askar_uniffi:0.2.0-wrapper.1")
implementation 'org.hyperledger:anoncreds_uniffi:0.2.0-wrapper.1'
implementation 'org.hyperledger:indy_vdr_uniffi:0.2.1-wrapper.2'
implementation 'org.hyperledger:askar_uniffi:0.2.0-wrapper.1'

implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0"
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation 'org.slf4j:slf4j-api:1.7.32'
implementation 'ch.qos.logback:logback-classic:1.2.6'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.0'
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.4.0'
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
implementation 'org.didcommx:peerdid:0.5.0'

implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
testImplementation 'junit:junit:4.13.2'
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0"
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0'
}

ext["githubUsername"] = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.hyperledger.ariesframework.connection.messages.ConnectionInvitationMe
import org.hyperledger.ariesframework.connection.models.ConnectionState
import org.hyperledger.ariesframework.connection.repository.ConnectionRecord
import org.hyperledger.ariesframework.oob.messages.OutOfBandInvitation
import org.hyperledger.ariesframework.oob.models.HandshakeProtocol
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
Expand Down Expand Up @@ -152,12 +153,13 @@ class AgentTest {
fun testDemoFaber() = runBlocking {
val context = InstrumentationRegistry.getInstrumentation().targetContext
var config = TestHelper.getBcorvinConfig()
config.preferredHandshakeProtocol = HandshakeProtocol.DidExchange11
config.mediatorConnectionsInvite = URL(mediatorInvitationUrl).readText()

agent = Agent(context, config)
agent.initialize()

val faberInvitationUrl = "http://localhost:9001?oob=eyJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvb3V0LW9mLWJhbmQvMS4xL2ludml0YXRpb24iLCJAaWQiOiIyNDFjNjNkMC1mMjZkLTRlNDktYjkyYy00N2JhYTk1MzAwMzUiLCJsYWJlbCI6ImZhYmVyIiwiYWNjZXB0IjpbImRpZGNvbW0vYWlwMSIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXSwiaGFuZHNoYWtlX3Byb3RvY29scyI6WyJodHRwczovL2RpZGNvbW0ub3JnL2RpZGV4Y2hhbmdlLzEuMSIsImh0dHBzOi8vZGlkY29tbS5vcmcvY29ubmVjdGlvbnMvMS4wIl0sInNlcnZpY2VzIjpbeyJpZCI6IiNpbmxpbmUtMCIsInNlcnZpY2VFbmRwb2ludCI6Imh0dHA6Ly8xMC4wLjIuMjo5MDAxIiwidHlwZSI6ImRpZC1jb21tdW5pY2F0aW9uIiwicmVjaXBpZW50S2V5cyI6WyJkaWQ6a2V5Ono2TWttcDZNNjhNeHFuazlGUzdFZU5lUHpETmNSWXhpR1lUcUJFVm4yRjhENk41YSJdLCJyb3V0aW5nS2V5cyI6W119XX0" // ktlint-disable max-line-length
val faberInvitationUrl = "http://localhost:9001?oob=eyJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvb3V0LW9mLWJhbmQvMS4xL2ludml0YXRpb24iLCJAaWQiOiIzNDU5NDk5NS0xOTk3LTQ5ODItYTQ0MC0xMjE2OTk4YjllM2MiLCJsYWJlbCI6ImZhYmVyIiwiYWNjZXB0IjpbImRpZGNvbW0vYWlwMSIsImRpZGNvbW0vYWlwMjtlbnY9cmZjMTkiXSwiaGFuZHNoYWtlX3Byb3RvY29scyI6WyJodHRwczovL2RpZGNvbW0ub3JnL2RpZGV4Y2hhbmdlLzEuMSIsImh0dHBzOi8vZGlkY29tbS5vcmcvY29ubmVjdGlvbnMvMS4wIl0sInNlcnZpY2VzIjpbeyJpZCI6IiNpbmxpbmUtMCIsInNlcnZpY2VFbmRwb2ludCI6Imh0dHA6Ly8xMC4wLjIuMjo5MDAxIiwidHlwZSI6ImRpZC1jb21tdW5pY2F0aW9uIiwicmVjaXBpZW50S2V5cyI6WyJkaWQ6a2V5Ono2TWtrcnQ2NURBVG5zeUs2bTlwZFZIY01FWmNLTFJCOFl5VnhaYjU3dkFIN3JRNyJdLCJyb3V0aW5nS2V5cyI6W119XX0" // ktlint-disable max-line-length
val invitation = OutOfBandInvitation.fromUrl(faberInvitationUrl)
agent.oob.receiveInvitation(invitation)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.hyperledger.ariesframework.connection

import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
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.oob.models.CreateOutOfBandInvitationConfig
import org.hyperledger.ariesframework.oob.models.HandshakeProtocol
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test

class DidExchangeTest {
lateinit var faberAgent: Agent
lateinit var aliceAgent: Agent

@Before
fun setUp() = runTest {
val faberConfig = TestHelper.getBaseConfig("faber")
val aliceConfig = TestHelper.getBaseConfig("alice")

val context = InstrumentationRegistry.getInstrumentation().targetContext
faberAgent = Agent(context, faberConfig)
aliceAgent = Agent(context, aliceConfig)

faberAgent.setOutboundTransport(SubjectOutboundTransport(aliceAgent))
aliceAgent.setOutboundTransport(SubjectOutboundTransport(faberAgent))

faberAgent.initialize()
aliceAgent.initialize()
}

@After
fun tearDown() = runTest {
faberAgent.reset()
aliceAgent.reset()
}

@Test
fun testOobConnection() = runBlocking {
val outOfBandRecord = faberAgent.oob.createInvitation(CreateOutOfBandInvitationConfig())
val invitation = outOfBandRecord.outOfBandInvitation

aliceAgent.agentConfig.preferredHandshakeProtocol = HandshakeProtocol.DidExchange11
val (_, connection) = aliceAgent.oob.receiveInvitation(invitation)
val aliceFaberConnection = connection
?: throw Exception("Connection is nil after receiving oob invitation")
assertEquals(aliceFaberConnection.state, ConnectionState.Complete)

val faberAliceConnection = faberAgent.connectionService.findByInvitationKey(invitation.invitationKey()!!)
?: throw Exception("Cannot find connection by invitation key")
assertEquals(faberAliceConnection.state, ConnectionState.Complete)

assertEquals(TestHelper.isConnectedWith(faberAliceConnection, aliceFaberConnection), true)
assertEquals(TestHelper.isConnectedWith(aliceFaberConnection, faberAliceConnection), true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.hyperledger.ariesframework.connection

import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.hyperledger.ariesframework.TestHelper
import org.hyperledger.ariesframework.agent.Agent
import org.hyperledger.ariesframework.agent.decorators.JwsFlattenedFormat
import org.hyperledger.ariesframework.decodeBase64url
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test

class JwsServiceTest {
lateinit var agent: Agent
val seed = "00000000000000000000000000000My2"
val verkey = "kqa2HyagzfMAq42H5f9u3UMwnSBPQx2QfrSyXbUPxMn"
val payload = "hello".toByteArray()

@Before
fun setUp() = runTest {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val config = TestHelper.getBaseConfig()
agent = Agent(context, config)
agent.initialize()

val didInfo = agent.wallet.createDid(seed)
Assert.assertEquals(didInfo.verkey, verkey)
}

@After
fun tearDown() = runTest {
agent.reset()
}

@Test
fun testCreateAndVerify() = runTest {
val jws = agent.jwsService.createJws(payload, verkey)
Assert.assertEquals(
"did:key:z6MkfD6ccYE22Y9pHKtixeczk92MmMi2oJCP6gmNooZVKB9A",
jws.header?.get("kid"),
)
val protectedJson = jws.protected.decodeBase64url().decodeToString()
val protected = Json.decodeFromString<JsonObject>(protectedJson)
Assert.assertEquals("EdDSA", protected["alg"]?.jsonPrimitive?.content)
Assert.assertNotNull(protected["jwk"])

val (valid, signer) = agent.jwsService.verifyJws(jws, payload)
Assert.assertTrue(valid)
Assert.assertEquals(signer, verkey)
}

@Test
fun testFlattenedJws() = runTest {
val jws = agent.jwsService.createJws(payload, verkey)
val list = JwsFlattenedFormat(arrayListOf(jws))

val (valid, signer) = agent.jwsService.verifyJws(list, payload)
Assert.assertTrue(valid)
Assert.assertEquals(signer, verkey)
}

@Test
fun testVerifyFail() = runTest {
val wrongPayload = "world".toByteArray()
val jws = agent.jwsService.createJws(payload, verkey)
val (valid, signer) = agent.jwsService.verifyJws(jws, wrongPayload)
Assert.assertFalse(valid)
Assert.assertEquals(signer, verkey)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.hyperledger.ariesframework.connection

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.connection.models.didauth.DidComm
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

class PeerDIDServiceTest {
lateinit var agent: Agent
val verkey = "3uhKmLCRYfe5YWDsgBC4VNTKk3RbnFCzgjVH3zmSKHWa"

@Before
fun setUp() = runTest {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val config = TestHelper.getBaseConfig()
agent = Agent(context, config)
agent.initialize()
}

@After
fun tearDown() = runTest {
agent.reset()
}

@Test
fun testPeerDIDwithLegacyService() = runTest {
val peerDID = agent.peerDIDService.createPeerDID(verkey)
parsePeerDID(peerDID)
}

@Test
fun testPeerDIDwithDidCommV2Service() = runTest {
val peerDID = agent.peerDIDService.createPeerDID(verkey, useLegacyService = false)
parsePeerDID(peerDID)
}

suspend fun parsePeerDID(peerDID: String) {
assertTrue(peerDID.startsWith("did:peer:2"))

val didDoc = agent.peerDIDService.parsePeerDID(peerDID)
assertEquals(didDoc.id, peerDID)
assertEquals(didDoc.publicKey.size, 1)
assertEquals(didDoc.service.size, 1)
assertEquals(didDoc.authentication.size, 1)
assertEquals(didDoc.publicKey[0].value, verkey)

val service = didDoc.service.first()
assertTrue(service is DidComm)
val didCommService = service as DidComm
assertEquals(didCommService.recipientKeys.size, 1)
assertEquals(didCommService.recipientKeys[0], verkey)
assertEquals(didCommService.routingKeys?.size, 0)
assertEquals(didCommService.serviceEndpoint, agent.agentConfig.endpoints[0])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ class DidDocTest {
"recipientKeys": ["DADEajsDSaksLng9h"],
"routingKeys": ["DADEajsDSaksLng9h"],
"priority": 10
},
{
"id": "did:example:123456789abcdefghi#didcomm-1",
"type": "DIDCommMessaging",
"serviceEndpoint": {
"uri": "https://example.com/path",
"accept": [
"didcomm/v2",
"didcomm/aip2;env=rfc587"
],
"routingKeys": ["did:example:somemediator#somekey"]
}
}
],
"authentication": [
Expand Down Expand Up @@ -94,6 +106,7 @@ class DidDocTest {
assert(didDoc.service[0] is DidDocumentService)
assert(didDoc.service[1] is IndyAgentService)
assert(didDoc.service[2] is DidCommService)
assert(didDoc.service[3] is DidCommV2Service)

assert(didDoc.authentication[0] is ReferencedAuthentication)
assert(didDoc.authentication[1] is EmbeddedAuthentication)
Expand All @@ -105,7 +118,7 @@ class DidDocTest {
assertEquals("did:sov:LjgpST2rjsoxYegQDRm7EL", encodedJson["id"]!!.jsonPrimitive.content)
assertEquals("https://w3id.org/did/v1", encodedJson["@context"]!!.jsonPrimitive.content)
assertEquals(3, encodedJson["publicKey"]!!.jsonArray.size)
assertEquals(3, encodedJson["service"]!!.jsonArray.size)
assertEquals(4, encodedJson["service"]!!.jsonArray.size)
assertEquals(2, encodedJson["authentication"]!!.jsonArray.size)

assertEquals("3", encodedJson["publicKey"]!!.jsonArray[0].jsonObject["id"]!!.jsonPrimitive.content)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import org.hyperledger.ariesframework.anoncreds.storage.RevocationRegistryReposi
import org.hyperledger.ariesframework.basicmessage.BasicMessageCommand
import org.hyperledger.ariesframework.connection.ConnectionCommand
import org.hyperledger.ariesframework.connection.ConnectionService
import org.hyperledger.ariesframework.connection.DidExchangeService
import org.hyperledger.ariesframework.connection.JwsService
import org.hyperledger.ariesframework.connection.PeerDIDService
import org.hyperledger.ariesframework.connection.repository.ConnectionRepository
import org.hyperledger.ariesframework.credentials.CredentialService
import org.hyperledger.ariesframework.credentials.CredentialsCommand
Expand All @@ -35,6 +38,9 @@ class Agent(val context: Context, val agentConfig: AgentConfig) {
val messageSender = MessageSender(this)
val connectionRepository = ConnectionRepository(this)
val connectionService = ConnectionService(this)
val didExchangeService = DidExchangeService(this)
val peerDIDService = PeerDIDService(this)
val jwsService = JwsService(this)
val connections = ConnectionCommand(this, dispatcher)
val mediationRecipient = MediationRecipient(this, dispatcher)
val outOfBandRepository = OutOfBandRepository(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.hyperledger.ariesframework.agent
import kotlinx.serialization.SerialName
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

@Serializable
Expand Down Expand Up @@ -35,6 +36,7 @@ enum class MediatorPickupStrategy {
* @property publicDidSeed The seed to use for the public did. The public did is used to register items on the ledger. For testing.
* @property agentEndpoints The agent endpoints to use for testing.
* @property useReturnRoute Whether to use the transport-return-route. Default is true.
* @property preferredHandshakeProtocol The preferred handshake protocol to use. Default is [HandshakeProtocol.Connections].
* @property endpoints The endpoints of the agent. Read only.
*/
@Serializable
Expand All @@ -58,6 +60,7 @@ data class AgentConfig(
var publicDidSeed: String? = null,
var agentEndpoints: List<String>? = null,
var useReturnRoute: Boolean = true,
var preferredHandshakeProtocol: HandshakeProtocol = HandshakeProtocol.Connections,
) {
val endpoints: List<String>
get() = agentEndpoints ?: listOf("didcomm:transport/queue")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import org.hyperledger.ariesframework.decodeBase64
import org.hyperledger.ariesframework.encodeBase64
import java.util.UUID

@Serializable
class AttachmentData(
Expand Down Expand Up @@ -54,7 +55,7 @@ class Attachment(
}

companion object {
fun fromData(data: ByteArray, id: String): Attachment {
fun fromData(data: ByteArray, id: String = UUID.randomUUID().toString()): Attachment {
return Attachment(
id = id,
mimetype = "application/json",
Expand Down
Loading

0 comments on commit 5e01569

Please sign in to comment.