diff --git a/waltid-crypto/src/commonTest/kotlin/TSEKeyTest.kt b/waltid-crypto/src/commonTest/kotlin/TSEKeyTest.kt index f65b97a27..fef0b40fe 100644 --- a/waltid-crypto/src/commonTest/kotlin/TSEKeyTest.kt +++ b/waltid-crypto/src/commonTest/kotlin/TSEKeyTest.kt @@ -37,7 +37,7 @@ class TSEKeyTest { @ParameterizedTest @MethodSource fun getPublicKey(key: TSEKey?) = runTest { - assumeTrue(hostCondition()) + assumeTrue(isVaultAvailable()) assumeTrue(key != null) val publicKey = key!!.getPublicKey() assertTrue(!publicKey.hasPrivateKey) @@ -55,7 +55,7 @@ class TSEKeyTest { @ParameterizedTest @MethodSource fun signRaw(key: TSEKey?) = runTest { - assumeTrue(hostCondition()) + assumeTrue(isVaultAvailable()) assumeTrue(key != null) val signed = key!!.signRaw(payload.toString().encodeToByteArray()) as String val verificationResult = key.verifyRaw(signed.decodeBase64Bytes(), payload.toString().encodeToByteArray()) @@ -66,7 +66,7 @@ class TSEKeyTest { @ParameterizedTest @MethodSource fun signJws(key: TSEKey?) = runTest { - assumeTrue(hostCondition()) + assumeTrue(isVaultAvailable()) val signed = key!!.signJws(payload.toString().encodeToByteArray()) val verificationResult = key.verifyJws(signed) assertTrue(verificationResult.isSuccess) @@ -99,7 +99,7 @@ class TSEKeyTest { @JvmStatic @BeforeAll fun initKeys() = runTest { - hostCondition().takeIf { it }?.let { + isVaultAvailable().takeIf { it }?.let { val tseMetadata = TSEKeyMetadata("http://127.0.0.1:8200/v1/transit", "dev-only-token") keys = enumValues().map { TSEKey.generate(KeyType.Ed25519, tseMetadata) } } @@ -116,7 +116,7 @@ class TSEKeyTest { @JvmStatic fun signJws(): Stream = keys.map { arguments(it) }.asSequence().asStream() - private fun hostCondition() = runCatching { + private fun isVaultAvailable() = runCatching { runBlocking { HttpClient().get("http://127.0.0.1:8200") }.status == HttpStatusCode.OK }.fold(onSuccess = { it }, onFailure = { false }) } diff --git a/waltid-crypto/src/jvmTest/kotlin/TestJvm.kt b/waltid-crypto/src/jvmTest/kotlin/TestJvm.kt index e7f862489..c1fc5cda9 100644 --- a/waltid-crypto/src/jvmTest/kotlin/TestJvm.kt +++ b/waltid-crypto/src/jvmTest/kotlin/TestJvm.kt @@ -49,7 +49,7 @@ class TestJvm { } @Test - @EnabledIf("hostCondition") + @EnabledIf("isVaultAvailable") fun testKeySerialization() = runTest { val localKey = LocalKey.generate(KeyType.Ed25519) val localKeySerialized = KeySerialization.serializeKey(localKey) @@ -189,7 +189,7 @@ class TestJvm { } } - private fun hostCondition() = runCatching { + private fun isVaultAvailable() = runCatching { runBlocking { HttpClient().get("http://127.0.0.1:8200") }.status == HttpStatusCode.OK }.fold(onSuccess = { true }, onFailure = { false }) diff --git a/waltid-did/src/commonMain/kotlin/id/walt/did/dids/DidService.kt b/waltid-did/src/commonMain/kotlin/id/walt/did/dids/DidService.kt index 3ab2836b2..d11e776be 100644 --- a/waltid-did/src/commonMain/kotlin/id/walt/did/dids/DidService.kt +++ b/waltid-did/src/commonMain/kotlin/id/walt/did/dids/DidService.kt @@ -105,8 +105,9 @@ object DidService { getRegistrarForMethod(options.method).create(options) } - suspend fun registerByKey(method: String, key: Key, options: DidCreateOptions = DidCreateOptions(method, emptyMap())): DidResult = - getRegistrarForMethod(method).createByKey(key, options) + suspend fun registerByKey( + method: String, key: Key, options: DidCreateOptions = DidCreateOptions(method, emptyMap()) + ): DidResult = getRegistrarForMethod(method).createByKey(key, options) fun update() { TODO("Not yet implemented") diff --git a/waltid-did/src/commonMain/kotlin/id/walt/did/dids/document/DidCheqdDocument.kt b/waltid-did/src/commonMain/kotlin/id/walt/did/dids/document/DidCheqdDocument.kt index 9fa0e7679..055e3b3d6 100644 --- a/waltid-did/src/commonMain/kotlin/id/walt/did/dids/document/DidCheqdDocument.kt +++ b/waltid-did/src/commonMain/kotlin/id/walt/did/dids/document/DidCheqdDocument.kt @@ -35,11 +35,14 @@ data class DidCheqdDocument( fun toMap() = Json.encodeToJsonElement(this).jsonObject.toMap() - constructor(didDoc: DidDocument, jwk: JsonObject) : this( + constructor(didDoc: DidDocument, jwk: JsonObject? = null) : this( context = DEFAULT_CONTEXT, id = didDoc.id, - verificationMethod = didDoc.verificationMethod.map { - VerificationMethod(it.id, "JsonWebKey2020", it.controller, jwk) + //TODO: publicKeyMultibase + verificationMethod = jwk?.let { key -> + didDoc.verificationMethod.map { + VerificationMethod(it.id, "JsonWebKey2020", it.controller, key) + } }, authentication = didDoc.authentication, assertionMethod = didDoc.authentication, diff --git a/waltid-did/src/commonMain/kotlin/id/walt/did/dids/registrar/local/cheqd/DidCheqdRegistrar.kt b/waltid-did/src/commonMain/kotlin/id/walt/did/dids/registrar/local/cheqd/DidCheqdRegistrar.kt index 318ec188d..51f9ab1c8 100644 --- a/waltid-did/src/commonMain/kotlin/id/walt/did/dids/registrar/local/cheqd/DidCheqdRegistrar.kt +++ b/waltid-did/src/commonMain/kotlin/id/walt/did/dids/registrar/local/cheqd/DidCheqdRegistrar.kt @@ -34,7 +34,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.encodeToJsonElement -class DidCheqdRegistrar() : LocalRegistrarMethod("cheqd") { +class DidCheqdRegistrar : LocalRegistrarMethod("cheqd") { private val log = KotlinLogging.logger { } @@ -151,4 +151,4 @@ class DidCheqdRegistrar() : LocalRegistrarMethod("cheqd") { java.util.Base64.getUrlEncoder().encodeToString(key.signRaw(it) as ByteArray) } } -} +} \ No newline at end of file diff --git a/waltid-did/src/commonMain/kotlin/id/walt/did/dids/registrar/local/jwk/DidJwkRegistrar.kt b/waltid-did/src/commonMain/kotlin/id/walt/did/dids/registrar/local/jwk/DidJwkRegistrar.kt index 946947e8c..9af9ac489 100644 --- a/waltid-did/src/commonMain/kotlin/id/walt/did/dids/registrar/local/jwk/DidJwkRegistrar.kt +++ b/waltid-did/src/commonMain/kotlin/id/walt/did/dids/registrar/local/jwk/DidJwkRegistrar.kt @@ -17,7 +17,7 @@ class DidJwkRegistrar : LocalRegistrarMethod("jwk") { } ?: throw IllegalArgumentException("KeyType option not found.") override suspend fun registerByKey(key: Key, options: DidCreateOptions): DidResult { - val did = "did:jwk:${key.exportJWK().toByteArray().encodeToBase64Url()}" + val did = "did:jwk:${key.getPublicKey().exportJWK().toByteArray().encodeToBase64Url()}" val didDocument = DidDocument( DidJwkDocument(did, key.getPublicKey().exportJWKObject()) diff --git a/waltid-did/src/commonMain/kotlin/id/walt/did/dids/resolver/local/DidCheqdResolver.kt b/waltid-did/src/commonMain/kotlin/id/walt/did/dids/resolver/local/DidCheqdResolver.kt new file mode 100644 index 000000000..250cca94b --- /dev/null +++ b/waltid-did/src/commonMain/kotlin/id/walt/did/dids/resolver/local/DidCheqdResolver.kt @@ -0,0 +1,53 @@ +package id.walt.did.dids.resolver.local + +import id.walt.crypto.keys.Key +import id.walt.did.dids.document.DidCheqdDocument +import id.walt.did.dids.document.DidDocument +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +class DidCheqdResolver : LocalResolverMethod("cheqd") { + private val httpClient = HttpClient() //TODO: inject + + override suspend fun resolve(did: String): Result = runCatching { + resolveDid(did) + } + + override suspend fun resolveToKey(did: String): Result { + TODO("Not yet implemented") + // response verificationMethod contains only publicKeyMultibase + // required: convert it to publicKeyJwk + // (no functionality provided by crypto, only multibase58btc available) + } + + private val json = Json() { ignoreUnknownKeys = true } + + private fun resolveDid(did: String): DidDocument { + val response = runBlocking { + httpClient.get("https://resolver.cheqd.net/1.0/identifiers/${did}") { + headers { + append("contentType", "application/did+ld+json") + } + }.bodyAsText() + } + val resolution = Json.decodeFromString(response) + + val didDocument = resolution.jsonObject["didResolutionMetadata"]?.jsonObject?.get("error")?.let { + throw IllegalArgumentException("Could not resolve did:cheqd, resolver responded: ${it.jsonPrimitive.content}") + } ?: let { + resolution.jsonObject["didDocument"]?.jsonObject + ?: throw IllegalArgumentException("Response for did:cheqd did not contain a DID document!") + }.let { + json.decodeFromString( + it.toString() + ) + } + return DidDocument(DidCheqdDocument(didDocument).toMap()) + } +} \ No newline at end of file diff --git a/waltid-did/src/commonMain/kotlin/id/walt/did/dids/resolver/local/DidWebResolver.kt b/waltid-did/src/commonMain/kotlin/id/walt/did/dids/resolver/local/DidWebResolver.kt index 3494fbb7d..818e76d45 100644 --- a/waltid-did/src/commonMain/kotlin/id/walt/did/dids/resolver/local/DidWebResolver.kt +++ b/waltid-did/src/commonMain/kotlin/id/walt/did/dids/resolver/local/DidWebResolver.kt @@ -15,25 +15,6 @@ import kotlinx.serialization.json.jsonObject class DidWebResolver(private val client: HttpClient) : LocalResolverMethod("web") { - companion object { - const val URL_PROTOCOL = "https" - val json = Json { ignoreUnknownKeys = true } - } - - private fun resolveDidToUrl(did: String): String = DidUtils.identifierFromDid(did)?.let { - val didParts = it.split(":") - - val domain = didParts[0].replace("%3A", ":") - val selectedPath = didParts.drop(1) - - val path = when { - selectedPath.isEmpty() -> "/.well-known/did.json" - else -> "/${selectedPath.joinToString("/")}/did.json" - } - - "$URL_PROTOCOL://$domain$path" - } ?: throw IllegalArgumentException("Unexpected did format (missing identifier): $did") - override suspend fun resolve(did: String): Result { val url = resolveDidToUrl(did) @@ -46,14 +27,6 @@ class DidWebResolver(private val client: HttpClient) : LocalResolverMethod("web" return response } - suspend fun tryConvertAnyPublicKeyJwkToKey(publicKeyJwks: List): Result { - publicKeyJwks.forEach { publicKeyJwk -> - val result = LocalKey.importJWK(publicKeyJwk) - if (result.isSuccess) return result - } - return Result.failure(NoSuchElementException("No key could be imported")) - } - override suspend fun resolveToKey(did: String): Result { val didDocumentResult = resolve(did) if (didDocumentResult.isFailure) return Result.failure(didDocumentResult.exceptionOrNull()!!) @@ -70,4 +43,31 @@ class DidWebResolver(private val client: HttpClient) : LocalResolverMethod("web" return tryConvertAnyPublicKeyJwkToKey(publicKeyJwks) } + + private fun resolveDidToUrl(did: String): String = DidUtils.identifierFromDid(did)?.let { + val didParts = it.split(":") + + val domain = didParts[0].replace("%3A", ":") + val selectedPath = didParts.drop(1) + + val path = when { + selectedPath.isEmpty() -> "/.well-known/did.json" + else -> "/${selectedPath.joinToString("/")}/did.json" + } + + "$URL_PROTOCOL://$domain$path" + } ?: throw IllegalArgumentException("Unexpected did format (missing identifier): $did") + + suspend fun tryConvertAnyPublicKeyJwkToKey(publicKeyJwks: List): Result { + publicKeyJwks.forEach { publicKeyJwk -> + val result = LocalKey.importJWK(publicKeyJwk) + if (result.isSuccess) return result + } + return Result.failure(NoSuchElementException("No key could be imported")) + } + + companion object { + const val URL_PROTOCOL = "https" + val json = Json { ignoreUnknownKeys = true } + } } diff --git a/waltid-did/src/commonTest/kotlin/id/walt/did/DidTest.kt b/waltid-did/src/commonTest/kotlin/id/walt/did/DidExamples.kt similarity index 63% rename from waltid-did/src/commonTest/kotlin/id/walt/did/DidTest.kt rename to waltid-did/src/commonTest/kotlin/id/walt/did/DidExamples.kt index 0cae7b41d..ad375697c 100644 --- a/waltid-did/src/commonTest/kotlin/id/walt/did/DidTest.kt +++ b/waltid-did/src/commonTest/kotlin/id/walt/did/DidExamples.kt @@ -7,14 +7,18 @@ import id.walt.crypto.keys.TSEKeyMetadata import id.walt.did.dids.DidService import id.walt.did.dids.registrar.dids.DidKeyCreateOptions import id.walt.did.helpers.WaltidServices +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.http.* +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.time.Duration.Companion.minutes -class DidTest { +class DidExamples { - val remoteKey = false + private val tseMetadata = TSEKeyMetadata("http://127.0.0.1:8200/v1/transit", "dev-only-token") @BeforeTest fun init() { @@ -28,9 +32,6 @@ class DidTest { @Test fun listDidMethods() { -// println("Registrar: " + DidService.registrarMethods.toList().groupBy { it.second.name }) -// println("Resolver: " + DidService.resolverMethods.toList().groupBy { it.second.name }) - println("Resolver:") println( groupDidList(DidService.resolverMethods.mapValues { it.value.name }) @@ -42,13 +43,12 @@ class DidTest { ) } - - val tseMetadata = TSEKeyMetadata("http://127.0.0.1:8200/v1/transit", "dev-only-token") - @Test - fun createDidJwk() = runTest { + fun exampleCreateDidJwk() = runTest { - val key = if (remoteKey) TSEKey.generate(KeyType.Ed25519, tseMetadata) else LocalKey.generate(KeyType.Ed25519) + val key = if (isVaultAvailable()) TSEKey.generate( + KeyType.Ed25519, tseMetadata + ) else LocalKey.generate(KeyType.Ed25519) val did = DidService.registerByKey("jwk", key) @@ -56,13 +56,19 @@ class DidTest { } @Test - fun createDidKeyJcs() = runTest { + fun exampleCreateDidKeyJcs() = runTest { - val key = if (remoteKey) TSEKey.generate(KeyType.Ed25519, tseMetadata) else LocalKey.generate(KeyType.Ed25519) + val key = if (isVaultAvailable()) TSEKey.generate( + KeyType.Ed25519, tseMetadata + ) else LocalKey.generate(KeyType.Ed25519) val options = DidKeyCreateOptions(KeyType.Ed25519, useJwkJcsPub = true) val did = DidService.registerByKey("key", key, options) println(did.didDocument.toJsonObject()) } + + private fun isVaultAvailable() = runCatching { + runBlocking { HttpClient().get("http://127.0.0.1:8200") }.status == HttpStatusCode.OK + }.fold(onSuccess = { it }, onFailure = { false }) } diff --git a/waltid-did/src/jvmTest/kotlin/registrars/DidKeyRegistrarTest.kt b/waltid-did/src/jvmTest/kotlin/registrars/DidKeyRegistrarTest.kt index 6c3ff1c68..59a7d9e8c 100644 --- a/waltid-did/src/jvmTest/kotlin/registrars/DidKeyRegistrarTest.kt +++ b/waltid-did/src/jvmTest/kotlin/registrars/DidKeyRegistrarTest.kt @@ -39,10 +39,10 @@ class DidKeyRegistrarTest : DidRegistrarTestBase(DidKeyRegistrar()) { @JvmStatic fun `given did options with no key when register then returns a valid did result`(): Stream = Stream.of( -// arguments(DidKeyCreateOptions(useJwkJcsPub = true), ed25519DidAssertions), -// arguments(DidKeyCreateOptions(KeyType.Ed25519), ed25519DidAssertions), -// arguments(DidKeyCreateOptions(KeyType.RSA), rsaDidAssertions), -// arguments(DidKeyCreateOptions(KeyType.secp256k1), secp256DidAssertions), + arguments(DidKeyCreateOptions(useJwkJcsPub = true), ed25519DidAssertions), + arguments(DidKeyCreateOptions(KeyType.Ed25519), ed25519DidAssertions), + arguments(DidKeyCreateOptions(KeyType.RSA), rsaDidAssertions), + arguments(DidKeyCreateOptions(KeyType.secp256k1), secp256DidAssertions), arguments(DidKeyCreateOptions(KeyType.secp256r1), secp256DidAssertions), ) diff --git a/waltid-did/src/jvmTest/kotlin/resolvers/DidCheqdResolverTest.kt b/waltid-did/src/jvmTest/kotlin/resolvers/DidCheqdResolverTest.kt new file mode 100644 index 000000000..52f4d0759 --- /dev/null +++ b/waltid-did/src/jvmTest/kotlin/resolvers/DidCheqdResolverTest.kt @@ -0,0 +1,61 @@ +package resolvers + +import id.walt.crypto.keys.Key +import id.walt.did.dids.document.DidDocument +import id.walt.did.dids.resolver.local.DidCheqdResolver +import id.walt.did.dids.resolver.local.LocalResolverMethod +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class DidCheqdResolverTest : DidResolverTestBase() { + override val sut: LocalResolverMethod = DidCheqdResolver() + + @ParameterizedTest + @MethodSource + override fun `given a did String, when calling resolve, then the result is a valid did document`( + did: String, + key: JsonObject, + assert: resolverAssertion + ) { + super.`given a did String, when calling resolve, then the result is a valid did document`(did, key, assert) + } + + @ParameterizedTest + @MethodSource + @Disabled // not implemented + override fun `given a did String, when calling resolveToKey, then the result is valid key`( + did: String, + key: JsonObject, + assert: resolverAssertion + ) { + super.`given a did String, when calling resolveToKey, then the result is valid key`(did, key, assert) + } + + companion object { + @JvmStatic + fun `given a did String, when calling resolve, then the result is a valid did document`(): Stream = + Stream.of( + arguments( + "did:cheqd:testnet:W5a3426DZ1f4qBkYC9ZT6s", + Json.decodeFromString("{\"kty\":\"OKP\",\"use\":\"sig\",\"crv\":\"Ed25519\",\"kid\":\"151df6ec01714883b812f26f2d63e584\",\"x\":\"qBDsYw3k62mUT8UmEx99Xz3yckiSRmTsL6aa21ZcAVM\",\"alg\":\"EdDSA\"}"), + ed25519DidAssertions + ) + ) + + @JvmStatic + fun `given a did String, when calling resolveToKey, then the result is valid key`(): Stream = + Stream.of( + arguments( + "did:cheqd:testnet:38088d21-a5f8-4277-bd35-b36918d81c14", + Json.decodeFromString("{\"kty\":\"OKP\",\"d\":\"24WxHxiKpnd1_BitZBU57ex8EKaNukiyC4punO4Lh-s\",\"crv\":\"Ed25519\",\"kid\":\"GaQD9pzL5wJyATB1UA2J71ygXgkykT1QOnL7uIBgcpo\",\"x\":\"bz2K3xX-D_R2_Pu7al6UCRXGSl1pzBRfEoD3bj94s_w\"}"), + ed25519KeyAssertions + ) + ) + } +} \ No newline at end of file diff --git a/waltid-did/src/jvmTest/kotlin/resolvers/DidResolverTestBase.kt b/waltid-did/src/jvmTest/kotlin/resolvers/DidResolverTestBase.kt index a77104c7d..acb857d7c 100644 --- a/waltid-did/src/jvmTest/kotlin/resolvers/DidResolverTestBase.kt +++ b/waltid-did/src/jvmTest/kotlin/resolvers/DidResolverTestBase.kt @@ -8,10 +8,7 @@ import id.walt.crypto.keys.Key import id.walt.did.dids.document.DidDocument import id.walt.did.dids.resolver.local.LocalResolverMethod import kotlinx.coroutines.runBlocking -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.* import kotlin.test.assertNotNull abstract class DidResolverTestBase { @@ -44,9 +41,9 @@ abstract class DidResolverTestBase { assertNotNull(doc) // assert [id] and [did] are identical assert(doc["id"]!!.jsonPrimitive.content == did) - assert(doc["verificationMethod"]!!.jsonArray.any { - defaultKeyChecks(it.jsonObject["publicKeyJwk"]!!.jsonObject, key) - }) + verificationMethodAssertions(doc, key) { v, k -> + defaultKeyChecks(v, k) + } } /** @@ -56,9 +53,9 @@ abstract class DidResolverTestBase { val ed25519DidAssertions: resolverAssertion = { did, key, result -> didDocAssertions(did, key, result) val doc = result.getOrNull()!! - assert(doc["verificationMethod"]!!.jsonArray.any { - ed25519KeyChecks(it.jsonObject["publicKeyJwk"]!!.jsonObject, key) - }) + verificationMethodAssertions(doc, key) { v, k -> + ed25519KeyChecks(v, k) + } } /** @@ -68,9 +65,9 @@ abstract class DidResolverTestBase { val secp256DidAssertions: resolverAssertion = { did, key, result -> ed25519DidAssertions(did, key, result) val doc = result.getOrNull()!! - assert(doc["verificationMethod"]!!.jsonArray.any { - secp256KeyChecks(it.jsonObject["publicKeyJwk"]!!.jsonObject, key) - }) + verificationMethodAssertions(doc, key) { v, k -> + secp256KeyChecks(v, k) + } } /** @@ -80,9 +77,9 @@ abstract class DidResolverTestBase { val rsaDidAssertions: resolverAssertion = { did, key, result -> didDocAssertions(did, key, result) val doc = result.getOrNull()!! - assert(doc["verificationMethod"]!!.jsonArray.any { - rsaKeyChecks(it.jsonObject["publicKeyJwk"]!!.jsonObject, key) - }) + verificationMethodAssertions(doc, key) { v, k -> + rsaKeyChecks(v, k) + } } //endregion -DidDocument assertions- @@ -125,6 +122,17 @@ abstract class DidResolverTestBase { assert(rsaKeyChecks(publicKey, key)) } //endregion -Key assertions- + + private val verificationMethodAssertions: ( + doc: DidDocument, key: JsonObject, runChecks: (actual: JsonObject, expected: JsonObject) -> Boolean + ) -> Unit = { doc, key, runChecks -> + // verification method is optional + doc["verificationMethod"]?.takeIf { it != JsonNull }?.run { + assert(this.jsonArray.any { + runChecks(it.jsonObject["publicKeyJwk"]!!.jsonObject, key) + }) + } + } } }