diff --git a/core/pom.xml b/core/pom.xml
index f168f363..eb08f23c 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -36,10 +36,6 @@
jar
-
- org.jetbrains.kotlin
- kotlin-stdlib
-
com.google.crypto.tink
tink
diff --git a/core/src/main/kotlin/org/nessus/didcomm/aries/AriesAgent.kt b/core/src/main/kotlin/org/nessus/didcomm/aries/AriesAgent.kt
deleted file mode 100644
index 4701d16d..00000000
--- a/core/src/main/kotlin/org/nessus/didcomm/aries/AriesAgent.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-package org.nessus.didcomm.aries
-
-import org.hyperledger.aries.AriesClient
-import org.hyperledger.aries.api.multitenancy.WalletRecord
-import java.net.MalformedURLException
-import java.net.URL
-
-class AgentConfiguration(val adminUrl: String?, val userUrl: String?, val apiKey: String?) {
-
- companion object {
- private val host = System.getenv("ACAPY_HOSTNAME") ?: "localhost"
- private val adminPort = System.getenv("ACAPY_ADMIN_PORT") ?: "8031"
- private val userPort = System.getenv("ACAPY_USER_PORT") ?: "8030"
- private val apiKey = System.getenv("ACAPY_ADMIN_API_KEY") ?: "adminkey"
- val defaultConfiguration= AgentConfigurationBuilder()
- .adminUrl(String.format("http://%s:%s", host, adminPort))
- .userUrl(String.format("http://%s:%s", host, userPort))
- .apiKey(apiKey)
- .build()
- }
-
- val webSocketUrl: String
- get() = try {
- val url = URL(adminUrl)
- String.format("ws://%s:%d/ws", url.host, url.port)
- } catch (ex: MalformedURLException) {
- throw IllegalArgumentException(ex)
- }
-
- override fun toString(): String {
- val reductedApiKey = if (apiKey != null) apiKey.substring(0, 4) + "..." else null
- return "AgentConfiguration [agentAdminUrl=$adminUrl, agentUserUrl=$userUrl, agentApiKey=$reductedApiKey]"
- }
-
- fun builder(): AgentConfigurationBuilder {
- return AgentConfigurationBuilder()
- }
-
- class AgentConfigurationBuilder {
- private var adminUrl: String? = null
- private var userUrl: String? = null
- private var apiKey: String? = null
- fun adminUrl(adminUrl: String?): AgentConfigurationBuilder {
- this.adminUrl = adminUrl
- return this
- }
-
- fun userUrl(userUrl: String?): AgentConfigurationBuilder {
- this.userUrl = userUrl
- return this
- }
-
- fun apiKey(apiKey: String?): AgentConfigurationBuilder {
- this.apiKey = apiKey
- return this
- }
-
- fun build(): AgentConfiguration {
- return AgentConfiguration(adminUrl, userUrl, apiKey)
- }
- }
-
- private fun getSystemEnv(key: String?, defaultValue: String?): String? {
- var value = System.getenv(key)
- if (value == null || value.isBlank() || value.isEmpty()) value = defaultValue
- return value
- }
-}
-
-object AriesClientFactory {
- /**
- * Create a client for the admin wallet
- */
- fun adminClient(): AriesClient {
- return createClient(AgentConfiguration.defaultConfiguration, null)
- }
-
- /**
- * Create a client for the admin wallet
- */
- fun adminClient(config: AgentConfiguration): AriesClient {
- return createClient(config, null)
- }
-
- /**
- * Create a client for a multitenant wallet
- */
- fun createClient(wallet: WalletRecord?): AriesClient {
- return createClient(AgentConfiguration.defaultConfiguration, wallet)
- }
-
- /**
- * Create a client for a multitenant wallet
- */
- fun createClient(config: AgentConfiguration, wallet: WalletRecord?): AriesClient {
- return AriesClient.builder()
- .url(config.adminUrl)
- .apiKey(config.apiKey)
- .bearerToken(wallet?.token)
- .build()
- }
-}
diff --git a/core/src/main/kotlin/org/nessus/didcomm/model/MessageParser.kt b/core/src/main/kotlin/org/nessus/didcomm/model/MessageParser.kt
deleted file mode 100644
index 9549f3dd..00000000
--- a/core/src/main/kotlin/org/nessus/didcomm/model/MessageParser.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.nessus.didcomm.model
-
-import com.google.gson.Gson
-import org.didcommx.didcomm.message.Message
-
-/**
- * Parses a JSON string to a DIDComm Message
- */
-class MessageParser {
-
- companion object {
- private val gson = Gson()
-
- fun fromJson(json: String) : Message {
- val jsonMap: MutableMap = mutableMapOf()
- gson.fromJson(json, Map::class.java).forEach { en ->
- val enval = en.value!!
- when(val key: String = en.key.toString()) {
- "created_time" -> jsonMap[key] = (enval as Double).toLong()
- "expires_time" -> jsonMap[key] = (enval as Double).toLong()
- "custom_headers" -> if ((enval as Map).isNotEmpty()) {
- jsonMap[key] = enval
- }
- else -> jsonMap[key] = enval
- }
- }
- return Message.parse(jsonMap)
- }
- }
-}
diff --git a/core/src/main/kotlin/org/nessus/didcomm/model/MessageReader.kt b/core/src/main/kotlin/org/nessus/didcomm/model/MessageReader.kt
new file mode 100644
index 00000000..33ce9de7
--- /dev/null
+++ b/core/src/main/kotlin/org/nessus/didcomm/model/MessageReader.kt
@@ -0,0 +1,32 @@
+package org.nessus.didcomm.model
+
+import com.google.gson.Gson
+import org.didcommx.didcomm.message.Message
+
+/**
+ * Parses a JSON string to a DIDComm Message
+ */
+object MessageReader {
+
+ private val gson = Gson()
+
+ fun fromJson(json: String) : Message {
+ val jsonMap: MutableMap = mutableMapOf()
+ gson.fromJson(json, Map::class.java).forEach { en ->
+ val enval = en.value!!
+ when(val key: String = en.key.toString()) {
+ "created_time" -> jsonMap[key] = (enval as Double).toLong()
+ "expires_time" -> jsonMap[key] = (enval as Double).toLong()
+ "custom_headers" -> if (enval is Map<*, *> && enval.isNotEmpty()) {
+ jsonMap[key] = enval
+ }
+ else -> jsonMap[key] = enval
+ }
+ }
+ return Message.parse(jsonMap)
+ }
+
+ fun fromJson(json: String, type: Class) : T {
+ return gson.fromJson(json, type)
+ }
+}
diff --git a/core/src/main/kotlin/org/nessus/didcomm/model/MessageType.kt b/core/src/main/kotlin/org/nessus/didcomm/model/MessageType.kt
new file mode 100644
index 00000000..d45c890a
--- /dev/null
+++ b/core/src/main/kotlin/org/nessus/didcomm/model/MessageType.kt
@@ -0,0 +1,14 @@
+package org.nessus.didcomm.model
+
+abstract class MessageType (
+
+ /**
+ * The header conveying the DIDComm Message Type URI.
+ * REQUIRED
+ */
+ val type: String
+) {
+ companion object {
+ const val OUT_OF_BAND_INVITATION = "https://didcomm.org/out-of-band/2.0/invitation"
+ }
+}
diff --git a/core/src/main/kotlin/org/nessus/didcomm/model/MessageWriter.kt b/core/src/main/kotlin/org/nessus/didcomm/model/MessageWriter.kt
index 5be3b366..51b1b258 100644
--- a/core/src/main/kotlin/org/nessus/didcomm/model/MessageWriter.kt
+++ b/core/src/main/kotlin/org/nessus/didcomm/model/MessageWriter.kt
@@ -3,29 +3,51 @@ package org.nessus.didcomm.model
import com.google.gson.FieldNamingPolicy
import com.google.gson.Gson
import com.google.gson.GsonBuilder
+import com.nimbusds.jose.util.Base64URL
import org.didcommx.didcomm.message.Message
/**
* Serializes a DIDComm Message to JSON
*/
-class MessageWriter {
-
- companion object {
- private val gson: Gson = GsonBuilder()
- .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
- .create()
- private val prettyGson: Gson = GsonBuilder()
- .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
- .setPrettyPrinting()
- .create()
-
- fun toJson(msg: Message, pretty: Boolean = false) : String {
- return toJson(msg as Any, pretty)
+object MessageWriter {
+
+ private val gson: Gson = GsonBuilder()
+ .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .create()
+ private val prettyGson: Gson = GsonBuilder()
+ .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .setPrettyPrinting()
+ .create()
+
+ fun toBase64URL(msg: Message): String {
+ return Base64URL.encode(toJson(msg)).toString()
+ }
+
+ fun toJson(msg: Message, pretty: Boolean = false) : String {
+ val jsonObj = gson.toJsonTree(msg).asJsonObject
+ // Remove empty 'custom_headers'
+ // [TODO] we may have to remove emtpty content for other headers too
+ val customHeaders = jsonObj.getAsJsonObject("custom_headers")
+ if (customHeaders.entrySet().isEmpty()) {
+ jsonObj.remove("custom_headers")
}
+ return auxGson(pretty).toJson(jsonObj)
+ }
- fun toJson(obj: Any, pretty: Boolean = false) : String {
- val gson = if (pretty) prettyGson else gson
- return gson.toJson(obj)
+ fun toJson(obj: Any, pretty: Boolean = false) : String {
+ return auxGson(pretty).toJson(obj)
+ }
+
+ fun toMutableMap(obj: Any) : MutableMap {
+ val result: MutableMap = mutableMapOf()
+ val input: String = if (obj is String) obj else gson.toJson(obj)
+ gson.fromJson(input, MutableMap::class.java).forEach {
+ en -> result[en.key as String] = en.value!!
}
+ return result
+ }
+
+ private fun auxGson(pretty: Boolean = false): Gson {
+ return if (pretty) prettyGson else gson
}
}
diff --git a/core/src/main/kotlin/org/nessus/didcomm/model/OutOfBandInvitationV2.kt b/core/src/main/kotlin/org/nessus/didcomm/model/OutOfBandInvitation.kt
similarity index 90%
rename from core/src/main/kotlin/org/nessus/didcomm/model/OutOfBandInvitationV2.kt
rename to core/src/main/kotlin/org/nessus/didcomm/model/OutOfBandInvitation.kt
index c457553b..a162daaa 100644
--- a/core/src/main/kotlin/org/nessus/didcomm/model/OutOfBandInvitationV2.kt
+++ b/core/src/main/kotlin/org/nessus/didcomm/model/OutOfBandInvitation.kt
@@ -28,7 +28,7 @@ import com.google.gson.Gson
* ]
* }
*/
-data class OutOfBandInvitationV2(
+data class OutOfBandInvitation(
/**
* Message ID. The id attribute value MUST be unique to the sender, across all messages they send.
@@ -69,19 +69,12 @@ data class OutOfBandInvitationV2(
* OPTIONAL
*/
val attachments: List?,
-) {
+) : MessageType(OUT_OF_BAND_INVITATION) {
companion object {
-
- /**
- * The header conveying the DIDComm MTURI.
- * REQUIRED
- */
- const val type: String = "https://didcomm.org/out-of-band/2.0/invitation"
-
- fun fromBody(body: Map): OutOfBandInvitationV2 {
+ fun fromBody(body: Map): OutOfBandInvitation {
val gson = Gson()
- return gson.fromJson(gson.toJson(body), OutOfBandInvitationV2::class.java)
+ return gson.fromJson(gson.toJson(body), OutOfBandInvitation::class.java)
}
}
}
diff --git a/core/src/main/kotlin/org/nessus/didcomm/service/AgentService.kt b/core/src/main/kotlin/org/nessus/didcomm/service/AgentService.kt
new file mode 100644
index 00000000..afd6bac5
--- /dev/null
+++ b/core/src/main/kotlin/org/nessus/didcomm/service/AgentService.kt
@@ -0,0 +1,19 @@
+package org.nessus.didcomm.service
+
+import org.didcommx.didcomm.message.Message
+import org.nessus.didcomm.wallet.NessusWallet
+
+/**
+ * An Agent can create, send, receive DIDComMessages
+ */
+interface AgentService : Service {
+
+ companion object {
+ val type: Class = AgentService::class.java
+ }
+
+ override val type: Class
+ get() = Companion.type
+
+ fun createMessage(wallet: NessusWallet, type: String, body: Map = mapOf()) : Message
+}
diff --git a/core/src/main/kotlin/org/nessus/didcomm/service/ServiceRegistry.kt b/core/src/main/kotlin/org/nessus/didcomm/service/ServiceRegistry.kt
new file mode 100644
index 00000000..6d485612
--- /dev/null
+++ b/core/src/main/kotlin/org/nessus/didcomm/service/ServiceRegistry.kt
@@ -0,0 +1,20 @@
+package org.nessus.didcomm.service
+
+// [TODO] document all services
+interface Service {
+ val type: Class
+}
+
+// [TODO] document ServiceRegistry
+object ServiceRegistry {
+
+ private val registry : MutableMap = mutableMapOf()
+
+ fun getService(type : Class) : T {
+ return registry[type.name] as T
+ }
+
+ fun addService(service: T) {
+ registry[service.type.name] = service
+ }
+}
diff --git a/core/src/main/kotlin/org/nessus/didcomm/service/WalletService.kt b/core/src/main/kotlin/org/nessus/didcomm/service/WalletService.kt
new file mode 100644
index 00000000..7e3f184e
--- /dev/null
+++ b/core/src/main/kotlin/org/nessus/didcomm/service/WalletService.kt
@@ -0,0 +1,34 @@
+package org.nessus.didcomm.service
+
+import org.nessus.didcomm.wallet.NessusWallet
+import org.nessus.didcomm.wallet.WalletException
+import org.nessus.didcomm.wallet.WalletRegistry
+
+interface WalletService : Service {
+
+ companion object {
+ val type: Class = WalletService::class.java
+ val registry = WalletRegistry()
+ }
+
+ override val type: Class
+ get() = Companion.type
+
+ fun assertConfigValue(config: Map, key: String) : Any {
+ return config[key] ?: throw WalletException("No config value for: $key")
+ }
+
+ fun getConfigValue(config: Map, key: String) : Any? {
+ return config[key]
+ }
+
+ fun hasConfigValue(config: Map, key: String) : Boolean {
+ return config[key] != null
+ }
+
+ fun createWallet(config: Map): NessusWallet
+
+ fun publicDid(wallet: NessusWallet): String?
+
+ fun closeAndRemove(wallet: NessusWallet?)
+}
diff --git a/core/src/main/kotlin/org/nessus/didcomm/wallet/Did.kt b/core/src/main/kotlin/org/nessus/didcomm/wallet/Did.kt
index 600f39a9..d4f30f3e 100644
--- a/core/src/main/kotlin/org/nessus/didcomm/wallet/Did.kt
+++ b/core/src/main/kotlin/org/nessus/didcomm/wallet/Did.kt
@@ -4,7 +4,7 @@ import id.walt.crypto.KeyAlgorithm
val DEFAULT_KEY_ALGORITHM = KeyAlgorithm.EdDSA_Ed25519
-enum class DidMethod(mname : String) {
+enum class DidMethod(val mname : String) {
KEY("key"),
SOV("sov");
@@ -13,11 +13,11 @@ enum class DidMethod(mname : String) {
}
}
-open class Did(method: DidMethod) {
-}
-
-class DidKey() : Did(DidMethod.KEY) {
-}
-
-class DidSov() : Did(DidMethod.SOV) {
-}
+//open class Did(method: DidMethod) {
+//}
+//
+//class DidKey() : Did(DidMethod.KEY) {
+//}
+//
+//class DidSov() : Did(DidMethod.SOV) {
+//}
diff --git a/core/src/main/kotlin/org/nessus/didcomm/wallet/LedgerRole.kt b/core/src/main/kotlin/org/nessus/didcomm/wallet/LedgerRole.kt
new file mode 100644
index 00000000..92677feb
--- /dev/null
+++ b/core/src/main/kotlin/org/nessus/didcomm/wallet/LedgerRole.kt
@@ -0,0 +1,7 @@
+package org.nessus.didcomm.wallet
+
+enum class LedgerRole {
+ STEWARD,
+ TRUSTEE,
+ ENDORSER
+}
diff --git a/core/src/main/kotlin/org/nessus/didcomm/wallet/NessusWallet.kt b/core/src/main/kotlin/org/nessus/didcomm/wallet/NessusWallet.kt
new file mode 100644
index 00000000..1419a01c
--- /dev/null
+++ b/core/src/main/kotlin/org/nessus/didcomm/wallet/NessusWallet.kt
@@ -0,0 +1,23 @@
+
+package org.nessus.didcomm.wallet
+
+import org.nessus.didcomm.service.ServiceRegistry
+import org.nessus.didcomm.service.WalletService
+
+class WalletException(msg: String) : Exception(msg)
+
+/**
+ * A NessusWallet gives acces to wallet information as known by the agent.
+ */
+data class NessusWallet(
+ val walletId: String,
+ val walletName: String,
+ val accessToken: String? = null,
+) {
+ // [TODO] override toString with redacted values
+
+ fun publicDid() {
+ val walletService = ServiceRegistry.getService(WalletService.type)
+ walletService.publicDid(this)
+ }
+}
diff --git a/core/src/main/kotlin/org/nessus/didcomm/wallet/NessusWalletBuilder.kt b/core/src/main/kotlin/org/nessus/didcomm/wallet/NessusWalletBuilder.kt
new file mode 100644
index 00000000..07af73ca
--- /dev/null
+++ b/core/src/main/kotlin/org/nessus/didcomm/wallet/NessusWalletBuilder.kt
@@ -0,0 +1,37 @@
+package org.nessus.didcomm.wallet
+
+import org.nessus.didcomm.service.ServiceRegistry
+import org.nessus.didcomm.service.WalletService
+
+class NessusWalletBuilder(val name: String) {
+
+ private var ledgerRole: LedgerRole? = null
+ private var selfRegister: Boolean = false
+ private var trusteeWallet: NessusWallet? = null
+
+ fun ledgerRole(ledgerRole: LedgerRole): NessusWalletBuilder {
+ this.ledgerRole = ledgerRole
+ return this
+ }
+
+ fun selfRegisterNym(): NessusWalletBuilder {
+ this.selfRegister = true
+ return this
+ }
+
+ fun trusteeWallet(trusteeWallet: NessusWallet): NessusWalletBuilder {
+ this.trusteeWallet = trusteeWallet
+ return this
+ }
+
+ fun build(): NessusWallet {
+ val walletService = ServiceRegistry.getService(WalletService.type)
+ val config: Map = mapOf(
+ "walletName" to name,
+ "ledgerRole" to ledgerRole,
+ "selfRegister" to selfRegister,
+ "trusteeWallet" to trusteeWallet,
+ )
+ return walletService.createWallet(config)
+ }
+}
diff --git a/core/src/main/kotlin/org/nessus/didcomm/wallet/Wallet.kt b/core/src/main/kotlin/org/nessus/didcomm/wallet/OldNessusWallet.kt
similarity index 90%
rename from core/src/main/kotlin/org/nessus/didcomm/wallet/Wallet.kt
rename to core/src/main/kotlin/org/nessus/didcomm/wallet/OldNessusWallet.kt
index 2d0ad4b8..74cda326 100644
--- a/core/src/main/kotlin/org/nessus/didcomm/wallet/Wallet.kt
+++ b/core/src/main/kotlin/org/nessus/didcomm/wallet/OldNessusWallet.kt
@@ -11,13 +11,11 @@ import mu.KotlinLogging
import java.security.SecureRandom
-class WalletError(message: String) : Exception(message)
-
fun ByteArray.encodeBase58(): String = Base58.encode(this)
fun String.decodeBase58(): ByteArray = Base58.decode(this)
-class Wallet {
+class OldNessusWallet {
private val log = KotlinLogging.logger {}
@@ -33,7 +31,7 @@ class Wallet {
// validate key_type
if (keyAlgorithm !in didMethod.supportedAlgorithms())
- throw WalletError("Invalid key type $keyType for method $method")
+ throw WalletException("Invalid key type $keyType for method $method")
var secureRandom = SecureRandom.getInstance("SHA1PRNG")
secureRandom.setSeed(seedBytes)
@@ -69,7 +67,7 @@ class Wallet {
SecureRandom().nextBytes(byteArray);
}
if (byteArray.size != 32) {
- throw WalletError("Seed value must be 32 bytes in length")
+ throw WalletException("Seed value must be 32 bytes in length")
}
return byteArray
}
diff --git a/core/src/main/kotlin/org/nessus/didcomm/wallet/WalletRegistry.kt b/core/src/main/kotlin/org/nessus/didcomm/wallet/WalletRegistry.kt
new file mode 100644
index 00000000..518537b0
--- /dev/null
+++ b/core/src/main/kotlin/org/nessus/didcomm/wallet/WalletRegistry.kt
@@ -0,0 +1,34 @@
+package org.nessus.didcomm.wallet
+
+class WalletRegistry {
+
+ private val walletsCache: MutableMap = mutableMapOf()
+
+ fun walletNames(): Set {
+ return walletsCache.keys
+ }
+
+ fun putWallet(wallet: NessusWallet) {
+ walletsCache[wallet.walletId] = wallet
+ }
+
+ fun removeWallet(walletId: String) {
+ walletsCache.remove(walletId)
+ }
+
+ fun wallets(): Set {
+ return walletsCache.values.toSet()
+ }
+
+ fun getWallet(walletId: String): NessusWallet? {
+ return walletsCache[walletId]
+ }
+
+ fun getWalletName(walletId: String): String? {
+ return getWallet(walletId)?.walletName
+ }
+
+ fun getWalletByName(walletName: String): NessusWallet? {
+ return walletsCache.values.firstOrNull { w -> w.walletName == walletName }
+ }
+}
diff --git a/core/src/test/kotlin/org/nessus/didcomm/test/did/DidKeyTest.kt b/core/src/test/kotlin/org/nessus/didcomm/test/did/DidKeyTest.kt
index 71f895a7..91c11401 100644
--- a/core/src/test/kotlin/org/nessus/didcomm/test/did/DidKeyTest.kt
+++ b/core/src/test/kotlin/org/nessus/didcomm/test/did/DidKeyTest.kt
@@ -3,6 +3,7 @@ package org.nessus.didcomm.test.did
import id.walt.crypto.KeyAlgorithm
import id.walt.services.crypto.TinkCryptoService
import org.junit.jupiter.api.Test
+import kotlin.test.assertTrue
class DidKeyTest {
@@ -17,5 +18,6 @@ class DidKeyTest {
val keyId = tinkCryptoService.generateKey(KeyAlgorithm.EdDSA_Ed25519)
val sig = tinkCryptoService.sign(keyId, data)
val res = tinkCryptoService.verify(keyId, sig, data)
+ assertTrue(res)
}
-}
\ No newline at end of file
+}
diff --git a/core/src/test/kotlin/org/nessus/didcomm/test/model/Fixtures.kt b/core/src/test/kotlin/org/nessus/didcomm/test/model/Fixtures.kt
index 75ea5cd6..96feda32 100644
--- a/core/src/test/kotlin/org/nessus/didcomm/test/model/Fixtures.kt
+++ b/core/src/test/kotlin/org/nessus/didcomm/test/model/Fixtures.kt
@@ -1,8 +1,6 @@
package org.nessus.didcomm.test.model
-import org.didcommx.didcomm.message.Attachment
-import org.didcommx.didcomm.message.Message
-import org.nessus.didcomm.model.OutOfBandInvitationV2
+import org.nessus.didcomm.model.MessageReader
class OutOfBand {
@@ -11,20 +9,97 @@ class OutOfBand {
const val ALICE_DID = "did:example:alice"
const val FABER_DID = "did:example:faber"
- private const val ID = "1234567890"
- private const val TYPE = OutOfBandInvitationV2.type
+ val FABER_OUT_OF_BAND_INVITATION = MessageReader.fromJson("""
+ {
+ "type": "https://didcomm.org/out-of-band/2.0/invitation",
+ "id": "1234567890",
+ "from": "did:example:faber",
+ "body": {
+ "goal_code": "issue-vc",
+ "goal": "To issue a Faber College Graduate credential",
+ "accept": [
+ "didcomm/v2",
+ "didcomm/aip2;env=rfc587"
+ ]
+ },
+ "attachments": [
+ {
+ "id": "request-0",
+ "media_type": "application/json",
+ "data": {
+ "json": {"protocol message": "content"}
+ }
+ }
+ ]
+ }
+ """.trimIndent())
- val OUT_OF_BAND_INVITATION = Message.builder(ID, mapOf(
- "goal_code" to "issue-vc",
- "goal" to "To issue a Faber College Graduate credential",
- "accept" to listOf("didcomm/v2", "didcomm/aip2;env=rfc587")),
- TYPE)
- .from(FABER_DID)
- .createdTime(1516269022)
- .expiresTime(1516385931)
- .attachments(listOf(
- Attachment.builder(
- "request-0", Attachment.Data.parse(mapOf("base64" to "qwerty"))).build()))
- .build()
+ val FABER_OUT_OF_BAND_INVITATION_WRAPPED = MessageReader.fromJson("""
+ {
+ "type": "https://didcomm.org/out-of-band/2.0/invitation",
+ "id": "1234567890",
+ "from": "did:example:faber",
+ "body": {
+ "goal_code": "issue-vc",
+ "goal": "To issue a Faber College Graduate credential",
+ "accept": [
+ "didcomm/v2",
+ "didcomm/aip2;env=rfc587"
+ ]
+ },
+ "attachments": [
+ {
+ "id": "0fa20500-2677-4e72-a8b3-dfff26ae2044",
+ "media_type": "application/json",
+ "data": {
+ "json": {
+ "invitation": {
+ "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/invitation",
+ "@id": "0fa20500-2677-4e72-a8b3-dfff26ae2044",
+ "label": "Aries Cloud Agent",
+ "services": [
+ {
+ "id": "#inline",
+ "type": "did-communication",
+ "recipientKeys": [
+ "did:key:z6MkqQUeLBtuYvef7BS1wvUjCJj1hskcs94tY38dKy1fzCsL"
+ ],
+ "serviceEndpoint": "http://localhost:8030"
+ }
+ ],
+ "handshake_protocols": [
+ "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0"
+ ]
+ },
+ "state": "initial",
+ "oob_id": "f2f24452-f43c-4a08-bf5c-5e6734d3f0db",
+ "invi_msg_id": "0fa20500-2677-4e72-a8b3-dfff26ae2044"
+ }
+ }
+ }
+ ]
+ }
+ """.trimIndent())
+
+ val ALICE_OUT_OF_BAND_INVITATION = MessageReader.fromJson("""
+ {
+ "type": "https://didcomm.org/out-of-band/2.0/invitation",
+ "id": "69212a3a-d068-4f9d-a2dd-4741bca89af3",
+ "from": "did:example:alice",
+ "body": {
+ "goal_code": "",
+ "goal": ""
+ },
+ "attachments": [
+ {
+ "id": "request-0",
+ "media_type": "application/json",
+ "data": {
+ "base64": "qwerty"
+ }
+ }
+ ]
+ }
+ """.trimIndent())
}
}
diff --git a/core/src/test/kotlin/org/nessus/didcomm/test/model/OutOfBandInvitationTest.kt b/core/src/test/kotlin/org/nessus/didcomm/test/model/OutOfBandInvitationTest.kt
index 366bf8e3..41db202b 100644
--- a/core/src/test/kotlin/org/nessus/didcomm/test/model/OutOfBandInvitationTest.kt
+++ b/core/src/test/kotlin/org/nessus/didcomm/test/model/OutOfBandInvitationTest.kt
@@ -1,15 +1,15 @@
package org.nessus.didcomm.test.model
import mu.KotlinLogging
+import org.didcommx.didcomm.message.Attachment
import org.didcommx.didcomm.message.Message
+import org.hyperledger.acy_py.generated.model.InvitationRecord
import org.junit.jupiter.api.Test
-import org.nessus.didcomm.model.MessageParser
+import org.nessus.didcomm.model.MessageReader
import org.nessus.didcomm.model.MessageWriter
-import org.nessus.didcomm.model.OutOfBandInvitationV2
-import kotlin.test.Ignore
+import org.nessus.didcomm.model.OutOfBandInvitation
import kotlin.test.assertEquals
-@Ignore
class OutOfBandInvitationTest {
private val log = KotlinLogging.logger {}
@@ -17,18 +17,41 @@ class OutOfBandInvitationTest {
@Test
fun testOutOfBandInvitation() {
- val exp: Message = OutOfBand.OUT_OF_BAND_INVITATION
- val expBody = OutOfBandInvitationV2.fromBody(exp.body)
+ val exp: Message = OutOfBand.FABER_OUT_OF_BAND_INVITATION
+ val expBody = OutOfBandInvitation.fromBody(exp.body)
val expJson: String = MessageWriter.toJson(exp)
log.info("exp: {}", expJson)
- val was = MessageParser.fromJson(expJson)
+ val was = MessageReader.fromJson(expJson)
log.info("was: {}", MessageWriter.toJson(was))
assertEquals(exp, was)
- val wasBody = OutOfBandInvitationV2.fromBody(was.body)
+ val wasBody = OutOfBandInvitation.fromBody(was.body)
log.info("body: {}", MessageWriter.toJson(wasBody))
assertEquals(expBody, wasBody)
}
+ @Test
+ fun testOutOfBandEncoding() {
+ val exp: Message = OutOfBand.ALICE_OUT_OF_BAND_INVITATION
+ val expJson: String = MessageWriter.toJson(exp)
+ log.info("exp: {}", expJson)
+
+ val base64URLEncoded = MessageWriter.toBase64URL(exp)
+ log.info("enc: {}", base64URLEncoded)
+ assertEquals("eyJpZCI6IjY5MjEy", base64URLEncoded.substring(0, 16))
+ }
+
+ @Test
+ fun testWrappedOutOfBandInvitation() {
+
+ val exp: Message = OutOfBand.FABER_OUT_OF_BAND_INVITATION_WRAPPED
+ val expJson: String = MessageWriter.toJson(exp)
+ log.info("exp: {}", expJson)
+
+ val att0: Attachment = exp.attachments?.get(0)!!
+ val invJson = MessageWriter.toJson(att0.data.toJSONObject()["json"]!!)
+ val invRec: InvitationRecord = MessageReader.fromJson(invJson, InvitationRecord::class.java)
+ assertEquals(att0.id, invRec.inviMsgId)
+ }
}
diff --git a/core/src/test/kotlin/org/nessus/didcomm/test/wallet/InMemoryWalletTest.kt b/core/src/test/kotlin/org/nessus/didcomm/test/wallet/InMemoryWalletTest.kt
index cbe79e43..119487b1 100644
--- a/core/src/test/kotlin/org/nessus/didcomm/test/wallet/InMemoryWalletTest.kt
+++ b/core/src/test/kotlin/org/nessus/didcomm/test/wallet/InMemoryWalletTest.kt
@@ -2,7 +2,6 @@ package org.nessus.didcomm.test.wallet
import mu.KotlinLogging
import org.junit.jupiter.api.Test
-import org.nessus.didcomm.wallet.Wallet
import kotlin.test.Ignore
@Ignore
@@ -14,6 +13,6 @@ class InMemoryWalletTest {
fun testCreateLocalDID() {
// Wallet().createLocalDID("sov")
- Wallet().createLocalDID("sov", seed = "000000000000000000000000Trustee1")
+ // Wallet().createLocalDID("sov", seed = "000000000000000000000000Trustee1")
}
}
diff --git a/core/src/test/resources/logback-test.xml b/core/src/test/resources/logback-test.xml
index 913be84c..47c37fc2 100644
--- a/core/src/test/resources/logback-test.xml
+++ b/core/src/test/resources/logback-test.xml
@@ -5,6 +5,7 @@
%date %level [%thread] %logger{10} [%file:%line] -%kvp- %msg%n
+ FALSE
@@ -12,7 +13,7 @@
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n
- WARN
+ INFO
diff --git a/demo/README.md b/demo/README.md
new file mode 100644
index 00000000..359abf15
--- /dev/null
+++ b/demo/README.md
@@ -0,0 +1,51 @@
+## Nessus DIDComm - Proof of Concept
+
+This POC describes the initial scope of Nessus DIDComm - an SSI agent that uses [DIDComm-2.0 ](https://identity.foundation/didcomm-messaging/spec/v2.0/)
+to communicate with other agents (e.g. [Aries Cloud Agent Python](https://github.com/hyperledger/aries-cloudagent-python))
+
+### Meet Alice, Faber and Acme
+
+Alice, a citizen of British Columbia has graduated from Faber College some time ago. Faber College, well situated at the heart of emerging tech,
+has since adopted a form of digital transcripts that it now offers to its former students. These transcripts are verifiable credentials,
+which are a key feature of [Self Sovereign Identity](https://www.manning.com/books/self-sovereign-identity). Alice has since moved to Munich, which
+provides access to [EBSI](https://ec.europa.eu/digital-building-blocks/wikis/display/EBSI/Home) services for its citizens.
+
+In SSI terms, Faber is an **Issuer** of verifiable credentials (VC) and Alice
+is a **Holder**. Alice may later apply for a job with Acme Corp, which then
+becomes a **Verifier** in our [Trust Triangle](https://academy.affinidi.com/what-is-the-trust-triangle-9a9caf36b321)
+
+### Agent Communication
+
+All three parties need to agree on reliable/secure communication, which [DIDComm Messaging](https://identity.foundation/didcomm-messaging/spec/v2.0) is
+well suited for. Faber uses [AcyPy](https://github.com/hyperledger/aries-cloudagent-python) and registers the necessary cryptographic material on the
+[VON Network](https://github.com/bcgov/von-network). Alice is not known to the VON Network, neither does she have access to a [Hyperledger Aries](https://aries-interop.info/) compliant agent.
+All parties communicate via DIDComm alone and use common standards to exchange information.
+
+### POC Milestones
+
+1. [Out of Band Invitation](https://identity.foundation/didcomm-messaging/spec/#out-of-band-messages) from Faber to Alice and vice versa
+2. [DID Exchange](https://github.com/hyperledger/aries-rfcs/tree/main/features/0023-did-exchange) between Faber & Alice
+3. Alice uses [did:key](https://w3c-ccg.github.io/did-method-key/) instead of [did:sov](https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html)
+4. [Plaintext Message](https://identity.foundation/didcomm-messaging/spec/#didcomm-plaintext-messages) exchange
+5. [Signed Message](https://identity.foundation/didcomm-messaging/spec/#didcomm-signed-messages) exchange
+6. [Encrypted Message](https://identity.foundation/didcomm-messaging/spec/#didcomm-encrypted-messages) exchange
+7. Anything else?
+
+### Further Work
+
+* Support for [W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model-2.0/)
+* Support for [Aries Verifiable Credentials](https://github.com/hyperledger/aries-rfcs/tree/main/features/0453-issue-credential-v2) (maybe)
+* Closely work with Aries on [AIP3.0](https://hackmd.io/_Kkl9ClTRBu8W4UmZVGdUQ)
+* Credential revocation
+* Anything else?
+
+### Tech Stack
+
+* Nessus DIDComm is written in [Kotlin](https://kotlinlang.org/)
+* For DIDComm Messages it uses [didcomm-jvm](https://github.com/sicpa-dlab/didcomm-jvm) from [SICPA](https://www.sicpa.com/)
+* For integration with AcaPy it uses [acapy-java-client](https://github.com/hyperledger-labs/acapy-java-client)
+* For integration with EBSI is uses [waltid-ssikit](https://github.com/walt-id/waltid-ssikit)
+* Future integration with [Apache Camel](https://camel.apache.org/) to supplement/replace [nessus-aries](https://github.com/tdiesler/nessus-aries)
+
+
+
diff --git a/docker-compose-single.yml b/docker-compose-single.yml
new file mode 100644
index 00000000..7f8bb97d
--- /dev/null
+++ b/docker-compose-single.yml
@@ -0,0 +1,203 @@
+# ---------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ---------------------------------------------------------------------------
+
+version: '3'
+
+networks:
+ von-network:
+
+services:
+
+ #
+ # Webserver
+ #
+ webserver:
+ image: nessusio/von-network:${VON_NETWORK_VERSION:-1.7.2}
+ container_name: indy-webserver
+ command: bash -c 'sleep 10 && ./scripts/start_webserver.sh'
+ environment:
+ - IP=${IP}
+ - IPS=${IPS}
+ - DOCKERHOST=${DOCKERHOST:-host.docker.internal}
+ - LOG_LEVEL=${LOG_LEVEL:-info}
+ - RUST_LOG=${RUST_LOG:-warn}
+ - GENESIS_URL=${GENESIS_URL}
+ - ANONYMOUS=${ANONYMOUS}
+ - LEDGER_SEED=${LEDGER_SEED}
+ - LEDGER_CACHE_PATH=${LEDGER_CACHE_PATH}
+ - MAX_FETCH=${MAX_FETCH:-50000}
+ - RESYNC_TIME=${RESYNC_TIME:-120}
+ - REGISTER_NEW_DIDS=${REGISTER_NEW_DIDS:-True}
+ - LEDGER_INSTANCE_NAME=${LEDGER_INSTANCE_NAME:-localhost}
+ - WEB_ANALYTICS_SCRIPT=${WEB_ANALYTICS_SCRIPT}
+ - INFO_SITE_TEXT=${INFO_SITE_TEXT}
+ - INFO_SITE_URL=${INFO_SITE_URL}
+ extra_hosts:
+ - host.docker.internal:host-gateway
+ networks:
+ - von-network
+ ports:
+ - ${WEB_SERVER_HOST_PORT:-9000}:8000
+ volumes:
+ - webserver-cli:/home/indy/.indy-cli
+ - webserver-ledger:/home/indy/ledger
+
+ #
+ # Nodes
+ #
+ node1:
+ image: nessusio/von-network:${VON_NETWORK_VERSION:-1.7.2}
+ container_name: indy-node1
+ command: ./scripts/start_node.sh 1
+ networks:
+ - von-network
+ ports:
+ - 9701:9701
+ - 9702:9702
+ environment:
+ - IP=${IP}
+ - IPS=${IPS}
+ - DOCKERHOST=${DOCKERHOST:-host.docker.internal}
+ - LOG_LEVEL=${LOG_LEVEL:-info}
+ - RUST_LOG=${RUST_LOG:-warn}
+ extra_hosts:
+ - host.docker.internal:host-gateway
+ volumes:
+ - node1-data:/home/indy/ledger
+
+ node2:
+ image: nessusio/von-network:${VON_NETWORK_VERSION:-1.7.2}
+ container_name: indy-node2
+ command: ./scripts/start_node.sh 2
+ networks:
+ - von-network
+ ports:
+ - 9703:9703
+ - 9704:9704
+ environment:
+ - IP=${IP}
+ - IPS=${IPS}
+ - DOCKERHOST=${DOCKERHOST:-host.docker.internal}
+ - LOG_LEVEL=${LOG_LEVEL:-info}
+ - RUST_LOG=${RUST_LOG:-warn}
+ extra_hosts:
+ - host.docker.internal:host-gateway
+ volumes:
+ - node2-data:/home/indy/ledger
+
+ node3:
+ image: nessusio/von-network:${VON_NETWORK_VERSION:-1.7.2}
+ container_name: indy-node3
+ command: ./scripts/start_node.sh 3
+ networks:
+ - von-network
+ ports:
+ - 9705:9705
+ - 9706:9706
+ environment:
+ - IP=${IP}
+ - IPS=${IPS}
+ - DOCKERHOST=${DOCKERHOST:-host.docker.internal}
+ - LOG_LEVEL=${LOG_LEVEL:-info}
+ - RUST_LOG=${RUST_LOG:-warn}
+ extra_hosts:
+ - host.docker.internal:host-gateway
+ volumes:
+ - node3-data:/home/indy/ledger
+
+ node4:
+ image: nessusio/von-network:${VON_NETWORK_VERSION:-1.7.2}
+ container_name: indy-node4
+ command: ./scripts/start_node.sh 4
+ networks:
+ - von-network
+ ports:
+ - 9707:9707
+ - 9708:9708
+ environment:
+ - IP=${IP}
+ - IPS=${IPS}
+ - DOCKERHOST=${DOCKERHOST:-host.docker.internal}
+ - LOG_LEVEL=${LOG_LEVEL:-info}
+ - RUST_LOG=${RUST_LOG:-warn}
+ extra_hosts:
+ - host.docker.internal:host-gateway
+ volumes:
+ - node4-data:/home/indy/ledger
+
+ tails-server:
+ image: nessusio/indy-tails-server:${TAILS_SERVER_VERSION:-1.0.0}
+ container_name: tails-server
+ ports:
+ - 6543:6543
+ networks:
+ - von-network
+ extra_hosts:
+ - host.docker.internal:host-gateway
+ command: >
+ tails-server
+ --host 0.0.0.0
+ --port 6543
+ --storage-path ${STORAGE_PATH:-/tmp/tails-files}
+ --log-level ${LOG_LEVEL:-info}
+
+ acapy:
+ image: nessusio/aries-cloudagent-python:${ACAPY_VERSION:-0.7.5}
+ container_name: acapy
+ ports:
+ - ${ACAPY_USER_PORT:-8030}:${ACAPY_USER_PORT:-8030}
+ - ${ACAPY_ADMIN_PORT:-8031}:${ACAPY_ADMIN_PORT:-8031}
+ networks:
+ - von-network
+ extra_hosts:
+ - host.docker.internal:host-gateway
+ command: >
+ start
+ --genesis-url http://${DOCKERHOST:-host.docker.internal}:9000/genesis
+ --endpoint http://${ACAPY_HOSTNAME:-localhost}:${ACAPY_USER_PORT:-8030}
+ --inbound-transport http 0.0.0.0 ${ACAPY_USER_PORT:-8030}
+ --outbound-transport http
+ --tails-server-base-url http://tails-server:6543
+ --admin 0.0.0.0 ${ACAPY_ADMIN_PORT:-8031}
+ --admin-api-key ${ACAPY_ADMIN_API_KEY:-adminkey}
+ --seed 000000000000000000000000Trustee1
+ --wallet-storage-type default
+ --wallet-key trusteewkey
+ --wallet-name trustee
+ --wallet-type indy
+ --storage-type indy
+ --recreate-wallet
+ --auto-provision
+ --auto-ping-connection
+ --auto-accept-requests
+ --log-level info
+ depends_on:
+ - node1
+ - node2
+ - node3
+ - node4
+ - webserver
+ - tails-server
+
+volumes:
+ webserver-cli:
+ webserver-ledger:
+ node1-data:
+ node2-data:
+ node3-data:
+ node4-data:
+ nodes-data:
diff --git a/docker-compose.yml b/docker-compose.yml
index 7f8bb97d..61ae3c47 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -174,6 +174,9 @@ services:
--tails-server-base-url http://tails-server:6543
--admin 0.0.0.0 ${ACAPY_ADMIN_PORT:-8031}
--admin-api-key ${ACAPY_ADMIN_API_KEY:-adminkey}
+ --jwt-secret ${ACAPY_MULTITENANT_JWT_SECRET:-jwtsecret}
+ --multitenant
+ --multitenant-admin
--seed 000000000000000000000000Trustee1
--wallet-storage-type default
--wallet-key trusteewkey
diff --git a/itests/pom.xml b/itests/pom.xml
index 2306ee78..f531d9fc 100644
--- a/itests/pom.xml
+++ b/itests/pom.xml
@@ -38,9 +38,13 @@
org.nessus.didcomm
- nessus-didcomm-core
+ nessus-didcomm-agent
${project.version}
+
+ network.idu.acapy
+ aries-client-python
+
@@ -58,6 +62,12 @@
kotlin-test-junit5
test
+
+ org.nessus.didcomm
+ nessus-didcomm-agent
+ 1.0-SNAPSHOT
+ test
+
diff --git a/itests/src/test/kotlin/org/nessus/didcomm/itest/AbstractAriesTest.kt b/itests/src/test/kotlin/org/nessus/didcomm/itest/AbstractAriesTest.kt
index 76a978fe..4b5d841a 100644
--- a/itests/src/test/kotlin/org/nessus/didcomm/itest/AbstractAriesTest.kt
+++ b/itests/src/test/kotlin/org/nessus/didcomm/itest/AbstractAriesTest.kt
@@ -1,56 +1,14 @@
package org.nessus.didcomm.itest
-import com.google.gson.JsonSyntaxException
import mu.KotlinLogging
-import okhttp3.OkHttpClient
-import okhttp3.logging.HttpLoggingInterceptor
-import org.hyperledger.aries.AriesClient
-import org.hyperledger.aries.config.GsonConfig
-import org.nessus.didcomm.aries.AgentConfiguration
-import java.util.concurrent.TimeUnit
+import org.nessus.didcomm.service.ServiceRegistry
+import org.nessus.didcomm.service.WalletService
abstract class AbstractAriesTest {
val log = KotlinLogging.logger {}
- private val gson = GsonConfig.defaultConfig()
-
- private fun messageLoggingInterceptor(): HttpLoggingInterceptor {
- val pretty = GsonConfig.prettyPrinter()
- val logging = HttpLoggingInterceptor { msg: String ->
- if (log.isDebugEnabled && msg.isNotEmpty()) {
- if (msg.startsWith("{")) {
- try {
- val json: Any = gson.fromJson(msg, Any::class.java)
- log.debug("\n{}", pretty.toJson(json))
- } catch (e: JsonSyntaxException) {
- log.debug("{}", msg)
- }
- } else {
- log.debug("{}", msg)
- }
- }
- }
- logging.level = HttpLoggingInterceptor.Level.BODY
- logging.redactHeader("X-API-Key")
- logging.redactHeader("Authorization")
- return logging
- }
-
- fun adminClient(loggingInterceptor: HttpLoggingInterceptor? = null): AriesClient {
- val loggingInterceptor = loggingInterceptor ?: messageLoggingInterceptor()
- val httpClient = OkHttpClient.Builder()
- .writeTimeout(60, TimeUnit.SECONDS)
- .readTimeout(60, TimeUnit.SECONDS)
- .connectTimeout(60, TimeUnit.SECONDS)
- .callTimeout(60, TimeUnit.SECONDS)
- .addInterceptor(loggingInterceptor)
- .build()
- val config = AgentConfiguration.defaultConfiguration
- return AriesClient.builder()
- .url(config.adminUrl)
- .apiKey(config.apiKey)
- .client(httpClient)
- .build()
+ fun walletService(): WalletService {
+ return ServiceRegistry.getService(WalletService.type)
}
}
diff --git a/itests/src/test/kotlin/org/nessus/didcomm/itest/Scenario001Test.kt b/itests/src/test/kotlin/org/nessus/didcomm/itest/Scenario001Test.kt
new file mode 100644
index 00000000..bfab127b
--- /dev/null
+++ b/itests/src/test/kotlin/org/nessus/didcomm/itest/Scenario001Test.kt
@@ -0,0 +1,82 @@
+package org.nessus.didcomm.itest
+
+import mu.KotlinLogging
+import org.didcommx.didcomm.message.Attachment
+import org.didcommx.didcomm.message.Message
+import org.hyperledger.acy_py.generated.model.InvitationRecord
+import org.junit.jupiter.api.AfterAll
+import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.Test
+import org.nessus.didcomm.agent.aries.AriesAgentService
+import org.nessus.didcomm.agent.aries.AriesWalletService
+import org.nessus.didcomm.model.MessageReader
+import org.nessus.didcomm.model.MessageType.Companion.OUT_OF_BAND_INVITATION
+import org.nessus.didcomm.model.MessageWriter
+import org.nessus.didcomm.service.AgentService
+import org.nessus.didcomm.service.ServiceRegistry
+import org.nessus.didcomm.service.WalletService
+import org.nessus.didcomm.wallet.LedgerRole
+import org.nessus.didcomm.wallet.NessusWallet
+import org.nessus.didcomm.wallet.NessusWalletBuilder
+import kotlin.test.assertEquals
+
+class Scenario001Test : AbstractAriesTest() {
+
+ companion object {
+ private val log = KotlinLogging.logger {}
+
+ var governmentWallet: NessusWallet? = null
+ var faberWallet: NessusWallet? = null
+
+ @BeforeAll
+ @JvmStatic
+ internal fun beforeAll() {
+ ServiceRegistry.addService(AriesAgentService())
+ ServiceRegistry.addService(AriesWalletService())
+
+ // Create initial TRUSTEE Wallet
+ governmentWallet = NessusWalletBuilder("Government")
+ .ledgerRole(LedgerRole.TRUSTEE)
+ .selfRegisterNym()
+ .build()
+
+ // Onboard an ENDORSER wallet
+ faberWallet = NessusWalletBuilder("Faber")
+ .trusteeWallet(governmentWallet!!)
+ .ledgerRole(LedgerRole.ENDORSER)
+ .build()
+
+ val did = faberWallet?.publicDid()
+ log.info("Faber: Public {}", did)
+ }
+
+ @AfterAll
+ @JvmStatic
+ internal fun afterAll() {
+ val walletService = ServiceRegistry.getService(WalletService.type)
+ walletService.closeAndRemove(faberWallet)
+ walletService.closeAndRemove(governmentWallet)
+ }
+ }
+
+ @Test
+ fun testFaberInvitesAlice() {
+
+ // Create the OOB Invitation through the Agent
+ val body = MessageWriter.toMutableMap("""
+ {
+ "goal_code": "did-exchange",
+ "goal": "Faber College invites you for a DID exchange",
+ "accept": [ "didcomm/v2" ]
+ }
+ """.trimIndent())
+ val agent = ServiceRegistry.getService(AgentService.type)
+ val msg: Message = agent.createMessage(faberWallet!!, OUT_OF_BAND_INVITATION, body)
+
+ // Verify the DCV2 message
+ val att0: Attachment = msg.attachments?.get(0)!!
+ val invJson = MessageWriter.toJson(att0.data.toJSONObject()["json"]!!)
+ val invRec: InvitationRecord = MessageReader.fromJson(invJson, InvitationRecord::class.java)
+ assertEquals(att0.id, invRec.inviMsgId)
+ }
+}
diff --git a/itests/src/test/kotlin/org/nessus/didcomm/itest/features/Scenario001Test.kt b/itests/src/test/kotlin/org/nessus/didcomm/itest/features/Scenario001Test.kt
deleted file mode 100644
index c2f86528..00000000
--- a/itests/src/test/kotlin/org/nessus/didcomm/itest/features/Scenario001Test.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.nessus.didcomm.itest.features
-
-import org.hyperledger.aries.api.out_of_band.InvitationCreateRequest
-import org.junit.jupiter.api.Test
-import org.nessus.didcomm.itest.AbstractAriesTest
-
-class Scenario001Test : AbstractAriesTest() {
-
- @Test
- fun testFaberInvitesAlice() {
- val client = adminClient()
- val invitationRequest = InvitationCreateRequest.builder()
- .alias("Faber")
- .build()
- val invitationResponse = client.outOfBandCreateInvitation(invitationRequest, null).get()
- log.info("{}", invitationResponse)
- }
-}
diff --git a/itests/src/test/resources/logback-test.xml b/itests/src/test/resources/logback-test.xml
index 913be84c..47c37fc2 100644
--- a/itests/src/test/resources/logback-test.xml
+++ b/itests/src/test/resources/logback-test.xml
@@ -5,6 +5,7 @@
%date %level [%thread] %logger{10} [%file:%line] -%kvp- %msg%n
+ FALSE
@@ -12,7 +13,7 @@
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n
- WARN
+ INFO
diff --git a/pom.xml b/pom.xml
index 3328e4b3..1a063d52 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,9 +1,9 @@
-
+
4.0.0
+ Nessus DIDComm
+
nessus-didcomm
org.nessus.didcomm
1.0-SNAPSHOT
@@ -112,6 +112,13 @@
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+
+
+
src/main/kotlin
src/test/kotlin
diff --git a/services/agent/pom.xml b/services/agent/pom.xml
index 1f462b44..2e62e6d5 100644
--- a/services/agent/pom.xml
+++ b/services/agent/pom.xml
@@ -36,6 +36,27 @@
jar
+
+ org.nessus.didcomm
+ nessus-didcomm-core
+ ${project.version}
+
+
+ io.github.microutils
+ kotlin-logging-jvm
+
+
+ network.idu.acapy
+ aries-client-python
+
+
+ org.didcommx
+ didcomm
+
+
+ org.slf4j
+ slf4j-api
+
diff --git a/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/AriesAgentService.kt b/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/AriesAgentService.kt
new file mode 100644
index 00000000..fe199d21
--- /dev/null
+++ b/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/AriesAgentService.kt
@@ -0,0 +1,77 @@
+package org.nessus.didcomm.agent.aries
+
+import mu.KotlinLogging
+import org.didcommx.didcomm.message.Attachment
+import org.didcommx.didcomm.message.Message
+import org.didcommx.didcomm.message.MessageBuilder
+import org.didcommx.didcomm.utils.idGeneratorDefault
+import org.hyperledger.acy_py.generated.model.InvitationRecord
+import org.hyperledger.aries.AriesClient
+import org.hyperledger.aries.api.connection.ConnectionRecord
+import org.hyperledger.aries.api.out_of_band.CreateInvitationFilter
+import org.hyperledger.aries.api.out_of_band.InvitationCreateRequest
+import org.nessus.didcomm.model.MessageType.Companion.OUT_OF_BAND_INVITATION
+import org.nessus.didcomm.model.MessageWriter
+import org.nessus.didcomm.service.AgentService
+import org.nessus.didcomm.wallet.NessusWallet
+import org.slf4j.event.Level
+
+class UnsupportedMessageType(msg: String) : Exception(msg)
+
+class AriesAgentService : AgentService {
+
+ private val log = KotlinLogging.logger {}
+
+ companion object {
+ private val interceptorLogLevel = Level.DEBUG
+ fun adminClient(): AriesClient {
+ return AriesClientFactory.adminClient(level=interceptorLogLevel)
+ }
+ fun walletClient(wallet: NessusWallet): AriesClient {
+ return AriesClientFactory.walletClient(wallet=wallet, level=interceptorLogLevel)
+ }
+ }
+
+ override fun createMessage(wallet: NessusWallet, type: String, body: Map) : Message {
+ val message = when(type) {
+ OUT_OF_BAND_INVITATION -> createOutOfBandInvitation(wallet, body)
+ else -> throw UnsupportedMessageType(type)
+ }
+ if (log.isDebugEnabled) {
+ log.debug("{}", MessageWriter.toJson(message, true))
+ }
+ return message
+ }
+
+// fun sendMessage(msg: Message) {
+//
+// }
+
+ private fun createId(): String {
+ return idGeneratorDefault()
+ }
+
+ private fun createOutOfBandInvitation(wallet: NessusWallet, body: Map): Message {
+
+ // Make the call to AcaPy
+ val reqObj = InvitationCreateRequest.builder()
+ .handshakeProtocols(listOf(ConnectionRecord.ConnectionProtocol.DID_EXCHANGE_V1.value))
+ .build()
+ val filterObj = CreateInvitationFilter.builder().build()
+ val invRecord: InvitationRecord = walletClient(wallet).outOfBandCreateInvitation(reqObj, filterObj).get()
+ val invRecordMap = MessageWriter.toMutableMap(invRecord)
+
+ // Get the msgId and remove the encoded URL that points to AcaPy
+ val msgId = invRecordMap["invi_msg_id"] as String
+ invRecordMap.remove("invitation_url")
+
+ // Prepare the attachment
+ val datMap = mapOf("json" to invRecordMap)
+ val att0 = Attachment.builder(msgId, Attachment.Data.parse(datMap)).build()
+
+ // Create the DIDcomm message
+ return MessageBuilder(createId(), body, OUT_OF_BAND_INVITATION)
+ .attachments(listOf(att0)).build()
+ }
+
+}
diff --git a/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/AriesClientFactory.kt b/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/AriesClientFactory.kt
new file mode 100644
index 00000000..d58f8f5e
--- /dev/null
+++ b/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/AriesClientFactory.kt
@@ -0,0 +1,141 @@
+package org.nessus.didcomm.agent.aries
+
+import com.google.gson.JsonSyntaxException
+import mu.KotlinLogging
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import org.hyperledger.aries.AriesClient
+import org.hyperledger.aries.config.GsonConfig
+import org.nessus.didcomm.wallet.NessusWallet
+import org.slf4j.event.Level
+import java.util.concurrent.TimeUnit
+
+class AgentConfiguration private constructor(
+ val adminUrl: String?,
+ val userUrl: String?,
+ val apiKey: String?
+) {
+ companion object {
+ private val host = System.getenv("ACAPY_HOSTNAME") ?: "localhost"
+ private val adminPort = System.getenv("ACAPY_ADMIN_PORT") ?: "8031"
+ private val userPort = System.getenv("ACAPY_USER_PORT") ?: "8030"
+ private val apiKey = System.getenv("ACAPY_ADMIN_API_KEY") ?: "adminkey"
+ val defaultConfiguration: AgentConfiguration = builder()
+ .adminUrl(String.format("http://%s:%s", host, adminPort))
+ .userUrl(String.format("http://%s:%s", host, userPort))
+ .apiKey(apiKey)
+ .build()
+
+ fun builder(): AgentConfigurationBuilder {
+ return AgentConfigurationBuilder()
+ }
+ }
+
+ override fun toString(): String {
+ val reductedApiKey = if (apiKey != null) apiKey.substring(0, 4) + "..." else null
+ return "AgentConfiguration [agentAdminUrl=$adminUrl, agentUserUrl=$userUrl, agentApiKey=$reductedApiKey]"
+ }
+
+ class AgentConfigurationBuilder {
+
+ private var adminUrl: String? = null
+ private var userUrl: String? = null
+ private var apiKey: String? = null
+
+ fun adminUrl(adminUrl: String): AgentConfigurationBuilder {
+ this.adminUrl = adminUrl
+ return this
+ }
+
+ fun userUrl(userUrl: String): AgentConfigurationBuilder {
+ this.userUrl = userUrl
+ return this
+ }
+
+ fun apiKey(apiKey: String): AgentConfigurationBuilder {
+ this.apiKey = apiKey
+ return this
+ }
+
+ fun build(): AgentConfiguration {
+ return AgentConfiguration(adminUrl, userUrl, apiKey)
+ }
+ }
+}
+
+object AriesClientFactory {
+
+ /**
+ * Create a client for the admin wallet
+ */
+ fun adminClient(config: AgentConfiguration? = null, level: Level? = null): AriesClient {
+ val auxConfig = config ?: AgentConfiguration.defaultConfiguration
+ return walletClient(auxConfig, level=level)
+ }
+
+ /**
+ * Create a client for a multitenant wallet
+ */
+ fun walletClient(config: AgentConfiguration? = null, wallet: NessusWallet? = null, level: Level? = null): AriesClient {
+ val auxConfig = config ?: AgentConfiguration.defaultConfiguration
+ val loggingInterceptor = if (level != null) createHttpLoggingInterceptor(level) else null
+ return walletClient(auxConfig, wallet, null, loggingInterceptor)
+ }
+
+ /**
+ * Create a client for a multitenant wallet
+ */
+ fun walletClient(
+ config: AgentConfiguration,
+ wallet: NessusWallet? = null,
+ httpClient: OkHttpClient? = null,
+ loggingInterceptor: HttpLoggingInterceptor? = null
+ ): AriesClient {
+ val auxHttpClient = httpClient ?: OkHttpClient.Builder()
+ .writeTimeout(60, TimeUnit.SECONDS)
+ .readTimeout(60, TimeUnit.SECONDS)
+ .connectTimeout(60, TimeUnit.SECONDS)
+ .callTimeout(60, TimeUnit.SECONDS)
+ .addInterceptor(loggingInterceptor ?: createHttpLoggingInterceptor(Level.TRACE))
+ .build()
+ return AriesClient.builder()
+ .url(config.adminUrl)
+ .apiKey(config.apiKey)
+ .bearerToken(wallet?.accessToken)
+ .client(auxHttpClient)
+ .build()
+ }
+
+ private fun createHttpLoggingInterceptor(level: Level): HttpLoggingInterceptor {
+ val log = KotlinLogging.logger {}
+ val gson = GsonConfig.defaultConfig()
+ val pretty = GsonConfig.prettyPrinter()
+ fun log(spec: String, msg: String) {
+ when(level) {
+ Level.ERROR -> log.error(spec, msg)
+ Level.WARN -> log.warn(spec, msg)
+ Level.INFO -> log.info(spec, msg)
+ Level.DEBUG -> log.debug(spec, msg)
+ else -> log.trace(spec, msg)
+ }
+ }
+ val interceptor = HttpLoggingInterceptor { msg: String ->
+ if (log.isEnabledForLevel(level) && msg.isNotEmpty()) {
+ if (msg.startsWith("{")) {
+ try {
+ val json: Any = gson.fromJson(msg, Any::class.java)
+ log("\n{}", pretty.toJson(json))
+ } catch (e: JsonSyntaxException) {
+ log("{}", msg)
+ }
+ } else {
+ log("{}", msg)
+ }
+ }
+ }
+ interceptor.level = HttpLoggingInterceptor.Level.BODY
+ interceptor.redactHeader("X-API-Key")
+ interceptor.redactHeader("Authorization")
+ return interceptor
+ }
+}
diff --git a/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/AriesWalletService.kt b/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/AriesWalletService.kt
new file mode 100644
index 00000000..4c548793
--- /dev/null
+++ b/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/AriesWalletService.kt
@@ -0,0 +1,110 @@
+package org.nessus.didcomm.agent.aries
+
+import mu.KotlinLogging
+import org.hyperledger.acy_py.generated.model.DID
+import org.hyperledger.aries.AriesClient
+import org.hyperledger.aries.api.ledger.IndyLedgerRoles
+import org.hyperledger.aries.api.ledger.RegisterNymFilter
+import org.hyperledger.aries.api.multitenancy.CreateWalletRequest
+import org.hyperledger.aries.api.multitenancy.RemoveWalletRequest
+import org.hyperledger.aries.api.wallet.WalletDIDCreate
+import org.nessus.didcomm.agent.aries.AriesAgentService.Companion.adminClient
+import org.nessus.didcomm.service.WalletService
+import org.nessus.didcomm.wallet.NessusWallet
+import org.nessus.didcomm.wallet.WalletException
+
+
+class AriesWalletService : WalletService {
+
+ private val log = KotlinLogging.logger {}
+
+ override fun createWallet(config: Map): NessusWallet {
+
+ val walletName = assertConfigValue(config,"walletName") as String
+ val auxWalletKey = getConfigValue(config,"walletKey")
+ val walletKey = if (auxWalletKey != null) auxWalletKey as String else walletName + "Key"
+ val selfRegister = getConfigValue(config, "selfRegister")
+ val trusteeWallet = getConfigValue(config, "trusteeWallet")
+ val ledgerRole = getConfigValue(config, "ledgerRole")
+ val indyLedgerRole = if (ledgerRole != null) IndyLedgerRoles.valueOf(ledgerRole.toString()) else null
+
+ val walletRequest = CreateWalletRequest.builder()
+ .walletName(walletName)
+ .walletKey(walletKey)
+ .build()
+ val walletRecord = adminClient().multitenancyWalletCreate(walletRequest).get()
+ val nessusWallet = NessusWallet(walletRecord.walletId, walletName, walletRecord.token)
+ WalletService.registry.putWallet(nessusWallet)
+ log.info("{}: [{}] {}", walletName, nessusWallet.walletId, nessusWallet)
+
+ if (indyLedgerRole != null) {
+ if (selfRegister == false && trusteeWallet == null)
+ throw WalletException("LedgerRole $indyLedgerRole requires selfRegister or trusteeWallet")
+
+ // Create a local DID for the wallet
+ val walletClient = AriesAgentService.walletClient(nessusWallet)
+ val did: DID = walletClient.walletDidCreate(WalletDIDCreate.builder().build()).get()
+ log.info("{}: {}", walletName, did)
+ if (trusteeWallet != null) {
+ val trustee: AriesClient = AriesAgentService.walletClient(trusteeWallet as NessusWallet)
+ val trusteeName: String = trusteeWallet.walletName
+ val nymResponse = trustee.ledgerRegisterNym(
+ RegisterNymFilter.builder()
+ .verkey(did.verkey)
+ .did(did.did)
+ .role(indyLedgerRole)
+ .build()
+ ).get()
+ log.info("{} for {}: {}", trusteeName, walletName, nymResponse)
+ } else if (selfRegister == true) {
+ // Register DID with the leder (out-of-band)
+ selfRegisterWithDid(walletName, did.did, did.verkey, indyLedgerRole)
+ }
+
+ // Set the public DID for the wallet
+ walletClient.walletDidPublic(did.did)
+ val didEndpoint = walletClient.walletGetDidEndpoint(did.did).get()
+ log.info("{}: {}", walletName, didEndpoint)
+ }
+
+ return nessusWallet
+ }
+
+ override fun publicDid(wallet: NessusWallet): String? {
+ val walletClient = AriesAgentService.walletClient(wallet)
+ return walletClient.walletDidPublic().orElse(null)?.toString()
+ }
+
+ override fun closeAndRemove(wallet: NessusWallet?) {
+
+ if (wallet != null) {
+
+ val walletId = wallet.walletId
+ val walletName = wallet.walletName
+ val accessToken = wallet.accessToken
+ log.info("Remove Wallet: {}", walletName)
+
+ WalletService.registry.removeWallet(walletId)
+
+ val adminClient: AriesClient = adminClient()
+ adminClient.multitenancyWalletRemove(
+ walletId, RemoveWalletRequest.builder()
+ .walletKey(accessToken)
+ .build()
+ )
+
+ // Wait for the wallet to get removed
+ Thread.sleep(500)
+ while (adminClient.multitenancyWallets(walletName).get().isNotEmpty()) {
+ Thread.sleep(500)
+ }
+ }
+ }
+
+ private fun selfRegisterWithDid(alias: String, did: String, vkey: String, role: IndyLedgerRoles): Boolean {
+ val host: String = System.getenv("INDY_WEBSERVER_HOSTNAME") ?: "localhost"
+ val port: String = System.getenv("INDY_WEBSERVER_PORT") ?: "9000"
+ return SelfRegistrationHandler(String.format("http://%s:%s/register", host, port))
+ .registerWithDID(alias, did, vkey, role)
+ }
+}
diff --git a/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/HttpClientFactory.kt b/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/HttpClientFactory.kt
new file mode 100644
index 00000000..2e4a03a3
--- /dev/null
+++ b/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/HttpClientFactory.kt
@@ -0,0 +1,42 @@
+package org.nessus.didcomm.agent.aries
+
+import mu.KotlinLogging
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import org.apache.commons.lang3.StringUtils
+import org.hyperledger.aries.config.GsonConfig
+import java.util.concurrent.TimeUnit
+
+object HttpClientFactory {
+
+ private val log = KotlinLogging.logger {}
+
+ fun createHttpClient(): OkHttpClient {
+ return OkHttpClient.Builder()
+ .writeTimeout(60, TimeUnit.SECONDS)
+ .readTimeout(60, TimeUnit.SECONDS)
+ .connectTimeout(60, TimeUnit.SECONDS)
+ .callTimeout(60, TimeUnit.SECONDS)
+ .addInterceptor(defaultLoggingInterceptor())
+ .build()
+ }
+
+ fun defaultLoggingInterceptor(): HttpLoggingInterceptor {
+ val gson = GsonConfig.defaultConfig()
+ val pretty = GsonConfig.prettyPrinter()
+ val interceptor = HttpLoggingInterceptor { msg: String ->
+ if (log.isTraceEnabled && StringUtils.isNotEmpty(msg)) {
+ if (msg.startsWith("{")) {
+ val json = gson.fromJson(msg, Any::class.java)
+ log.trace("\n{}", pretty.toJson(json))
+ } else {
+ log.trace("{}", msg)
+ }
+ }
+ }
+ interceptor.level = HttpLoggingInterceptor.Level.BODY
+ interceptor.redactHeader("Authorization")
+ interceptor.redactHeader("X-API-Key")
+ return interceptor
+ }
+}
diff --git a/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/SelfRegistrationHandler.kt b/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/SelfRegistrationHandler.kt
new file mode 100644
index 00000000..556837ff
--- /dev/null
+++ b/services/agent/src/main/kotlin/org/nessus/didcomm/agent/aries/SelfRegistrationHandler.kt
@@ -0,0 +1,86 @@
+package org.nessus.didcomm.agent.aries
+
+import com.google.gson.JsonObject
+import mu.KotlinLogging
+import okhttp3.MediaType
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.Response
+import org.apache.commons.lang3.StringUtils
+import org.hyperledger.acy_py.generated.model.DID
+import org.hyperledger.aries.api.exception.AriesException
+import org.hyperledger.aries.api.ledger.IndyLedgerRoles
+import org.hyperledger.aries.config.GsonConfig
+import java.util.concurrent.TimeUnit
+
+class SelfRegistrationHandler(private val networkURL: String) {
+
+ private val log = KotlinLogging.logger {}
+ private val gson = GsonConfig.defaultConfig()
+
+ companion object {
+ private val httpClient = OkHttpClient.Builder()
+ .writeTimeout(60, TimeUnit.SECONDS)
+ .readTimeout(60, TimeUnit.SECONDS)
+ .connectTimeout(60, TimeUnit.SECONDS)
+ .callTimeout(60, TimeUnit.SECONDS)
+ .build()
+ }
+
+ fun registerWithDID(alias: String?, did: String?, verkey: String?, role: IndyLedgerRoles?): Boolean {
+ var json = JsonObject()
+ json.addProperty("did", did)
+ json.addProperty("verkey", verkey)
+ if (alias != null) json.addProperty("alias", alias)
+ if (role != null) json.addProperty("role", role.toString())
+ log.info("Self register: {}", json)
+ val res = call(buildPost(json))
+ json = gson.fromJson(res, JsonObject::class.java)
+ log.info("Respose: {}", json)
+ return true
+ }
+
+ fun registerWithSeed(alias: String?, seed: String?, role: IndyLedgerRoles?): DID {
+ val json = JsonObject()
+ json.addProperty("seed", seed)
+ if (alias != null) json.addProperty("alias", alias)
+ if (role != null) json.addProperty("role", role.toString())
+ log.info("Self register: {}", json)
+ val res = call(buildPost(json))
+ val did = gson.fromJson(res, DID::class.java)
+ log.info("Respose: {}", did)
+ return did
+ }
+
+ private fun buildPost(body: Any): Request {
+ val jsonType: MediaType = "application/json; charset=utf-8".toMediaType()
+ val jsonBody: RequestBody = gson.toJson(body).toRequestBody(jsonType)
+ return Request.Builder().url(networkURL).post(jsonBody).build()
+ }
+
+ private fun call(req: Request): String? {
+ var result: String? = null
+ httpClient.newCall(req).execute().use { resp ->
+ if (resp.isSuccessful && resp.body != null) {
+ result = resp.body!!.string()
+ } else if (!resp.isSuccessful) {
+ handleError(resp)
+ }
+ }
+ return result
+ }
+
+ private fun handleError(resp: Response) {
+ val msg = if (StringUtils.isNotEmpty(resp.message)) resp.message else ""
+ val body = if (resp.body != null) resp.body!!.string() else ""
+ log.error("code={} message={}\nbody={}", resp.code, msg, body)
+ throw AriesException(resp.code, """
+ $msg
+ $body
+ """.trimIndent()
+ )
+ }
+}