From 5e01569e4c6a66578e59809966bff4b617691404 Mon Sep 17 00:00:00 2001 From: conanoc Date: Thu, 9 May 2024 18:03:01 +0900 Subject: [PATCH] Implement DID Exchange Protocol (#28) Signed-off-by: conanoc --- .idea/.gitignore | 2 + DEVELOP.md | 5 + README.md | 3 +- ariesframework/build.gradle | 13 +- .../ariesframework/agent/AgentTest.kt | 4 +- .../connection/DidExchangeTest.kt | 61 +++++ .../connection/JwsServiceTest.kt | 74 ++++++ .../connection/PeerDIDServiceTest.kt | 61 +++++ .../connection/didauth/DidDocTest.kt | 15 +- .../hyperledger/ariesframework/agent/Agent.kt | 6 + .../ariesframework/agent/AgentConfig.kt | 3 + .../agent/decorators/Attachment.kt | 3 +- .../connection/ConnectionCommand.kt | 12 + .../connection/ConnectionService.kt | 2 +- .../connection/DidExchangeService.kt | 222 ++++++++++++++++++ .../ariesframework/connection/JwsService.kt | 82 +++++++ .../connection/PeerDIDService.kt | 92 ++++++++ .../handlers/DidExchangeCompleteHandler.kt | 20 ++ .../handlers/DidExchangeRequestHandler.kt | 20 ++ .../handlers/DidExchangeResponseHandler.kt | 20 ++ .../messages/DidExchangeCompleteMessage.kt | 16 ++ .../messages/DidExchangeRequestMessage.kt | 18 ++ .../messages/DidExchangeResponseMessage.kt | 23 ++ .../models/didauth/DidCommV2Service.kt | 18 ++ .../connection/models/didauth/DidDoc.kt | 61 ++++- .../models/didauth/DidDocService.kt | 2 +- .../models/didauth/DidDocumentService.kt | 2 +- .../routing/MediationRecipient.kt | 9 +- .../ariesframework/wallet/Wallet.kt | 2 +- settings.gradle | 1 + 30 files changed, 852 insertions(+), 20 deletions(-) create mode 100644 ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/DidExchangeTest.kt create mode 100644 ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/JwsServiceTest.kt create mode 100644 ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/PeerDIDServiceTest.kt create mode 100644 ariesframework/src/main/java/org/hyperledger/ariesframework/connection/DidExchangeService.kt create mode 100644 ariesframework/src/main/java/org/hyperledger/ariesframework/connection/JwsService.kt create mode 100644 ariesframework/src/main/java/org/hyperledger/ariesframework/connection/PeerDIDService.kt create mode 100644 ariesframework/src/main/java/org/hyperledger/ariesframework/connection/handlers/DidExchangeCompleteHandler.kt create mode 100644 ariesframework/src/main/java/org/hyperledger/ariesframework/connection/handlers/DidExchangeRequestHandler.kt create mode 100644 ariesframework/src/main/java/org/hyperledger/ariesframework/connection/handlers/DidExchangeResponseHandler.kt create mode 100644 ariesframework/src/main/java/org/hyperledger/ariesframework/connection/messages/DidExchangeCompleteMessage.kt create mode 100644 ariesframework/src/main/java/org/hyperledger/ariesframework/connection/messages/DidExchangeRequestMessage.kt create mode 100644 ariesframework/src/main/java/org/hyperledger/ariesframework/connection/messages/DidExchangeResponseMessage.kt create mode 100644 ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidCommV2Service.kt diff --git a/.idea/.gitignore b/.idea/.gitignore index 26d3352..8f00030 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,3 +1,5 @@ # Default ignored files /shelf/ /workspace.xml +# GitHub Copilot persisted chat sessions +/copilot/chatSessions diff --git a/DEVELOP.md b/DEVELOP.md index 5c978a0..0b461ec 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -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. diff --git a/README.md b/README.md index c22840f..87e4cd6 100644 --- a/README.md +++ b/README.md @@ -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 @@ -44,6 +44,7 @@ allprojects { password = "your github token for read:packages" } } + maven { url 'https://jitpack.io' } } } ``` diff --git a/ariesframework/build.gradle b/ariesframework/build.gradle index b711ee4..e7dec7c 100644 --- a/ariesframework/build.gradle +++ b/ariesframework/build.gradle @@ -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 diff --git a/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/agent/AgentTest.kt b/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/agent/AgentTest.kt index 25a166d..0a5be22 100644 --- a/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/agent/AgentTest.kt +++ b/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/agent/AgentTest.kt @@ -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 @@ -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) diff --git a/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/DidExchangeTest.kt b/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/DidExchangeTest.kt new file mode 100644 index 0000000..7dc9142 --- /dev/null +++ b/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/DidExchangeTest.kt @@ -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) + } +} diff --git a/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/JwsServiceTest.kt b/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/JwsServiceTest.kt new file mode 100644 index 0000000..55f613f --- /dev/null +++ b/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/JwsServiceTest.kt @@ -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(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) + } +} diff --git a/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/PeerDIDServiceTest.kt b/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/PeerDIDServiceTest.kt new file mode 100644 index 0000000..c95e9f7 --- /dev/null +++ b/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/PeerDIDServiceTest.kt @@ -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]) + } +} diff --git a/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/didauth/DidDocTest.kt b/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/didauth/DidDocTest.kt index 4d55eb4..358d737 100644 --- a/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/didauth/DidDocTest.kt +++ b/ariesframework/src/androidTest/java/org/hyperledger/ariesframework/connection/didauth/DidDocTest.kt @@ -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": [ @@ -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) @@ -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) diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/Agent.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/Agent.kt index 2fc3e0c..43d8600 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/Agent.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/Agent.kt @@ -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 @@ -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) diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/AgentConfig.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/AgentConfig.kt index 73c2bed..fc3069f 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/AgentConfig.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/AgentConfig.kt @@ -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 @@ -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 @@ -58,6 +60,7 @@ data class AgentConfig( var publicDidSeed: String? = null, var agentEndpoints: List? = null, var useReturnRoute: Boolean = true, + var preferredHandshakeProtocol: HandshakeProtocol = HandshakeProtocol.Connections, ) { val endpoints: List get() = agentEndpoints ?: listOf("didcomm:transport/queue") diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/decorators/Attachment.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/decorators/Attachment.kt index 9d8f06a..4f7b73b 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/decorators/Attachment.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/agent/decorators/Attachment.kt @@ -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( @@ -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", diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/ConnectionCommand.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/ConnectionCommand.kt index 5fd01f9..4058a1f 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/ConnectionCommand.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/ConnectionCommand.kt @@ -6,10 +6,16 @@ import org.hyperledger.ariesframework.agent.Dispatcher import org.hyperledger.ariesframework.agent.MessageSerializer import org.hyperledger.ariesframework.connection.handlers.ConnectionRequestHandler import org.hyperledger.ariesframework.connection.handlers.ConnectionResponseHandler +import org.hyperledger.ariesframework.connection.handlers.DidExchangeCompleteHandler +import org.hyperledger.ariesframework.connection.handlers.DidExchangeRequestHandler +import org.hyperledger.ariesframework.connection.handlers.DidExchangeResponseHandler import org.hyperledger.ariesframework.connection.handlers.TrustPingMessageHandler import org.hyperledger.ariesframework.connection.messages.ConnectionInvitationMessage import org.hyperledger.ariesframework.connection.messages.ConnectionRequestMessage import org.hyperledger.ariesframework.connection.messages.ConnectionResponseMessage +import org.hyperledger.ariesframework.connection.messages.DidExchangeCompleteMessage +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.repository.ConnectionRecord import org.hyperledger.ariesframework.oob.messages.OutOfBandInvitation @@ -29,6 +35,9 @@ class ConnectionCommand(val agent: Agent, private val dispatcher: Dispatcher) { dispatcher.registerHandler(ConnectionRequestHandler(agent)) dispatcher.registerHandler(ConnectionResponseHandler(agent)) dispatcher.registerHandler(TrustPingMessageHandler(agent)) + dispatcher.registerHandler(DidExchangeRequestHandler(agent)) + dispatcher.registerHandler(DidExchangeResponseHandler(agent)) + dispatcher.registerHandler(DidExchangeCompleteHandler(agent)) } private fun registerMessages() { @@ -36,6 +45,9 @@ class ConnectionCommand(val agent: Agent, private val dispatcher: Dispatcher) { MessageSerializer.registerMessage(ConnectionRequestMessage.type, ConnectionRequestMessage::class) MessageSerializer.registerMessage(ConnectionResponseMessage.type, ConnectionResponseMessage::class) MessageSerializer.registerMessage(TrustPingMessage.type, TrustPingMessage::class) + MessageSerializer.registerMessage(DidExchangeRequestMessage.type, DidExchangeRequestMessage::class) + MessageSerializer.registerMessage(DidExchangeResponseMessage.type, DidExchangeResponseMessage::class) + MessageSerializer.registerMessage(DidExchangeCompleteMessage.type, DidExchangeCompleteMessage::class) } /** diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/ConnectionService.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/ConnectionService.kt index 5b2d42e..d97257d 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/ConnectionService.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/ConnectionService.kt @@ -32,7 +32,7 @@ class ConnectionService(val agent: Agent) { val connectionRepository = agent.connectionRepository private val logger = LoggerFactory.getLogger(ConnectionService::class.java) - private fun createConnection( + fun createConnection( role: ConnectionRole, state: ConnectionState, invitation: ConnectionInvitationMessage? = null, diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/DidExchangeService.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/DidExchangeService.kt new file mode 100644 index 0000000..e845b4b --- /dev/null +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/DidExchangeService.kt @@ -0,0 +1,222 @@ +package org.hyperledger.ariesframework.connection + +import org.hyperledger.ariesframework.InboundMessageContext +import org.hyperledger.ariesframework.OutboundMessage +import org.hyperledger.ariesframework.agent.Agent +import org.hyperledger.ariesframework.agent.AgentEvents +import org.hyperledger.ariesframework.agent.MessageSerializer +import org.hyperledger.ariesframework.agent.decorators.Attachment +import org.hyperledger.ariesframework.agent.decorators.ThreadDecorator +import org.hyperledger.ariesframework.connection.messages.DidExchangeCompleteMessage +import org.hyperledger.ariesframework.connection.messages.DidExchangeRequestMessage +import org.hyperledger.ariesframework.connection.messages.DidExchangeResponseMessage +import org.hyperledger.ariesframework.connection.models.ConnectionRole +import org.hyperledger.ariesframework.connection.models.ConnectionState +import org.hyperledger.ariesframework.connection.repository.ConnectionRecord +import org.hyperledger.ariesframework.decodeBase64 +import org.hyperledger.ariesframework.oob.repository.OutOfBandRecord +import org.hyperledger.ariesframework.util.DIDParser +import org.slf4j.LoggerFactory + +class DidExchangeService(val agent: Agent) { + val connectionRepository = agent.connectionRepository + private val logger = LoggerFactory.getLogger(DidExchangeService::class.java) + + /** + * Create a DID exchange request message for the connection with the specified connection id. + * + * @param connectionId the id of the connection for which to create a DID exchange request. + * @param label the label to use for the DID exchange request. + * @param autoAcceptConnection whether to automatically accept the DID exchange response. + * @return the outbound message containing DID exchange request. + */ + suspend fun createRequest( + connectionId: String, + label: String? = null, + autoAcceptConnection: Boolean? = null, + ): OutboundMessage { + var connectionRecord = connectionRepository.getById(connectionId) + assert(connectionRecord.state == ConnectionState.Invited) + assert(connectionRecord.role == ConnectionRole.Invitee) + + val peerDid = agent.peerDIDService.createPeerDID(connectionRecord.verkey) + logger.debug("Created peer DID for a RequestMessage: $peerDid") + val message = DidExchangeRequestMessage(label ?: agent.agentConfig.label, did = peerDid) + + if (autoAcceptConnection != null) { + connectionRecord.autoAcceptConnection = autoAcceptConnection + } + connectionRecord.threadId = message.id + connectionRecord.did = peerDid + updateState(connectionRecord, ConnectionState.Requested) + + return OutboundMessage(message, connectionRecord) + } + + /** + * Process a received DID exchange request message. This will not accept the DID exchange request + * or send a DID exchange response message. It will only update the existing connection record + * with all the new information from the DID exchange request message. Use [createResponse()] + * after calling this function to create a DID exchange response. + * + * @param messageContext the message context containing the DID exchange request message. + * @return updated connection record. + */ + suspend fun processRequest(messageContext: InboundMessageContext): ConnectionRecord { + val message = MessageSerializer.decodeFromString(messageContext.plaintextMessage) as DidExchangeRequestMessage + val recipientKey = messageContext.recipientVerkey + ?: throw Exception("Unable to process connection request without recipientVerkey") + + var outOfBandRecord: OutOfBandRecord? + val outOfBandRecords = agent.outOfBandService.findAllByInvitationKey(recipientKey) + if (outOfBandRecords.isEmpty()) { + throw Exception("No out-of-band record or connection record found for invitation key: $recipientKey") + } else { + outOfBandRecord = outOfBandRecords[0] + } + + val didDoc = agent.peerDIDService.parsePeerDID(message.did) + var connectionRecord = agent.connectionService.createConnection( + ConnectionRole.Inviter, + ConnectionState.Invited, + null, + outOfBandRecord!!.outOfBandInvitation, + null, + agent.mediationRecipient.getRouting(), + message.label, + outOfBandRecord!!.autoAcceptConnection, + true, + null, + null, + message.threadId, + ) + + connectionRepository.save(connectionRecord) + + connectionRecord.theirDidDoc = didDoc + connectionRecord.theirLabel = message.label + connectionRecord.threadId = message.id + connectionRecord.theirDid = didDoc.id + + if (connectionRecord.theirKey() == null) { + throw Exception("Connection with id ${connectionRecord.id} has no recipient keys.") + } + + updateState(connectionRecord, ConnectionState.Requested) + return connectionRecord + } + + /** + * Create a DID exchange response message for the connection with the specified connection id. + * + * @param connectionId the id of the connection for which to create a DID exchange response. + * @return outbound message containing DID exchange response. + */ + suspend fun createResponse(connectionId: String): OutboundMessage { + var connectionRecord = connectionRepository.getById(connectionId) + assert(connectionRecord.state == ConnectionState.Requested) + assert(connectionRecord.role == ConnectionRole.Inviter) + val threadId = connectionRecord.threadId + ?: throw Exception("Connection record with id ${connectionRecord.id} has no thread id.") + + val peerDid = agent.peerDIDService.createPeerDID(connectionRecord.verkey) + connectionRecord.did = peerDid + + val message = DidExchangeResponseMessage(threadId, peerDid) + message.thread = ThreadDecorator(threadId) + + val payload = peerDid.toByteArray() + val signingKey = connectionRecord.getTags()["invitationKey"] ?: connectionRecord.verkey + val jws = agent.jwsService.createJws(payload, signingKey) + var attachment = Attachment.fromData(payload) + attachment.addJws(jws) + message.didRotate = attachment + + updateState(connectionRecord, ConnectionState.Responded) + + return OutboundMessage(message, connectionRecord) + } + + /** + * Process a received DID exchange response message. This will not accept the DID exchange response + * or send a DID exchange complete message. It will only update the existing connection record + * with all the new information from the DID exchange response message. Use [createComplete()] + * after calling this function to create a DID exchange complete message. + * + * @param messageContext the message context containing a DID exchange response message. + * @return updated connection record. + */ + suspend fun processResponse(messageContext: InboundMessageContext): ConnectionRecord { + val message = MessageSerializer.decodeFromString(messageContext.plaintextMessage) as DidExchangeResponseMessage + var connectionRecord = try { + agent.connectionService.getByThreadId(message.threadId) + } catch (e: Exception) { + throw Exception("Unable to process DID exchange response: connection for threadId: ${message.threadId} not found") + } + assert(connectionRecord.state == ConnectionState.Requested) + assert(connectionRecord.role == ConnectionRole.Invitee) + + if (message.threadId != connectionRecord.threadId) { + throw Exception("Invalid or missing thread ID") + } + + verifyDidRotate(message, connectionRecord) + + val didDoc = agent.peerDIDService.parsePeerDID(message.did) + connectionRecord.theirDid = didDoc.id + connectionRecord.theirDidDoc = didDoc + + updateState(connectionRecord, ConnectionState.Responded) + return connectionRecord + } + + private fun verifyDidRotate(message: DidExchangeResponseMessage, connectionRecord: ConnectionRecord) { + val didRotateAttachment = message.didRotate + ?: throw Exception("Missing valid did_rotate in response: ${message.didRotate}") + val jws = didRotateAttachment.data.jws + ?: throw Exception("Missing valid jws in did_rotate attachment: ${didRotateAttachment.data.jws}") + val base64Payload = didRotateAttachment.data.base64 + ?: throw Exception("Missing valid base64 in did_rotate attachment: ${didRotateAttachment.data.base64}") + val payload = base64Payload.decodeBase64() + + val signedDid = payload.decodeToString() + if (message.did != signedDid) { + throw Exception("DID Rotate attachment's did $signedDid does not correspond to message did ${message.did}") + } + + val (isValid, signer) = agent.jwsService.verifyJws(jws, payload) + val senderKeys = connectionRecord.outOfBandInvitation!!.fingerprints().map { + DIDParser.convertFingerprintToVerkey(it) + } + if (!isValid || !senderKeys.contains(signer)) { + throw Exception("Failed to verify did rotate signature. isValid: $isValid, signer: $signer, senderKeys: $senderKeys") + } + } + + /** + * Create a DID exchange complete message for the connection with the specified connection id. + * + * @param connectionId the id of the connection for which to create a DID exchange complete message. + * @return outbound message containing a DID exchange complete message. + */ + suspend fun createComplete(connectionId: String): OutboundMessage { + var connectionRecord = connectionRepository.getById(connectionId) + assert(connectionRecord.state == ConnectionState.Responded) + + val threadId = connectionRecord.threadId + ?: throw Exception("Connection record with id ${connectionRecord.id} has no thread id.") + val parentThreadId = connectionRecord.outOfBandInvitation?.id + ?: throw Exception("Connection record with id ${connectionRecord.id} has no parent thread id.") + + val message = DidExchangeCompleteMessage(threadId, parentThreadId) + updateState(connectionRecord, ConnectionState.Complete) + + return OutboundMessage(message, connectionRecord) + } + + private suspend fun updateState(connectionRecord: ConnectionRecord, newState: ConnectionState) { + connectionRecord.state = newState + connectionRepository.update(connectionRecord) + agent.eventBus.publish(AgentEvents.ConnectionEvent(connectionRecord.copy())) + } +} diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/JwsService.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/JwsService.kt new file mode 100644 index 0000000..6569262 --- /dev/null +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/JwsService.kt @@ -0,0 +1,82 @@ +package org.hyperledger.ariesframework.connection + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import org.hyperledger.ariesframework.agent.Agent +import org.hyperledger.ariesframework.agent.decorators.Jws +import org.hyperledger.ariesframework.agent.decorators.JwsFlattenedFormat +import org.hyperledger.ariesframework.agent.decorators.JwsGeneralFormat +import org.hyperledger.ariesframework.decodeBase64url +import org.hyperledger.ariesframework.encodeBase64url +import org.hyperledger.ariesframework.util.Base58 +import org.hyperledger.ariesframework.util.DIDParser +import org.slf4j.LoggerFactory + +class JwsService(val agent: Agent) { + private val logger = LoggerFactory.getLogger(JwsService::class.java) + + /** + * Create a JWS using the given payload and verkey. + * + * @param payload The payload to sign. + * @param verkey The verkey to sign the payload for. The verkey should be created using [Wallet.createDid]. + * @return The created JWS. + */ + suspend fun createJws(payload: ByteArray, verkey: String): JwsGeneralFormat { + val keyEntry = agent.wallet.session!!.fetchKey(verkey, false) + ?: throw Exception("Unable to find key for verkey: $verkey") + val key = keyEntry.loadLocalKey() + val jwk = Json.decodeFromString(key.toJwkPublic(null)) + val protectedHeader = JsonObject( + mapOf( + "alg" to JsonPrimitive("EdDSA"), + "jwk" to jwk, + ), + ) + val protectedHeaderJson = Json.encodeToString(protectedHeader) + val base64ProtectedHeader = protectedHeaderJson.encodeToByteArray().encodeBase64url() + val base64Payload = payload.encodeBase64url() + + val message = "$base64ProtectedHeader.$base64Payload".toByteArray() + val signature = key.signMessage(message, null) + val base64Signature = signature.encodeBase64url() + val header = mapOf( + "kid" to DIDParser.convertVerkeyToDidKey(verkey), + ) + + return JwsGeneralFormat(header, base64Signature, base64ProtectedHeader) + } + + /** + * Verify the given JWS against the given payload. + * + * @param jws The JWS to verify. + * @param payload The payload to verify the JWS against. + * @return A pair containing the validity of the JWS and the signer's verkey. + */ + fun verifyJws(jws: Jws, payload: ByteArray): Pair { + logger.debug("Verifying JWS...") + val firstSig = when (jws) { + is JwsGeneralFormat -> jws + is JwsFlattenedFormat -> jws.signatures.first() + else -> throw Exception("Unsupported JWS type") + } + val protected = Json.decodeFromString(firstSig.protected.decodeBase64url().decodeToString()) + val signature = firstSig.signature.decodeBase64url() + val jwk = protected["jwk"] as JsonObject + val jwkString = Json.encodeToString(jwk) + logger.debug("JWK: $jwkString") + + val key = agent.wallet.keyFactory.fromJwk(jwkString) + val publicBytes = key.toPublicBytes() + val signer = Base58.encode(publicBytes) + + val base64Payload = payload.encodeBase64url() + val message = "${firstSig.protected}.$base64Payload".toByteArray() + val isValid = key.verifySignature(message, signature, null) + + return Pair(isValid, signer) + } +} diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/PeerDIDService.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/PeerDIDService.kt new file mode 100644 index 0000000..f565acb --- /dev/null +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/PeerDIDService.kt @@ -0,0 +1,92 @@ +package org.hyperledger.ariesframework.connection + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.didcommx.peerdid.DIDDocPeerDID +import org.didcommx.peerdid.VerificationMaterialAgreement +import org.didcommx.peerdid.VerificationMaterialAuthentication +import org.didcommx.peerdid.VerificationMaterialFormatPeerDID +import org.didcommx.peerdid.VerificationMethodTypeAgreement +import org.didcommx.peerdid.VerificationMethodTypeAuthentication +import org.didcommx.peerdid.createPeerDIDNumalgo2 +import org.didcommx.peerdid.resolvePeerDID +import org.hyperledger.ariesframework.agent.Agent +import org.hyperledger.ariesframework.connection.models.didauth.DidCommService +import org.hyperledger.ariesframework.connection.models.didauth.DidCommV2Service +import org.hyperledger.ariesframework.connection.models.didauth.DidDoc +import org.hyperledger.ariesframework.connection.models.didauth.DidDocService +import org.hyperledger.ariesframework.connection.models.didauth.ServiceEndpoint +import org.hyperledger.ariesframework.connection.models.didauth.didDocServiceModule +import org.hyperledger.ariesframework.util.DIDParser +import org.slf4j.LoggerFactory + +class PeerDIDService(val agent: Agent) { + private val logger = LoggerFactory.getLogger(PeerDIDService::class.java) + private val encoder = Json { serializersModule = didDocServiceModule } + + /** + * Create a Peer DID with numAlgo 2 using the provided verkey. + * This function adds a service of type "did-communication" to the Peer DID if useLegacyService is true, + * else it adds a service of type "DIDCommMessaging". + * + * @param verkey The verkey to use for the Peer DID. + * @param useLegacyService whether to use the legacy service type or not. Default is true. + * @return The created Peer DID. + */ + suspend fun createPeerDID(verkey: String, useLegacyService: Boolean = true): String { + logger.debug("Creating Peer DID for verkey: $verkey") + val (endpoints, routingKeys) = agent.mediationRecipient.getRoutingInfo() + val didRoutingKeys = routingKeys.map { rawKey -> + val key = DIDParser.convertVerkeyToDidKey(rawKey) + return "$key#${DIDParser.getMethodId(key)}" + } + val authKey = VerificationMaterialAuthentication( + type = VerificationMethodTypeAuthentication.ED25519_VERIFICATION_KEY_2020, + format = VerificationMaterialFormatPeerDID.BASE58, + value = verkey, + ) + val agreementKey = VerificationMaterialAgreement( + type = VerificationMethodTypeAgreement.X25519_KEY_AGREEMENT_KEY_2019, + format = VerificationMaterialFormatPeerDID.BASE58, + value = verkey, + ) + val service = if (useLegacyService) { + val service = DidCommService( + id = "#service-1", + serviceEndpoint = endpoints.first(), + routingKeys = didRoutingKeys, + recipientKeys = listOf("#key-2"), + ) + encoder.encodeToString(service) + } else { + val service = DidCommV2Service( + id = "#service-1", + serviceEndpoint = ServiceEndpoint( + uri = endpoints.first(), + routingKeys = didRoutingKeys, + ), + ) + encoder.encodeToString(service) + } + return createPeerDIDNumalgo2( + encryptionKeys = listOf(agreementKey), + signingKeys = listOf(authKey), + service = service, + ) + } + + /** + * Parse a Peer DID into a DidDoc. Only numAlgo 0 and 2 are supported. + * In case of numAlgo 2 DID, the routing keys should be did:key format. + * + * @param did The Peer DID to parse. + * @return The parsed DID Document. + */ + suspend fun parsePeerDID(did: String): DidDoc { + logger.debug("Parsing Peer DID: $did") + val json = resolvePeerDID(did, VerificationMaterialFormatPeerDID.BASE58) + logger.debug("Parsed Peer DID JSON: $json") + val didDocument = DIDDocPeerDID.fromJson(json) + return DidDoc(didDocument) + } +} diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/handlers/DidExchangeCompleteHandler.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/handlers/DidExchangeCompleteHandler.kt new file mode 100644 index 0000000..d255179 --- /dev/null +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/handlers/DidExchangeCompleteHandler.kt @@ -0,0 +1,20 @@ +package org.hyperledger.ariesframework.connection.handlers + +import org.hyperledger.ariesframework.InboundMessageContext +import org.hyperledger.ariesframework.OutboundMessage +import org.hyperledger.ariesframework.agent.Agent +import org.hyperledger.ariesframework.agent.MessageHandler +import org.hyperledger.ariesframework.connection.messages.DidExchangeCompleteMessage +import org.hyperledger.ariesframework.connection.models.ConnectionState + +class DidExchangeCompleteHandler(val agent: Agent) : MessageHandler { + override val messageType = DidExchangeCompleteMessage.type + + override suspend fun handle(messageContext: InboundMessageContext): OutboundMessage? { + val connection = messageContext.connection + if (connection != null && connection.state == ConnectionState.Responded) { + agent.connectionService.updateState(connection, ConnectionState.Complete) + } + return null + } +} diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/handlers/DidExchangeRequestHandler.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/handlers/DidExchangeRequestHandler.kt new file mode 100644 index 0000000..f77c235 --- /dev/null +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/handlers/DidExchangeRequestHandler.kt @@ -0,0 +1,20 @@ +package org.hyperledger.ariesframework.connection.handlers + +import org.hyperledger.ariesframework.InboundMessageContext +import org.hyperledger.ariesframework.OutboundMessage +import org.hyperledger.ariesframework.agent.Agent +import org.hyperledger.ariesframework.agent.MessageHandler +import org.hyperledger.ariesframework.connection.messages.DidExchangeRequestMessage + +class DidExchangeRequestHandler(val agent: Agent) : MessageHandler { + override val messageType = DidExchangeRequestMessage.type + + override suspend fun handle(messageContext: InboundMessageContext): OutboundMessage? { + val connectionRecord = agent.didExchangeService.processRequest(messageContext) + if (connectionRecord.autoAcceptConnection == true || agent.agentConfig.autoAcceptConnections) { + return agent.didExchangeService.createResponse(connectionRecord.id) + } + + return null + } +} diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/handlers/DidExchangeResponseHandler.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/handlers/DidExchangeResponseHandler.kt new file mode 100644 index 0000000..4b02329 --- /dev/null +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/handlers/DidExchangeResponseHandler.kt @@ -0,0 +1,20 @@ +package org.hyperledger.ariesframework.connection.handlers + +import org.hyperledger.ariesframework.InboundMessageContext +import org.hyperledger.ariesframework.OutboundMessage +import org.hyperledger.ariesframework.agent.Agent +import org.hyperledger.ariesframework.agent.MessageHandler +import org.hyperledger.ariesframework.connection.messages.DidExchangeResponseMessage + +class DidExchangeResponseHandler(val agent: Agent) : MessageHandler { + override val messageType = DidExchangeResponseMessage.type + + override suspend fun handle(messageContext: InboundMessageContext): OutboundMessage? { + val connectionRecord = agent.didExchangeService.processResponse(messageContext) + if (connectionRecord.autoAcceptConnection == true || agent.agentConfig.autoAcceptConnections) { + return agent.didExchangeService.createComplete(connectionRecord.id) + } + + return null + } +} diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/messages/DidExchangeCompleteMessage.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/messages/DidExchangeCompleteMessage.kt new file mode 100644 index 0000000..b4e630a --- /dev/null +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/messages/DidExchangeCompleteMessage.kt @@ -0,0 +1,16 @@ +package org.hyperledger.ariesframework.connection.messages + +import kotlinx.serialization.Serializable +import org.hyperledger.ariesframework.agent.AgentMessage +import org.hyperledger.ariesframework.agent.decorators.ThreadDecorator + +@Serializable +class DidExchangeCompleteMessage() : AgentMessage(generateId(), type) { + constructor(threadId: String, parentThreadId: String) : this() { + thread = ThreadDecorator(threadId, parentThreadId) + } + + companion object { + const val type = "https://didcomm.org/didexchange/1.1/complete" + } +} diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/messages/DidExchangeRequestMessage.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/messages/DidExchangeRequestMessage.kt new file mode 100644 index 0000000..7cc15b9 --- /dev/null +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/messages/DidExchangeRequestMessage.kt @@ -0,0 +1,18 @@ +package org.hyperledger.ariesframework.connection.messages + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.hyperledger.ariesframework.agent.AgentMessage + +@Serializable +class DidExchangeRequestMessage( + val label: String, + @SerialName("goal_code") + val goalCode: String? = null, + val goal: String? = null, + val did: String, +) : AgentMessage(generateId(), type) { + companion object { + const val type = "https://didcomm.org/didexchange/1.1/request" + } +} diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/messages/DidExchangeResponseMessage.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/messages/DidExchangeResponseMessage.kt new file mode 100644 index 0000000..26312a6 --- /dev/null +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/messages/DidExchangeResponseMessage.kt @@ -0,0 +1,23 @@ +package org.hyperledger.ariesframework.connection.messages + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.hyperledger.ariesframework.agent.AgentMessage +import org.hyperledger.ariesframework.agent.decorators.Attachment +import org.hyperledger.ariesframework.agent.decorators.ThreadDecorator + +@Serializable +class DidExchangeResponseMessage( + val did: String, + @SerialName("did_doc~attach") + var didDoc: Attachment? = null, + @SerialName("did_rotate~attach") + var didRotate: Attachment? = null, +) : AgentMessage(generateId(), type) { + constructor(threadId: String, did: String, didDoc: Attachment? = null, didRotate: Attachment? = null) : this(did, didDoc, didRotate) { + this.thread = ThreadDecorator(threadId) + } + companion object { + const val type = "https://didcomm.org/didexchange/1.1/response" + } +} diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidCommV2Service.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidCommV2Service.kt new file mode 100644 index 0000000..7f1c1ef --- /dev/null +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidCommV2Service.kt @@ -0,0 +1,18 @@ +package org.hyperledger.ariesframework.connection.models.didauth + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("DIDCommMessaging") +open class DidCommV2Service( + override val id: String, + val serviceEndpoint: ServiceEndpoint, +) : DidDocService() + +@Serializable +class ServiceEndpoint( + val uri: String, + val routingKeys: List? = null, + val accept: List? = null, +) diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDoc.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDoc.kt index 0d6007f..5fcb3fc 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDoc.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDoc.kt @@ -3,21 +3,74 @@ package org.hyperledger.ariesframework.connection.models.didauth import kotlinx.serialization.EncodeDefault import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import org.didcommx.peerdid.DIDCommServicePeerDID +import org.didcommx.peerdid.DIDDocPeerDID +import org.didcommx.peerdid.OtherService +import org.hyperledger.ariesframework.connection.models.didauth.publicKey.Ed25119Sig2018 import org.hyperledger.ariesframework.connection.models.didauth.publicKey.PublicKey +import org.hyperledger.ariesframework.util.DIDParser @Serializable class DidDoc( @SerialName("@context") @EncodeDefault val context: String = "https://w3id.org/did/v1", - val id: String, - val publicKey: List, - val authentication: List, - val service: List, + var id: String, + var publicKey: List = emptyList(), + var authentication: List = emptyList(), + var service: List = emptyList(), ) { fun publicKey(id: String) = publicKey.find { it.id == id } inline fun servicesByType() = service.filter { it is T }.map { it as T } fun didCommServices() = servicesByType().sortedByDescending { it.priority } + + constructor(didDocument: DIDDocPeerDID) : this(id = didDocument.did) { + if (didDocument.authentication.isEmpty()) { + throw Exception("No authentication method found in DIDDocument") + } + val recipientKey = didDocument.authentication.first().verMaterial.value.toString() + publicKey = listOf( + Ed25119Sig2018( + id = "$id#1", + controller = id, + publicKeyBase58 = recipientKey, + ), + ) + authentication = listOf( + ReferencedAuthentication( + type = Ed25119Sig2018.type, + publicKey = publicKey[0].id, + ), + ) + didDocument.service?.let { services -> + service = services.map { service -> + when (service) { + is OtherService -> { + val serviceEndpoint = service.data["serviceEndpoint"] as? Map + ?: throw Exception("Service endpoint map not found in OtherService") + val routingKeys = serviceEndpoint["routingKeys"] as? List + val parsedRoutingKeys = routingKeys?.map { DIDParser.convertDidKeyToVerkey(it) } + DidCommService( + id = service.data["id"] as String, + serviceEndpoint = serviceEndpoint["uri"] as String, + recipientKeys = listOf(recipientKey), + routingKeys = parsedRoutingKeys, + ) + } + is DIDCommServicePeerDID -> { + val parsedRoutingKeys = service.serviceEndpoint.routingKeys.map { DIDParser.convertDidKeyToVerkey(it) } + DidCommService( + id = service.id, + serviceEndpoint = service.serviceEndpoint.uri, + recipientKeys = listOf(recipientKey), + routingKeys = parsedRoutingKeys, + ) + } + else -> throw Exception("Unsupported service type") + } + } + } + } } diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDocService.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDocService.kt index 1ac5f62..5d12664 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDocService.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDocService.kt @@ -8,7 +8,6 @@ import kotlinx.serialization.modules.subclass @Serializable sealed class DidDocService { abstract val id: String - abstract val serviceEndpoint: String } interface DidComm { @@ -23,6 +22,7 @@ interface DidComm { val didDocServiceModule = SerializersModule { polymorphic(DidDocService::class) { subclass(DidCommService::class) + subclass(DidCommV2Service::class) subclass(IndyAgentService::class) subclass(DidDocumentService::class) defaultDeserializer { DidDocumentService.serializer() } diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDocumentService.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDocumentService.kt index 380c415..b93fe74 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDocumentService.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/connection/models/didauth/DidDocumentService.kt @@ -7,5 +7,5 @@ import kotlinx.serialization.Serializable @SerialName("DidDocumentService") class DidDocumentService( override val id: String, - override val serviceEndpoint: String, + val serviceEndpoint: String, ) : DidDocService() diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/routing/MediationRecipient.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/routing/MediationRecipient.kt index 0d61277..6a1315b 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/routing/MediationRecipient.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/routing/MediationRecipient.kt @@ -71,7 +71,7 @@ class MediationRecipient(private val agent: Agent, private val dispatcher: Dispa MessageSerializer.registerMessage(ForwardMessage.type, ForwardMessage::class) } - suspend fun getRouting(): Routing { + suspend fun getRoutingInfo(): Pair, List> { val mediator = repository.getDefault() val endpoints = if (mediator?.endpoint == null) { agent.agentConfig.endpoints @@ -80,11 +80,16 @@ class MediationRecipient(private val agent: Agent, private val dispatcher: Dispa } val routingKeys = mediator?.routingKeys ?: emptyList() + return Pair(endpoints, routingKeys) + } + + suspend fun getRouting(): Routing { + val (endpoints, routingKeys) = getRoutingInfo() val (did, verkey) = agent.wallet.createDid() + val mediator = repository.getDefault() if (agent.agentConfig.mediatorConnectionsInvite != null && mediator != null && mediator.isReady()) { keylistUpdate(mediator, verkey) } - logger.debug("Routing initialized with DID: $did, verkey: $verkey, endpoints: $endpoints, routingKeys: $routingKeys") return Routing(endpoints, verkey, did, routingKeys, mediator?.id) } diff --git a/ariesframework/src/main/java/org/hyperledger/ariesframework/wallet/Wallet.kt b/ariesframework/src/main/java/org/hyperledger/ariesframework/wallet/Wallet.kt index df5400e..3ad38f9 100644 --- a/ariesframework/src/main/java/org/hyperledger/ariesframework/wallet/Wallet.kt +++ b/ariesframework/src/main/java/org/hyperledger/ariesframework/wallet/Wallet.kt @@ -33,7 +33,7 @@ class Wallet(private val agent: Agent) { private val secretIdKey = agent.agentConfig.label + " aries_framework_wallet_secret_id_key" private val logger = LoggerFactory.getLogger(Wallet::class.java) private val storeManager = AskarStoreManager() - private val keyFactory = LocalKeyFactory() + val keyFactory = LocalKeyFactory() private val crypto = AskarCrypto() var store: AskarStore? = null var session: AskarSession? = null diff --git a/settings.gradle b/settings.gradle index da871de..65ed3ae 100644 --- a/settings.gradle +++ b/settings.gradle @@ -40,6 +40,7 @@ dependencyResolutionManagement { password = getExtraString("githubToken") } } + maven { url 'https://jitpack.io' } } }