diff --git a/src/main/kotlin/id/walt/cli/did/DidCommand.kt b/src/main/kotlin/id/walt/cli/did/DidCommand.kt index 103fad515..463aeef90 100644 --- a/src/main/kotlin/id/walt/cli/did/DidCommand.kt +++ b/src/main/kotlin/id/walt/cli/did/DidCommand.kt @@ -68,6 +68,7 @@ class CreateDidCommand : CliktCommand( is WebMethodOption -> DidService.create(web, keyId, DidWebCreateOptions((method as WebMethodOption).domain, (method as WebMethodOption).path)) is EbsiMethodOption -> DidService.create(ebsi, keyId, DidEbsiCreateOptions((method as EbsiMethodOption).version)) is CheqdMethodOption -> DidService.create(cheqd, keyId, DidCheqdCreateOptions((method as CheqdMethodOption).network)) + is KeyMethodOption -> DidService.create(key, keyId, DidKeyCreateOptions((method as KeyMethodOption).isJwk)) else -> DidService.create(DidMethod.valueOf(method.method), keyId) } diff --git a/src/main/kotlin/id/walt/cli/did/DidMethodOptions.kt b/src/main/kotlin/id/walt/cli/did/DidMethodOptions.kt index 728554a66..3653566f4 100644 --- a/src/main/kotlin/id/walt/cli/did/DidMethodOptions.kt +++ b/src/main/kotlin/id/walt/cli/did/DidMethodOptions.kt @@ -2,12 +2,12 @@ package id.walt.cli.did import com.github.ajalt.clikt.parameters.groups.OptionGroup import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.choice import com.github.ajalt.clikt.parameters.types.int sealed class DidMethodOption(val method: String) : OptionGroup() -class KeyMethodOption : DidMethodOption("key") class WebMethodOption : DidMethodOption("web") { val domain: String by option("-d", "--domain", help = "did:web - Domain for did:web").default("walt.id") val path: String? by option("-p", "--path", help = "did:web - Path for did:web") @@ -22,3 +22,6 @@ class JwkMethodOption : DidMethodOption("jwk") class CheqdMethodOption : DidMethodOption("cheqd") { val network: String by option("-n", "--network", help = "did:cheqd - Specify the network [testnet]").choice("mainnet", "testnet").default("testnet") } +class KeyMethodOption : DidMethodOption("key") { + val isJwk: Boolean by option("-j", "--is-jwk", help = "did:key - is jwk_jcs-pub").flag(default = false) +} diff --git a/src/main/kotlin/id/walt/common/CommonUtils.kt b/src/main/kotlin/id/walt/common/CommonUtils.kt index f6b361988..8dc096b4a 100644 --- a/src/main/kotlin/id/walt/common/CommonUtils.kt +++ b/src/main/kotlin/id/walt/common/CommonUtils.kt @@ -1,5 +1,11 @@ package id.walt.common +import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.jwk.OctetKeyPair +import com.nimbusds.jose.jwk.RSAKey +import id.walt.crypto.KeyAlgorithm +import id.walt.model.Jwk import id.walt.services.WaltIdServices.httpNoAuth import io.ktor.client.request.* import io.ktor.client.statement.* @@ -106,3 +112,45 @@ inline fun T.asMap() : Map { val props = T::class.memberProperties.associateBy { it.name } return props.keys.associateWith { props[it]?.get(this) } } + +/** + * Converts the JWK public key to a required-only members JSON string + * @param - the JWK key + * @return - the JSON string representing the public key having only the required members + */ +fun convertToRequiredMembersJsonString(jwk: JWK): Jwk = when (getKeyAlgorithm(jwk)) { + KeyAlgorithm.ECDSA_Secp256k1, KeyAlgorithm.ECDSA_Secp256r1 -> EcPublicKeyRequiredMembers(jwk.toECKey()) + KeyAlgorithm.EdDSA_Ed25519 -> OkpPublicKeyRequiredMembers(jwk.toOctetKeyPair()) + KeyAlgorithm.RSA -> RsaPublicKeyRequiredMembers(jwk.toPublicJWK().toRSAKey()) +} + +fun getKeyAlgorithm(jwk: JWK): KeyAlgorithm = when (jwk.keyType.value.lowercase()) { + "rsa" -> KeyAlgorithm.RSA + "ec" -> when (jwk.toECKey().curve.stdName.lowercase()) { + "secp256k1" -> KeyAlgorithm.ECDSA_Secp256k1 + "secp256r1" -> KeyAlgorithm.ECDSA_Secp256r1 + else -> throw IllegalArgumentException("Curve ${jwk.toECKey().curve.stdName} for EC algorithm not supported.") + } + + "okp" -> KeyAlgorithm.EdDSA_Ed25519 + else -> throw IllegalArgumentException("Key algorithm ${jwk.keyType.value} not supported.") +} + +private fun OkpPublicKeyRequiredMembers(okp: OctetKeyPair) = Jwk( + crv = okp.curve.name, + kty = okp.keyType.value, + x = okp.x.toString() +) + +private fun EcPublicKeyRequiredMembers(ec: ECKey) = Jwk( + crv = ec.curve.name, + kty = ec.keyType.value, + x = ec.x.toString(), + y = ec.y.toString() +) + +private fun RsaPublicKeyRequiredMembers(rsa: RSAKey) = Jwk( + e = rsa.publicExponent.toString(), + kty = rsa.algorithm.name, + n = rsa.modulus.toString() +) diff --git a/src/main/kotlin/id/walt/crypto/CryptFun.kt b/src/main/kotlin/id/walt/crypto/CryptFun.kt index 037902022..91a070682 100644 --- a/src/main/kotlin/id/walt/crypto/CryptFun.kt +++ b/src/main/kotlin/id/walt/crypto/CryptFun.kt @@ -45,11 +45,11 @@ enum class KeyAlgorithm { ECDSA_Secp256r1; companion object { - fun fromString(algorithm: String): KeyAlgorithm = when (algorithm) { - "EdDSA", "Ed25519", "EdDSA_Ed25519" -> EdDSA_Ed25519 - "ECDSA", "Secp256k1", "ECDSA_Secp256k1" -> ECDSA_Secp256k1 - "RSA" -> RSA - "Secp256r1", "ECDSA_Secp256r1" -> ECDSA_Secp256r1 + fun fromString(algorithm: String): KeyAlgorithm = when (algorithm.lowercase()) { + "eddsa", "ed25519", "eddsa_ed25519" -> EdDSA_Ed25519 + "ecdsa", "secp256k1", "ecdsa_secp256k1" -> ECDSA_Secp256k1 + "rsa" -> RSA + "secp256r1", "ecdsa_secp256r1" -> ECDSA_Secp256r1 else -> throw IllegalArgumentException("Algorithm not supported") } } @@ -251,6 +251,9 @@ fun convertX25519PublicKeyFromMultibase58Btc(mbase58: String): ByteArray { // 0x1205 rsa-pub // 0xed ed25519-pub // 0xe7 secp256k1-pub +// 0xeb51 jwk_jcs-pub + +const val JwkJcsPubMultiCodecKeyCode = 0xeb51u @Suppress("REDUNDANT_ELSE_IN_WHEN") fun getMulticodecKeyCode(algorithm: KeyAlgorithm) = when (algorithm) { @@ -261,16 +264,14 @@ fun getMulticodecKeyCode(algorithm: KeyAlgorithm) = when (algorithm) { else -> throw IllegalArgumentException("No multicodec for algorithm $algorithm") } -fun getKeyAlgorithmFromMultibase(mb: String): KeyAlgorithm { - val decoded = mb.decodeMultiBase58Btc() - val code = UVarInt.fromBytes(decoded) - return when (code.value) { - 0xEDu -> KeyAlgorithm.EdDSA_Ed25519 - 0xE7u -> KeyAlgorithm.ECDSA_Secp256k1 - 0x1205u -> KeyAlgorithm.RSA - 0x1200u -> KeyAlgorithm.ECDSA_Secp256r1 - else -> throw IllegalArgumentException("No multicodec algorithm for code $code") - } +fun getMultiCodecKeyCode(mb: String): UInt = UVarInt.fromBytes(mb.decodeMultiBase58Btc()).value + +fun getKeyAlgorithmFromKeyCode(keyCode: UInt): KeyAlgorithm = when (keyCode) { + 0xEDu -> KeyAlgorithm.EdDSA_Ed25519 + 0xE7u -> KeyAlgorithm.ECDSA_Secp256k1 + 0x1205u -> KeyAlgorithm.RSA + 0x1200u -> KeyAlgorithm.ECDSA_Secp256r1 + else -> throw IllegalArgumentException("No multicodec algorithm for code $keyCode") } fun convertRawKeyToMultiBase58Btc(key: ByteArray, code: UInt): String { diff --git a/src/main/kotlin/id/walt/rest/core/DidController.kt b/src/main/kotlin/id/walt/rest/core/DidController.kt index d73bbf820..98c6b588d 100644 --- a/src/main/kotlin/id/walt/rest/core/DidController.kt +++ b/src/main/kotlin/id/walt/rest/core/DidController.kt @@ -4,10 +4,7 @@ import id.walt.common.KlaxonWithConverters import id.walt.common.prettyPrint import id.walt.crypto.KeyAlgorithm import id.walt.model.DidMethod -import id.walt.rest.core.requests.did.CheqdCreateDidRequest -import id.walt.rest.core.requests.did.CreateDidRequest -import id.walt.rest.core.requests.did.EbsiCreateDidRequest -import id.walt.rest.core.requests.did.WebCreateDidRequest +import id.walt.rest.core.requests.did.* import id.walt.services.did.* import io.javalin.http.Context import io.javalin.http.HttpCode @@ -82,6 +79,7 @@ object DidController { is WebCreateDidRequest -> DidWebCreateOptions(request.domain ?: "walt.id", request.path) is EbsiCreateDidRequest -> DidEbsiCreateOptions(request.version) is CheqdCreateDidRequest -> DidCheqdCreateOptions(request.network) + is KeyCreateDidRequest -> DidKeyCreateOptions(request.isJwk) else -> null } diff --git a/src/main/kotlin/id/walt/rest/core/requests/did/CreateDidRequest.kt b/src/main/kotlin/id/walt/rest/core/requests/did/CreateDidRequest.kt index 4389cb870..62c6e2f0e 100644 --- a/src/main/kotlin/id/walt/rest/core/requests/did/CreateDidRequest.kt +++ b/src/main/kotlin/id/walt/rest/core/requests/did/CreateDidRequest.kt @@ -27,7 +27,10 @@ class CreateDidRequestMethodAdapter : TypeAdapter { @Serializable -class KeyCreateDidRequest(override val keyAlias: String? = null) : CreateDidRequest("key") +class KeyCreateDidRequest( + override val keyAlias: String? = null, + val isJwk: Boolean = false, +) : CreateDidRequest("key") @Serializable class WebCreateDidRequest( val domain: String? = null, diff --git a/src/main/kotlin/id/walt/services/did/DidOptions.kt b/src/main/kotlin/id/walt/services/did/DidOptions.kt index e250dc1e1..2fbfaee08 100644 --- a/src/main/kotlin/id/walt/services/did/DidOptions.kt +++ b/src/main/kotlin/id/walt/services/did/DidOptions.kt @@ -5,5 +5,6 @@ sealed class DidOptions data class DidWebCreateOptions(val domain: String?, val path: String? = null) : DidOptions() data class DidEbsiCreateOptions(val version: Int) : DidOptions() data class DidCheqdCreateOptions(val network: String) : DidOptions() +data class DidKeyCreateOptions(val isJwk: Boolean) : DidOptions() data class DidEbsiResolveOptions(val isRaw: Boolean) : DidOptions() diff --git a/src/main/kotlin/id/walt/services/did/DidService.kt b/src/main/kotlin/id/walt/services/did/DidService.kt index 8b224f011..15720c672 100644 --- a/src/main/kotlin/id/walt/services/did/DidService.kt +++ b/src/main/kotlin/id/walt/services/did/DidService.kt @@ -3,24 +3,23 @@ package id.walt.services.did import com.beust.klaxon.Klaxon import com.github.benmanes.caffeine.cache.Caffeine import com.nimbusds.jose.jwk.JWK -import com.nimbusds.jose.util.Base64URL import id.walt.crypto.* import id.walt.crypto.KeyAlgorithm.* import id.walt.crypto.LdVerificationKeyType.* import id.walt.model.* import id.walt.model.did.DidEbsi -import id.walt.model.did.DidWeb import id.walt.services.CryptoProvider import id.walt.services.WaltIdServices import id.walt.services.context.ContextManager import id.walt.services.crypto.CryptoService +import id.walt.services.did.composers.DidEbsiV2DocumentComposer +import id.walt.services.did.composers.DidJwkDocumentComposer +import id.walt.services.did.composers.DidKeyDocumentComposer +import id.walt.services.did.factories.DidFactoryBase import id.walt.services.did.resolvers.DidResolverFactory -import id.walt.services.ecosystems.cheqd.CheqdService -import id.walt.services.ecosystems.iota.IotaService import id.walt.services.ecosystems.iota.IotaWrapper import id.walt.services.hkvstore.HKVKey import id.walt.services.key.KeyService -import id.walt.services.keystore.KeyType import id.walt.services.vc.JsonLdCredentialService import id.walt.signatory.ProofConfig import io.ipfs.multibase.Multibase @@ -28,9 +27,7 @@ import mu.KotlinLogging import org.bouncycastle.asn1.edec.EdECObjectIdentifiers import org.bouncycastle.asn1.x509.AlgorithmIdentifier import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo -import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey import java.io.File -import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.security.KeyFactory import java.security.KeyPair @@ -54,7 +51,10 @@ object DidService { private val didResolverFactory = DidResolverFactory( httpNoAuth = WaltIdServices.httpNoAuth, keyService = keyService, - iotaWrapper = IotaWrapper.createInstance() + iotaWrapper = IotaWrapper.createInstance(), + didKeyDocumentComposer = DidKeyDocumentComposer(keyService), + didJwkDocumentComposer = DidJwkDocumentComposer(), + ebsiV2DocumentComposer = DidEbsiV2DocumentComposer(), ) private val didCache = Caffeine.newBuilder() .maximumSize(1000) @@ -78,180 +78,13 @@ object DidService { "did.json" // region did-create - fun create(method: DidMethod, keyAlias: String? = null, options: DidOptions? = null): String { - @Suppress("REDUNDANT_ELSE_IN_WHEN") - val didUrl = when (method) { - DidMethod.key -> createDidKey(keyAlias) - DidMethod.web -> createDidWeb(keyAlias, - options?.let { it as DidWebCreateOptions } ?: DidWebCreateOptions("walt.id", UUID.randomUUID().toString())) - - DidMethod.ebsi -> createDidEbsi(keyAlias, options as? DidEbsiCreateOptions) - DidMethod.iota -> createDidIota(keyAlias) - DidMethod.jwk -> createDidJwk(keyAlias) - DidMethod.cheqd -> createDidCheqd(keyAlias, options as? DidCheqdCreateOptions) - else -> throw UnsupportedOperationException("DID method $method not supported") - } - - return didUrl - } - - private fun createDidCheqd(keyAlias: String?, options: DidCheqdCreateOptions?): String { - val keyId = keyAlias?.let { KeyId(it) } ?: cryptoService.generateKey(EdDSA_Ed25519) - val did = CheqdService.createDid(keyId.id, options?.network ?: "testnet") - storeDid(did) - ContextManager.keyStore.addAlias(keyId, did.id) - ContextManager.keyStore.addAlias(keyId, did.verificationMethod!![0].id) - return did.id - } - - private fun createDidKey(keyAlias: String?): String { - val keyId = keyAlias?.let { KeyId(it) } ?: cryptoService.generateKey(DEFAULT_KEY_ALGORITHM) - val key = ContextManager.keyStore.load(keyId.id) - - if (key.algorithm !in setOf(EdDSA_Ed25519, RSA, ECDSA_Secp256k1, ECDSA_Secp256r1)) - throw IllegalArgumentException("did:key can not be created with an ${key.algorithm} key.") - - val identifier = convertRawKeyToMultiBase58Btc(getPublicKeyBytesForDidKey(key), getMulticodecKeyCode(key.algorithm)) - - val didUrl = "did:key:$identifier" - - runCatching { - ContextManager.keyStore.load(keyId.id) - }.onSuccess { - log.debug { "A key with the id \"${keyId.id}\" exists." } - //throw IllegalArgumentException("A key with the id \"${keyId.id}\" already exists.") - } - ContextManager.keyStore.addAlias(keyId, didUrl) - - val didDoc = resolve(didUrl) - didDoc.verificationMethod?.forEach { (id) -> - ContextManager.keyStore.addAlias(keyId, id) - } - storeDid(didDoc) - - return didUrl - } - - private fun createDidJwk(keyAlias: String?): String { - val keyId = keyAlias?.let { KeyId(it) } ?: cryptoService.generateKey(DEFAULT_KEY_ALGORITHM) - - val identifier = Base64URL.encode(keyService.toJwk(keyId.id).toString()) - - val didUrl = "did:jwk:$identifier" - - runCatching { - ContextManager.keyStore.load(keyId.id) - }.onSuccess { - log.debug { "A key with the id \"${keyId.id}\" exists." } - //throw IllegalArgumentException("A key with the id \"${keyId.id}\" already exists.") - } - ContextManager.keyStore.addAlias(keyId, didUrl) - - val didDoc = resolve(didUrl) - didDoc.verificationMethod?.forEach { (id) -> - ContextManager.keyStore.addAlias(keyId, id) - } - storeDid(didDoc) - - return didUrl - } - - private fun createDidWeb(keyAlias: String?, options: DidWebCreateOptions?): String { - - options ?: throw Exception("DidWebOptions are mandatory") - if (options.domain.isNullOrEmpty()) - throw IllegalArgumentException("Missing 'domain' parameter for creating did:web") - - val key = keyAlias?.let { ContextManager.keyStore.load(it) } ?: cryptoService.generateKey(DEFAULT_KEY_ALGORITHM) - .let { ContextManager.keyStore.load(it.id) } - - val domain = URLEncoder.encode(options.domain, StandardCharsets.UTF_8) - - val path = when { - options.path.isNullOrEmpty() -> "" - else -> ":${ - options.path.split("/").joinToString(":") { part -> URLEncoder.encode(part, StandardCharsets.UTF_8) } - }" - } - - val didUrlStr = DidUrl("web", "$domain$path").did - - ContextManager.keyStore.addAlias(key.keyId, didUrlStr) - - // Created DID doc - val kid = didUrlStr + "#" + key.keyId - ContextManager.keyStore.addAlias(key.keyId, kid) - - val verificationMethods = buildVerificationMethods(key, kid, didUrlStr) - - - val keyRef = listOf(VerificationMethod.Reference(kid)) - - val didDoc = DidWeb(DID_CONTEXT_URL, didUrlStr, verificationMethods, keyRef, keyRef) - - storeDid(didDoc) - - return didUrlStr - } - - private fun createDidIota(keyAlias: String?): String { - // did:iota requires key of type EdDSA_Ed25519 - val keyId = keyAlias?.let { KeyId(it) } ?: cryptoService.generateKey(EdDSA_Ed25519) - val didIota = IotaService.createDid(keyId.id) - storeDid(didIota) - ContextManager.keyStore.addAlias(keyId, didIota.id) - ContextManager.keyStore.addAlias(keyId, didIota.verificationMethod!![0].id) - return didIota.id - } - - private fun createDidEbsi(keyAlias: String?, didEbsiOptions: DidEbsiCreateOptions?): String { - val version = didEbsiOptions?.version ?: 1 - return when (version) { - 1 -> createDidEbsiV1(keyAlias) - 2 -> createDidEbsiV2(keyAlias) - else -> throw Exception("Did ebsi version must be 1 or 2") - } - } - - private fun createDidEbsiV1(keyAlias: String?): String { - val keyId = keyAlias?.let { KeyId(it) } ?: cryptoService.generateKey(DEFAULT_KEY_ALGORITHM) - val key = ContextManager.keyStore.load(keyId.id) - - // Created identifier - val didUrlStr = DidUrl.generateDidEbsiV1DidUrl().did - - ContextManager.keyStore.addAlias(keyId, didUrlStr) - - // Created DID doc - val kid = didUrlStr + "#" + key.keyId - ContextManager.keyStore.addAlias(keyId, kid) - - val verificationMethods = buildVerificationMethods(key, kid, didUrlStr) - - val didEbsi = DidEbsi( - listOf(DID_CONTEXT_URL), // TODO Context not working "https://ebsi.org/ns/did/v1" - didUrlStr, - verificationMethods, - listOf(VerificationMethod.Reference(kid)), listOf(VerificationMethod.Reference(kid)) - ) - - storeDid(didEbsi) - - return didUrlStr - } - - private fun createDidEbsiV2(keyAlias: String?): String { - val keyId = keyAlias?.let { KeyId(it) } ?: cryptoService.generateKey(DEFAULT_KEY_ALGORITHM) - val publicKeyJwk = keyService.toJwk(keyId.id, KeyType.PUBLIC) - val publicKeyThumbprint = publicKeyJwk.computeThumbprint() - val didUrlStr = DidUrl.generateDidEbsiV2DidUrl(publicKeyThumbprint.decode()).did - val vmId = "$didUrlStr#$publicKeyThumbprint" - ContextManager.keyStore.addAlias(keyId, didUrlStr) - ContextManager.keyStore.addAlias(keyId, vmId) - val didDoc = resolve(didUrlStr) - storeDid(didDoc) - return didUrlStr - } + fun create(method: DidMethod, keyAlias: String? = null, options: DidOptions? = null): String = + ensureKey(method, keyAlias).let { + Pair(it.keyId, DidFactoryBase.new(method, keyService).create(it, options)) + }.also { + addKeyAlias(it.first, it.second) + storeDid(it.second) + }.second.id //endregion //region did-load @@ -341,41 +174,6 @@ object DidService { } } - private fun getPublicKeyBytesForDidKey(key: Key): ByteArray { - return when (key.algorithm) { - ECDSA_Secp256k1, ECDSA_Secp256r1 -> (key.getPublicKey() as BCECPublicKey).q.getEncoded(true) - RSA, EdDSA_Ed25519 -> key.getPublicKeyBytes() - } - } - - private fun buildVerificationMethods( - key: Key, - kid: String, - didUrlStr: String - ): MutableList { - val keyType = when (key.algorithm) { - EdDSA_Ed25519 -> Ed25519VerificationKey2019 - ECDSA_Secp256k1 -> EcdsaSecp256k1VerificationKey2019 - ECDSA_Secp256r1 -> EcdsaSecp256r1VerificationKey2019 - RSA -> RsaVerificationKey2018 - } - log.debug { "Verification method JWK for kid: ${keyService.toJwk(kid)}" } - log.debug { "Verification method public JWK: ${keyService.toJwk(kid).toPublicJWK()}" } - log.debug { - "Verification method parsed public JWK: ${ - Klaxon().parse( - keyService.toJwk(kid).toPublicJWK().toString() - ) - }" - } - val publicKeyJwk = Klaxon().parse(keyService.toJwk(kid).toPublicJWK().toString()) - - val verificationMethods = mutableListOf( - VerificationMethod(kid, keyType.name, didUrlStr, null, null, publicKeyJwk), - ) - return verificationMethods - } - fun setKeyIdForDid(did: String, keyId: String) { val key = ContextManager.keyStore.load(keyId) log.debug { "Loaded key: $keyId" } @@ -548,6 +346,26 @@ object DidService { } } + private fun ensureKey(didMethod: DidMethod, keyAlias: String? = null): Key = when (didMethod) { + DidMethod.iota -> EdDSA_Ed25519 +// DidMethod.key -> ECDSA_Secp256r1 + else -> DEFAULT_KEY_ALGORITHM + }.let { + keyAlias?.let { KeyId(it) } ?: cryptoService.generateKey(it) + }.let { + keyService.load(it.id) + } + + private fun addKeyAlias(keyId: KeyId, did: Did) { + runCatching { ContextManager.keyStore.load(keyId.id) }.onSuccess { _ -> + log.debug { "A key with the id \"${keyId.id}\" exists." } + } + ContextManager.keyStore.addAlias(keyId, did.id) + did.verificationMethod?.forEach { (id) -> + ContextManager.keyStore.addAlias(keyId, id) + } + } + // TODO: consider the methods below. They might be deprecated! // fun resolveDid(did: String): Did = resolveDid(did.DidUrl.toDidUrl()) @@ -612,5 +430,3 @@ object DidService { // } } - - diff --git a/src/main/kotlin/id/walt/services/did/builders/DidBuilder.kt b/src/main/kotlin/id/walt/services/did/builders/DidBuilder.kt deleted file mode 100644 index 469475ae4..000000000 --- a/src/main/kotlin/id/walt/services/did/builders/DidBuilder.kt +++ /dev/null @@ -1,7 +0,0 @@ -package id.walt.services.did.builders - -import id.walt.services.did.DidOptions - -interface DidBuilder { - fun create(keyAlias: String? = null, options: DidOptions? = null) -} diff --git a/src/main/kotlin/id/walt/services/did/composers/DidDocumentComposer.kt b/src/main/kotlin/id/walt/services/did/composers/DidDocumentComposer.kt new file mode 100644 index 000000000..871c235c8 --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/composers/DidDocumentComposer.kt @@ -0,0 +1,8 @@ +package id.walt.services.did.composers + +import id.walt.model.Did +import id.walt.services.did.composers.models.DocumentComposerBaseParameter + +interface DidDocumentComposer { + fun make(parameter: DocumentComposerBaseParameter): T +} diff --git a/src/main/kotlin/id/walt/services/did/composers/DidDocumentComposerBase.kt b/src/main/kotlin/id/walt/services/did/composers/DidDocumentComposerBase.kt new file mode 100644 index 000000000..bd5b26510 --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/composers/DidDocumentComposerBase.kt @@ -0,0 +1,34 @@ +package id.walt.services.did.composers + +import com.beust.klaxon.Klaxon +import com.nimbusds.jose.jwk.JWK +import id.walt.crypto.Key +import id.walt.crypto.KeyAlgorithm +import id.walt.crypto.LdVerificationKeyType +import id.walt.model.Did +import id.walt.model.Jwk +import id.walt.model.VerificationMethod +import mu.KotlinLogging + +abstract class DidDocumentComposerBase : DidDocumentComposer { + private val log = KotlinLogging.logger {} + + protected fun buildVerificationMethods( + key: Key, kid: String, didUrlStr: String, jwk: JWK + ): MutableList { + val keyType = when (key.algorithm) { + KeyAlgorithm.EdDSA_Ed25519 -> LdVerificationKeyType.Ed25519VerificationKey2019 + KeyAlgorithm.ECDSA_Secp256k1 -> LdVerificationKeyType.EcdsaSecp256k1VerificationKey2019 + KeyAlgorithm.ECDSA_Secp256r1 -> LdVerificationKeyType.EcdsaSecp256r1VerificationKey2019 + KeyAlgorithm.RSA -> LdVerificationKeyType.RsaVerificationKey2018 + } + log.debug { "Verification method JWK for kid: $jwk" } + log.debug { "Verification method public JWK: ${jwk.toPublicJWK()}" } + log.debug { "Verification method parsed public JWK: ${Klaxon().parse(jwk.toPublicJWK().toString())}" } + val publicKeyJwk = Klaxon().parse(jwk.toPublicJWK().toString()) + + return mutableListOf( + VerificationMethod(kid, keyType.name, didUrlStr, null, null, publicKeyJwk), + ) + } +} diff --git a/src/main/kotlin/id/walt/services/did/composers/DidEbsiV1DocumentComposer.kt b/src/main/kotlin/id/walt/services/did/composers/DidEbsiV1DocumentComposer.kt new file mode 100644 index 000000000..0c063e3e1 --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/composers/DidEbsiV1DocumentComposer.kt @@ -0,0 +1,20 @@ +package id.walt.services.did.composers + +import id.walt.model.DID_CONTEXT_URL +import id.walt.model.VerificationMethod +import id.walt.model.did.DidEbsi +import id.walt.services.did.composers.models.DocumentComposerBaseParameter +import id.walt.services.did.composers.models.DocumentComposerKeyJwkParameter + +class DidEbsiV1DocumentComposer : DidDocumentComposerBase() { + override fun make(parameter: DocumentComposerBaseParameter): DidEbsi = + (parameter as? DocumentComposerKeyJwkParameter)?.let { + val kid = it.didUrl.did + "#" + it.key.keyId + val verificationMethods = buildVerificationMethods(it.key, kid, it.didUrl.did, it.jwk) + val keyRef = listOf(VerificationMethod.Reference(kid)) + DidEbsi( + listOf(DID_CONTEXT_URL), // TODO Context not working "https://ebsi.org/ns/did/v1" + it.didUrl.did, verificationMethods, keyRef, keyRef + ) + } ?: throw IllegalArgumentException("Couldn't parse ebsi-v1 document composer parameter") +} diff --git a/src/main/kotlin/id/walt/services/did/composers/DidEbsiV2DocumentComposer.kt b/src/main/kotlin/id/walt/services/did/composers/DidEbsiV2DocumentComposer.kt new file mode 100644 index 000000000..bf73e9b2d --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/composers/DidEbsiV2DocumentComposer.kt @@ -0,0 +1,32 @@ +package id.walt.services.did.composers + +import com.beust.klaxon.Klaxon +import id.walt.model.DidUrl +import id.walt.model.VerificationMethod +import id.walt.model.did.DidEbsi +import id.walt.services.did.composers.models.DocumentComposerBaseParameter +import id.walt.services.did.composers.models.DocumentComposerJwkParameter + +class DidEbsiV2DocumentComposer : DidDocumentComposerBase() { + override fun make(parameter: DocumentComposerBaseParameter): DidEbsi = + (parameter as? DocumentComposerJwkParameter)?.let { + val vmId = "${it.didUrl.did}#${it.jwk.computeThumbprint()}" + if (DidUrl.generateDidEbsiV2DidUrl(it.jwk.computeThumbprint().decode()).identifier != it.didUrl.identifier) { + throw IllegalArgumentException("Public key doesn't match with DID identifier") + } + DidEbsi( + context = listOf("https://w3id.org/did/v1"), + id = it.didUrl.did, + verificationMethod = listOf( + VerificationMethod( + id = vmId, + type = "JsonWebKey2020", + controller = it.didUrl.did, + publicKeyJwk = Klaxon().parse(it.jwk.toJSONString()) + ) + ), + authentication = listOf(VerificationMethod.Reference(vmId)), + assertionMethod = listOf(VerificationMethod.Reference(vmId)) + ) + } ?: throw IllegalArgumentException("Couldn't parse ebsi-v2 document composer parameter") +} diff --git a/src/main/kotlin/id/walt/services/did/composers/DidJwkDocumentComposer.kt b/src/main/kotlin/id/walt/services/did/composers/DidJwkDocumentComposer.kt new file mode 100644 index 000000000..4c3d05684 --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/composers/DidJwkDocumentComposer.kt @@ -0,0 +1,28 @@ +package id.walt.services.did.composers + +import com.beust.klaxon.Klaxon +import com.nimbusds.jose.util.Base64URL +import id.walt.model.Did +import id.walt.model.Jwk +import id.walt.model.VerificationMethod +import id.walt.services.did.composers.models.DocumentComposerBaseParameter + +class DidJwkDocumentComposer : DidDocumentComposerBase() { + override fun make(parameter: DocumentComposerBaseParameter): Did = Did( + context = listOf("https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"), + id = parameter.didUrl.did, + verificationMethod = listOf( + VerificationMethod( + id = "${parameter.didUrl.did}#0", + type = "JsonWebKey2020", + controller = parameter.didUrl.did, + publicKeyJwk = Klaxon().parse(Base64URL.from(parameter.didUrl.identifier).decodeToString()) + ) + ), + assertionMethod = listOf(VerificationMethod.Reference("${parameter.didUrl.did}#0")), + authentication = listOf(VerificationMethod.Reference("${parameter.didUrl.did}#0")), + capabilityInvocation = listOf(VerificationMethod.Reference("${parameter.didUrl.did}#0")), + capabilityDelegation = listOf(VerificationMethod.Reference("${parameter.didUrl.did}#0")), + keyAgreement = listOf(VerificationMethod.Reference("${parameter.didUrl.did}#0")) + ) +} diff --git a/src/main/kotlin/id/walt/services/did/composers/DidKeyDocumentComposer.kt b/src/main/kotlin/id/walt/services/did/composers/DidKeyDocumentComposer.kt new file mode 100644 index 000000000..9998189b3 --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/composers/DidKeyDocumentComposer.kt @@ -0,0 +1,158 @@ +package id.walt.services.did.composers + +import com.beust.klaxon.Klaxon +import com.nimbusds.jose.jwk.Curve +import com.nimbusds.jose.jwk.JWK +import id.walt.crypto.* +import id.walt.model.* +import id.walt.model.did.DidKey +import id.walt.services.CryptoProvider +import id.walt.services.did.composers.models.DocumentComposerBaseParameter +import id.walt.services.key.KeyService +import java.security.KeyPair + +class DidKeyDocumentComposer( + private val keyService: KeyService, +) : DidDocumentComposerBase() { + + override fun make(parameter: DocumentComposerBaseParameter): DidKey = parameter.didUrl.let { + val pubKey = convertMultiBase58BtcToRawKey(it.identifier) + val keyCode = getMultiCodecKeyCode(it.identifier) + constructDidKey(it, pubKey, keyCode) + } + + /** + * Top level did:key composer + * @param [didUrl] - the did-url to build the did document for + * @param [pubKey] - the public key byte array + * @param [keyCode] - the multi-codec key code + * @return the [Did] document + */ + private fun constructDidKey(didUrl: DidUrl, pubKey: ByteArray, keyCode: UInt): DidKey = + keyCode.takeIf { it == JwkJcsPubMultiCodecKeyCode }?.let { + constructDidKey(didUrl, pubKey) + } ?: constructDidKey(didUrl, pubKey, getKeyAlgorithmFromKeyCode(keyCode)) + + /** + * jwk_jcs-pub did:key + * @param [didUrl] - the did-url to build the did document for + * @param [pubKey] - the public key byte array + * @return the [Did] document + */ + private fun constructDidKey(didUrl: DidUrl, pubKey: ByteArray): DidKey = DidKey( + context = listOf( + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ), + id = didUrl.did, + verificationMethod = listOf( + VerificationMethod( + id = "${didUrl.did}#${didUrl.did}", + type = "JsonWebKey2020", + controller = didUrl.did, + publicKeyJwk = Klaxon().parse(JWK.parse(String(pubKey)).toJSONString()) + ) + ), + assertionMethod = listOf(VerificationMethod.Reference(didUrl.did)), + authentication = listOf(VerificationMethod.Reference(didUrl.did)), + capabilityInvocation = listOf(VerificationMethod.Reference(didUrl.did)), + capabilityDelegation = listOf(VerificationMethod.Reference(didUrl.did)), + ) + + /** + * other types of did:key + * @param [didUrl] - the did-url to build the did document for + * @param [pubKey] - the public key byte array + * @param [keyAlgorithm] - the key algorithm + * @return the [Did] document + */ + private fun constructDidKey(didUrl: DidUrl, pubKey: ByteArray, keyAlgorithm: KeyAlgorithm): DidKey { + + val (keyAgreementKeys, verificationMethods, keyRef) = when (keyAlgorithm) { + KeyAlgorithm.EdDSA_Ed25519 -> generateEdParams(pubKey, didUrl) + KeyAlgorithm.ECDSA_Secp256r1, KeyAlgorithm.ECDSA_Secp256k1 -> generateEcKeyParams( + pubKey, didUrl, keyAlgorithm + ) + + KeyAlgorithm.RSA -> generateRSAKeyParams(pubKey, didUrl) + } + + return DidKey( + context = DID_CONTEXT_URL, + id = didUrl.did, + verificationMethod = verificationMethods, + authentication = keyRef, + assertionMethod = keyRef, + capabilityDelegation = keyRef, + capabilityInvocation = keyRef, + keyAgreement = keyAgreementKeys, + ) + } + + private fun generateEdParams( + pubKey: ByteArray, didUrl: DidUrl + ): Triple?, MutableList, List> { + val dhKey = convertPublicKeyEd25519ToCurve25519(pubKey) + + val dhKeyMb = convertX25519PublicKeyToMultiBase58Btc(dhKey) + + val pubKeyId = didUrl.did + "#" + didUrl.identifier + val dhKeyId = didUrl.did + "#" + dhKeyMb + + val verificationMethods = mutableListOf( + VerificationMethod( + pubKeyId, LdVerificationKeyType.Ed25519VerificationKey2019.name, didUrl.did, pubKey.encodeBase58() + ), VerificationMethod(dhKeyId, "X25519KeyAgreementKey2019", didUrl.did, dhKey.encodeBase58()) + ) + + return Triple( + listOf(VerificationMethod.Reference(dhKeyId)), verificationMethods, listOf( + VerificationMethod.Reference( + pubKeyId + ) + ) + ) + } + + private fun generateEcKeyParams( + pubKey: ByteArray, didUrl: DidUrl, algorithm: KeyAlgorithm + ): Triple?, MutableList, List> { + val curve = if (algorithm == KeyAlgorithm.ECDSA_Secp256k1) Curve.SECP256K1 else Curve.P_256 + val vmType = + if (algorithm == KeyAlgorithm.ECDSA_Secp256k1) LdVerificationKeyType.EcdsaSecp256k1VerificationKey2019.name else LdVerificationKeyType.EcdsaSecp256r1VerificationKey2019.name + + val uncompressedPubKey = + uncompressSecp256k1(pubKey, curve) ?: throw IllegalArgumentException("Error uncompressing public key bytes") + val pubKeyId = didUrl.did + "#" + didUrl.identifier + val key = Key(newKeyId(), algorithm, CryptoProvider.SUN, KeyPair(uncompressedPubKey.toECPublicKey(), null)) + + val verificationMethods = mutableListOf( + VerificationMethod( + id = pubKeyId, + type = vmType, + controller = didUrl.did, + publicKeyJwk = Klaxon().parse(keyService.toSecp256Jwk(key, curve, key.keyId.id).toJSONString()) + ) + ) + + return Triple( + null, verificationMethods, listOf(VerificationMethod.Reference(pubKeyId)) + ) + } + + private fun generateRSAKeyParams( + pubKey: ByteArray, didUrl: DidUrl + ): Triple?, MutableList, List> { + + val pubKeyId = didUrl.did + "#" + didUrl.identifier + + val verificationMethods = mutableListOf( + VerificationMethod( + pubKeyId, LdVerificationKeyType.RsaVerificationKey2018.name, didUrl.did, pubKey.encodeBase58() + ), + ) + + val keyRef = listOf(VerificationMethod.Reference(pubKeyId)) + return Triple(null, verificationMethods, keyRef) + } +} diff --git a/src/main/kotlin/id/walt/services/did/composers/DidWebDocumentComposer.kt b/src/main/kotlin/id/walt/services/did/composers/DidWebDocumentComposer.kt new file mode 100644 index 000000000..f507889d2 --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/composers/DidWebDocumentComposer.kt @@ -0,0 +1,16 @@ +package id.walt.services.did.composers + +import id.walt.model.DID_CONTEXT_URL +import id.walt.model.VerificationMethod +import id.walt.model.did.DidWeb +import id.walt.services.did.composers.models.DocumentComposerBaseParameter +import id.walt.services.did.composers.models.DocumentComposerKeyJwkParameter + +class DidWebDocumentComposer : DidDocumentComposerBase() { + override fun make(parameter: DocumentComposerBaseParameter): DidWeb = (parameter as? DocumentComposerKeyJwkParameter)?.let { + val kid = it.didUrl.did + "#" + it.key.keyId + val verificationMethods = buildVerificationMethods(it.key, kid, it.didUrl.did, it.jwk) + val keyRef = listOf(VerificationMethod.Reference(kid)) + DidWeb(DID_CONTEXT_URL, it.didUrl.did, verificationMethods, keyRef, keyRef) + } ?: throw IllegalArgumentException("Couldn't parse web document composer parameter") +} diff --git a/src/main/kotlin/id/walt/services/did/composers/models/DocumentComposerParameter.kt b/src/main/kotlin/id/walt/services/did/composers/models/DocumentComposerParameter.kt new file mode 100644 index 000000000..f96600e4f --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/composers/models/DocumentComposerParameter.kt @@ -0,0 +1,20 @@ +package id.walt.services.did.composers.models + +import com.nimbusds.jose.jwk.JWK +import id.walt.crypto.Key +import id.walt.model.DidUrl + +open class DocumentComposerBaseParameter( + open val didUrl: DidUrl +) + +open class DocumentComposerJwkParameter( + override val didUrl: DidUrl, + open val jwk: JWK, +) : DocumentComposerBaseParameter(didUrl) + +open class DocumentComposerKeyJwkParameter( + override val didUrl: DidUrl, + override val jwk: JWK, + open val key: Key +) : DocumentComposerJwkParameter(didUrl, jwk) diff --git a/src/main/kotlin/id/walt/services/did/factories/DidCheqdFactory.kt b/src/main/kotlin/id/walt/services/did/factories/DidCheqdFactory.kt new file mode 100644 index 000000000..d8358700e --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/factories/DidCheqdFactory.kt @@ -0,0 +1,12 @@ +package id.walt.services.did.factories + +import id.walt.crypto.Key +import id.walt.model.Did +import id.walt.services.did.DidCheqdCreateOptions +import id.walt.services.did.DidOptions +import id.walt.services.ecosystems.cheqd.CheqdService + +class DidCheqdFactory : DidFactory { + override fun create(key: Key, options: DidOptions?): Did = + CheqdService.createDid(key.keyId.id, (options as? DidCheqdCreateOptions)?.network ?: "testnet") +} diff --git a/src/main/kotlin/id/walt/services/did/factories/DidEbsiFactory.kt b/src/main/kotlin/id/walt/services/did/factories/DidEbsiFactory.kt new file mode 100644 index 000000000..c8250741f --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/factories/DidEbsiFactory.kt @@ -0,0 +1,43 @@ +package id.walt.services.did.factories + +import id.walt.crypto.Key +import id.walt.model.Did +import id.walt.model.DidUrl +import id.walt.model.did.DidEbsi +import id.walt.services.did.DidEbsiCreateOptions +import id.walt.services.did.DidOptions +import id.walt.services.did.composers.DidDocumentComposer +import id.walt.services.did.composers.models.DocumentComposerJwkParameter +import id.walt.services.did.composers.models.DocumentComposerKeyJwkParameter +import id.walt.services.key.KeyService +import id.walt.services.keystore.KeyType + +class DidEbsiFactory( + private val keyService: KeyService, + private val documentV1Composer: DidDocumentComposer, + private val documentV2Composer: DidDocumentComposer, +) : DidFactory { + override fun create(key: Key, options: DidOptions?): Did { + return when ((options as? DidEbsiCreateOptions)?.version ?: 1) { + 1 -> createDidEbsiV1(key) + 2 -> createDidEbsiV2(key) + else -> throw Exception("Did ebsi version must be 1 or 2") + } + } + + private fun createDidEbsiV1(key: Key) = documentV1Composer.make( + DocumentComposerKeyJwkParameter( + DidUrl.generateDidEbsiV1DidUrl(), keyService.toJwk(key.keyId.id), key + ) + ) + + private fun createDidEbsiV2(key: Key) = let { + val publicKeyJwk = keyService.toJwk(key.keyId.id, KeyType.PUBLIC) + val publicKeyThumbprint = publicKeyJwk.computeThumbprint() + documentV2Composer.make( + DocumentComposerJwkParameter( + DidUrl.generateDidEbsiV2DidUrl(publicKeyThumbprint.decode()), publicKeyJwk + ) + ) + } +} diff --git a/src/main/kotlin/id/walt/services/did/factories/DidFactory.kt b/src/main/kotlin/id/walt/services/did/factories/DidFactory.kt new file mode 100644 index 000000000..e04258202 --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/factories/DidFactory.kt @@ -0,0 +1,9 @@ +package id.walt.services.did.factories + +import id.walt.crypto.Key +import id.walt.model.Did +import id.walt.services.did.DidOptions + +interface DidFactory { + fun create(key: Key, options: DidOptions? = null): Did +} diff --git a/src/main/kotlin/id/walt/services/did/factories/DidFactoryBase.kt b/src/main/kotlin/id/walt/services/did/factories/DidFactoryBase.kt new file mode 100644 index 000000000..d185b3b4d --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/factories/DidFactoryBase.kt @@ -0,0 +1,27 @@ +package id.walt.services.did.factories + +import id.walt.model.DidMethod +import id.walt.services.did.composers.* +import id.walt.services.key.KeyService + +abstract class DidFactoryBase : DidFactory { + companion object { +// private val didKeyDocumentComposer = DidKeyDocumentComposer() + private val didJwkDocumentComposer = DidJwkDocumentComposer() + private val didWebDocumentComposer = DidWebDocumentComposer() + private val didEbsiV1DocumentComposer = DidEbsiV1DocumentComposer() + private val didEbsiV2DocumentComposer = DidEbsiV2DocumentComposer() + + @Suppress("REDUNDANT_ELSE_IN_WHEN") + fun new(method: DidMethod, keyService: KeyService): DidFactory = when (method) { + DidMethod.iota -> DidIotaFactory() + DidMethod.cheqd -> DidCheqdFactory() + // TODO: remove key-service dependency and cache composer similar others + DidMethod.key -> DidKeyFactory(keyService, DidKeyDocumentComposer(keyService)) + DidMethod.web -> DidWebFactory(keyService, didWebDocumentComposer) + DidMethod.ebsi -> DidEbsiFactory(keyService, didEbsiV1DocumentComposer, didEbsiV2DocumentComposer) + DidMethod.jwk -> DidJwkFactory(keyService, didJwkDocumentComposer) + else -> throw UnsupportedOperationException("DID method $method not supported") + } + } +} diff --git a/src/main/kotlin/id/walt/services/did/factories/DidIotaFactory.kt b/src/main/kotlin/id/walt/services/did/factories/DidIotaFactory.kt new file mode 100644 index 000000000..9eafadc42 --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/factories/DidIotaFactory.kt @@ -0,0 +1,14 @@ +package id.walt.services.did.factories + +import id.walt.crypto.Key +import id.walt.crypto.KeyAlgorithm +import id.walt.model.Did +import id.walt.services.did.DidOptions +import id.walt.services.ecosystems.iota.IotaService + +class DidIotaFactory : DidFactory { + override fun create(key: Key, options: DidOptions?): Did = let { + if (key.algorithm != KeyAlgorithm.EdDSA_Ed25519) throw IllegalArgumentException("did:iota can not be created with an ${key.algorithm} key.") + IotaService.createDid(key.keyId.id) + } +} diff --git a/src/main/kotlin/id/walt/services/did/factories/DidJwkFactory.kt b/src/main/kotlin/id/walt/services/did/factories/DidJwkFactory.kt new file mode 100644 index 000000000..bb2a4bcdf --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/factories/DidJwkFactory.kt @@ -0,0 +1,18 @@ +package id.walt.services.did.factories + +import com.nimbusds.jose.util.Base64URL +import id.walt.crypto.Key +import id.walt.model.Did +import id.walt.model.DidUrl +import id.walt.services.did.DidOptions +import id.walt.services.did.composers.DidJwkDocumentComposer +import id.walt.services.did.composers.models.DocumentComposerBaseParameter +import id.walt.services.key.KeyService + +class DidJwkFactory( + private val keyService: KeyService, + private val documentComposer: DidJwkDocumentComposer, +) : DidFactory { + override fun create(key: Key, options: DidOptions?): Did = + documentComposer.make(DocumentComposerBaseParameter(DidUrl.from("did:jwk:${Base64URL.encode(keyService.toJwk(key.keyId.id).toString())}"))) +} diff --git a/src/main/kotlin/id/walt/services/did/factories/DidKeyFactory.kt b/src/main/kotlin/id/walt/services/did/factories/DidKeyFactory.kt new file mode 100644 index 000000000..107040d5d --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/factories/DidKeyFactory.kt @@ -0,0 +1,78 @@ +package id.walt.services.did.factories + +import com.beust.klaxon.Klaxon +import id.walt.common.convertToRequiredMembersJsonString +import id.walt.crypto.* +import id.walt.model.Did +import id.walt.model.DidUrl +import id.walt.model.did.DidKey +import id.walt.services.did.DidKeyCreateOptions +import id.walt.services.did.DidOptions +import id.walt.services.did.composers.DidDocumentComposer +import id.walt.services.did.composers.models.DocumentComposerBaseParameter +import id.walt.services.key.KeyService +import id.walt.services.keystore.KeyType +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey +import org.erdtman.jcs.JsonCanonicalizer + +class DidKeyFactory( + private val keyService: KeyService, + private val documentComposer: DidDocumentComposer, +) : DidFactory { + override fun create(key: Key, options: DidOptions?): Did = let { + if (key.algorithm !in setOf( + KeyAlgorithm.EdDSA_Ed25519, KeyAlgorithm.RSA, KeyAlgorithm.ECDSA_Secp256k1, KeyAlgorithm.ECDSA_Secp256r1 + ) + ) throw IllegalArgumentException("did:key can not be created with an ${key.algorithm} key.") + val identifierComponents = getIdentifierComponents(key, options as? DidKeyCreateOptions) + val identifier = convertRawKeyToMultiBase58Btc(identifierComponents.pubKeyBytes, identifierComponents.multiCodecKeyCode) + documentComposer.make(DocumentComposerBaseParameter(DidUrl.from("did:key:$identifier"))) + } + + private fun getIdentifierComponents(key: Key, options: DidKeyCreateOptions?): IdentifierComponents = + options?.takeIf { it.isJwk }?.let { + IdentifierComponents(JwkJcsPubMultiCodecKeyCode, getJwkPubKeyRequiredMembersBytes(key)) + } ?: IdentifierComponents(getMulticodecKeyCode(key.algorithm), getPublicKeyBytesForDidKey(key)) + + private fun getJwkPubKeyRequiredMembersBytes(key: Key) = JsonCanonicalizer( + Klaxon().toJsonString( + convertToRequiredMembersJsonString( + keyService.toJwk( + key.keyId.id, + KeyType.PUBLIC + ) + ) + ) + ).encodedUTF8 + + private fun getPublicKeyBytesForDidKey(key: Key): ByteArray = when (key.algorithm) { + KeyAlgorithm.ECDSA_Secp256k1, KeyAlgorithm.ECDSA_Secp256r1 -> (key.getPublicKey() as BCECPublicKey).q.getEncoded( + true + ) + + KeyAlgorithm.RSA, KeyAlgorithm.EdDSA_Ed25519 -> key.getPublicKeyBytes() + } + + data class IdentifierComponents( + val multiCodecKeyCode: UInt, + val pubKeyBytes: ByteArray, + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IdentifierComponents + + if (multiCodecKeyCode != other.multiCodecKeyCode) return false + if (!pubKeyBytes.contentEquals(other.pubKeyBytes)) return false + + return true + } + + override fun hashCode(): Int { + var result = multiCodecKeyCode.hashCode() + result = 31 * result + pubKeyBytes.contentHashCode() + return result + } + } +} diff --git a/src/main/kotlin/id/walt/services/did/factories/DidWebFactory.kt b/src/main/kotlin/id/walt/services/did/factories/DidWebFactory.kt new file mode 100644 index 000000000..9c1e7cbcc --- /dev/null +++ b/src/main/kotlin/id/walt/services/did/factories/DidWebFactory.kt @@ -0,0 +1,35 @@ +package id.walt.services.did.factories + +import id.walt.crypto.Key +import id.walt.model.Did +import id.walt.model.DidUrl +import id.walt.model.did.DidWeb +import id.walt.services.did.DidOptions +import id.walt.services.did.DidWebCreateOptions +import id.walt.services.did.composers.DidDocumentComposer +import id.walt.services.did.composers.models.DocumentComposerKeyJwkParameter +import id.walt.services.key.KeyService +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +class DidWebFactory( + private val keyService: KeyService, + private val documentComposer: DidDocumentComposer, +) : DidFactory { + override fun create(key: Key, options: DidOptions?): Did = let { + (options as? DidWebCreateOptions) ?: throw Exception("DidWebOptions are mandatory") + if (options.domain.isNullOrEmpty()) throw IllegalArgumentException("Missing 'domain' parameter for creating did:web") + val domain = URLEncoder.encode(options.domain, StandardCharsets.UTF_8) + val path = when { + options.path.isNullOrEmpty() -> "" + else -> ":${ + options.path.split("/").joinToString(":") { part -> URLEncoder.encode(part, StandardCharsets.UTF_8) } + }" + } + documentComposer.make( + DocumentComposerKeyJwkParameter( + DidUrl("web", "$domain$path"), keyService.toJwk(key.keyId.id), key + ) + ) + } +} diff --git a/src/main/kotlin/id/walt/services/did/resolvers/DidEbsiResolver.kt b/src/main/kotlin/id/walt/services/did/resolvers/DidEbsiResolver.kt index d4ceb0371..00673a588 100644 --- a/src/main/kotlin/id/walt/services/did/resolvers/DidEbsiResolver.kt +++ b/src/main/kotlin/id/walt/services/did/resolvers/DidEbsiResolver.kt @@ -1,13 +1,12 @@ package id.walt.services.did.resolvers -import com.beust.klaxon.Klaxon import id.walt.model.Did import id.walt.model.DidUrl -import id.walt.model.Jwk -import id.walt.model.VerificationMethod import id.walt.model.did.DidEbsi import id.walt.services.did.DidEbsiResolveOptions import id.walt.services.did.DidOptions +import id.walt.services.did.composers.DidDocumentComposer +import id.walt.services.did.composers.models.DocumentComposerJwkParameter import id.walt.services.ecosystems.essif.TrustedIssuerClient import id.walt.services.key.KeyService import io.ipfs.multibase.Multibase @@ -21,6 +20,7 @@ import kotlinx.coroutines.runBlocking class DidEbsiResolver( private val httpClient: HttpClient, private val keyService: KeyService, + private val ebsiV2documentComposer: DidDocumentComposer, ) : DidResolverBase() { private val didRegistryPath = "did-registry/${TrustedIssuerClient.apiVersion}/identifiers" @@ -59,24 +59,10 @@ class DidEbsiResolver( private fun resolveDidEbsiV2(didUrl: DidUrl): DidEbsi { val jwk = keyService.toJwk(didUrl.did) - val vmId = "${didUrl.did}#${jwk.computeThumbprint()}" if (DidUrl.generateDidEbsiV2DidUrl(jwk.computeThumbprint().decode()).identifier != didUrl.identifier) { throw IllegalArgumentException("Public key doesn't match with DID identifier") } - return DidEbsi( - context = listOf("https://w3id.org/did/v1"), - id = didUrl.did, - verificationMethod = listOf( - VerificationMethod( - id = vmId, - type = "JsonWebKey2020", - controller = didUrl.did, - publicKeyJwk = Klaxon().parse(jwk.toJSONString()) - ) - ), - authentication = listOf(VerificationMethod.Reference(vmId)), - assertionMethod = listOf(VerificationMethod.Reference(vmId)) - ) + return ebsiV2documentComposer.make(DocumentComposerJwkParameter(didUrl, jwk)) } private fun resolveDidEbsiRaw(did: String): Did = runBlocking { diff --git a/src/main/kotlin/id/walt/services/did/resolvers/DidJwkResolver.kt b/src/main/kotlin/id/walt/services/did/resolvers/DidJwkResolver.kt index 1516ef595..2a5966e33 100644 --- a/src/main/kotlin/id/walt/services/did/resolvers/DidJwkResolver.kt +++ b/src/main/kotlin/id/walt/services/did/resolvers/DidJwkResolver.kt @@ -1,29 +1,14 @@ package id.walt.services.did.resolvers -import com.beust.klaxon.Klaxon -import com.nimbusds.jose.util.Base64URL import id.walt.model.Did import id.walt.model.DidUrl -import id.walt.model.Jwk -import id.walt.model.VerificationMethod import id.walt.services.did.DidOptions +import id.walt.services.did.composers.DidDocumentComposer +import id.walt.services.did.composers.models.DocumentComposerBaseParameter -class DidJwkResolver : DidResolverBase() { - override fun resolve(didUrl: DidUrl, options: DidOptions?): Did = Did( - context = listOf("https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"), - id = didUrl.did, - verificationMethod = listOf( - VerificationMethod( - id = "${didUrl.did}#0", - type = "JsonWebKey2020", - controller = didUrl.did, - publicKeyJwk = Klaxon().parse(Base64URL.from(didUrl.identifier).decodeToString()) - ) - ), - assertionMethod = listOf(VerificationMethod.Reference("${didUrl.did}#0")), - authentication = listOf(VerificationMethod.Reference("${didUrl.did}#0")), - capabilityInvocation = listOf(VerificationMethod.Reference("${didUrl.did}#0")), - capabilityDelegation = listOf(VerificationMethod.Reference("${didUrl.did}#0")), - keyAgreement = listOf(VerificationMethod.Reference("${didUrl.did}#0")) - ) +class DidJwkResolver( + private val documentComposer: DidDocumentComposer, +) : DidResolverBase() { + override fun resolve(didUrl: DidUrl, options: DidOptions?): Did = + documentComposer.make(DocumentComposerBaseParameter(didUrl)) } diff --git a/src/main/kotlin/id/walt/services/did/resolvers/DidKeyResolver.kt b/src/main/kotlin/id/walt/services/did/resolvers/DidKeyResolver.kt index 736899b5f..4705acd68 100644 --- a/src/main/kotlin/id/walt/services/did/resolvers/DidKeyResolver.kt +++ b/src/main/kotlin/id/walt/services/did/resolvers/DidKeyResolver.kt @@ -1,116 +1,13 @@ package id.walt.services.did.resolvers -import com.beust.klaxon.Klaxon -import com.nimbusds.jose.jwk.Curve -import id.walt.crypto.* -import id.walt.model.* +import id.walt.model.DidUrl import id.walt.model.did.DidKey -import id.walt.services.CryptoProvider import id.walt.services.did.DidOptions -import id.walt.services.did.DidService -import java.security.KeyPair +import id.walt.services.did.composers.DidDocumentComposer +import id.walt.services.did.composers.models.DocumentComposerBaseParameter -class DidKeyResolver : DidResolverBase() { - - override fun resolve(didUrl: DidUrl, options: DidOptions?) = resolveDidKey(didUrl) - - private fun resolveDidKey(didUrl: DidUrl): Did { - val keyAlgorithm = getKeyAlgorithmFromMultibase(didUrl.identifier) - val pubKey = convertMultiBase58BtcToRawKey(didUrl.identifier) - return constructDidKey(didUrl, pubKey, keyAlgorithm) - } - - private fun constructDidKey(didUrl: DidUrl, pubKey: ByteArray, keyAlgorithm: KeyAlgorithm): Did { - - val (keyAgreementKeys, verificationMethods, keyRef) = when (keyAlgorithm) { - KeyAlgorithm.EdDSA_Ed25519 -> generateEdParams(pubKey, didUrl) - KeyAlgorithm.ECDSA_Secp256r1, KeyAlgorithm.ECDSA_Secp256k1 -> generateEcKeyParams( - pubKey, - didUrl, - keyAlgorithm - ) - - KeyAlgorithm.RSA -> generateRSAKeyParams(pubKey, didUrl) - } - - return Did( - context = DID_CONTEXT_URL, - id = didUrl.did, - verificationMethod = verificationMethods, - authentication = keyRef, - assertionMethod = keyRef, - capabilityDelegation = keyRef, - capabilityInvocation = keyRef, - keyAgreement = keyAgreementKeys, - service = null - ) - } - - private fun generateEdParams( - pubKey: ByteArray, didUrl: DidUrl - ): Triple?, MutableList, List> { - val dhKey = convertPublicKeyEd25519ToCurve25519(pubKey) - - val dhKeyMb = convertX25519PublicKeyToMultiBase58Btc(dhKey) - - val pubKeyId = didUrl.did + "#" + didUrl.identifier - val dhKeyId = didUrl.did + "#" + dhKeyMb - - val verificationMethods = mutableListOf( - VerificationMethod( - pubKeyId, - LdVerificationKeyType.Ed25519VerificationKey2019.name, - didUrl.did, - pubKey.encodeBase58() - ), - VerificationMethod(dhKeyId, "X25519KeyAgreementKey2019", didUrl.did, dhKey.encodeBase58()) - ) - - return Triple( - listOf(VerificationMethod.Reference(dhKeyId)), - verificationMethods, - listOf(VerificationMethod.Reference(pubKeyId)) - ) - } - - private fun generateEcKeyParams( - pubKey: ByteArray, didUrl: DidUrl, algorithm: KeyAlgorithm - ): Triple?, MutableList, List> { - val curve = if (algorithm == KeyAlgorithm.ECDSA_Secp256k1) Curve.SECP256K1 else Curve.P_256 - val vmType = - if (algorithm == KeyAlgorithm.ECDSA_Secp256k1) LdVerificationKeyType.EcdsaSecp256k1VerificationKey2019.name else LdVerificationKeyType.EcdsaSecp256r1VerificationKey2019.name - - val uncompressedPubKey = uncompressSecp256k1(pubKey, curve) ?: throw IllegalArgumentException("Error uncompressing public key bytes") - val key = Key(newKeyId(), algorithm, CryptoProvider.SUN, KeyPair(uncompressedPubKey.toECPublicKey(), null)) - val pubKeyId = didUrl.did + "#" + didUrl.identifier - - val verificationMethods = mutableListOf( - VerificationMethod( - pubKeyId, - vmType, - didUrl.did, - publicKeyJwk = Klaxon().parse(DidService.keyService.toSecp256Jwk(key, curve, key.keyId.id).toJSONString()) - ) - ) - - return Triple( - null, - verificationMethods, - listOf(VerificationMethod.Reference(pubKeyId)) - ) - } - - private fun generateRSAKeyParams( - pubKey: ByteArray, didUrl: DidUrl - ): Triple?, MutableList, List> { - - val pubKeyId = didUrl.did + "#" + didUrl.identifier - - val verificationMethods = mutableListOf( - VerificationMethod(pubKeyId, LdVerificationKeyType.RsaVerificationKey2018.name, didUrl.did, pubKey.encodeBase58()), - ) - - val keyRef = listOf(VerificationMethod.Reference(pubKeyId)) - return Triple(null, verificationMethods, keyRef) - } +class DidKeyResolver( + private val documentComposer: DidDocumentComposer, +) : DidResolverBase() { + override fun resolve(didUrl: DidUrl, options: DidOptions?) = documentComposer.make(DocumentComposerBaseParameter(didUrl)) } diff --git a/src/main/kotlin/id/walt/services/did/resolvers/DidResolverFactory.kt b/src/main/kotlin/id/walt/services/did/resolvers/DidResolverFactory.kt index a993b0660..608cb68c7 100644 --- a/src/main/kotlin/id/walt/services/did/resolvers/DidResolverFactory.kt +++ b/src/main/kotlin/id/walt/services/did/resolvers/DidResolverFactory.kt @@ -1,7 +1,11 @@ package id.walt.services.did.resolvers +import id.walt.model.Did import id.walt.model.DidMethod +import id.walt.model.did.DidEbsi +import id.walt.model.did.DidKey import id.walt.services.WaltIdServices +import id.walt.services.did.composers.DidDocumentComposer import id.walt.services.ecosystems.iota.IotaWrapper import id.walt.services.key.KeyService import io.ktor.client.* @@ -11,14 +15,17 @@ class DidResolverFactory( private val httpAuth: HttpClient = WaltIdServices.httpWithAuth, private val keyService: KeyService, private val iotaWrapper: IotaWrapper, + private val didKeyDocumentComposer: DidDocumentComposer, + private val didJwkDocumentComposer: DidDocumentComposer, + private val ebsiV2DocumentComposer: DidDocumentComposer, ) { fun create(didMethod: String): DidResolver { return when (DidMethod.valueOf(didMethod)) { - DidMethod.key -> DidKeyResolver() + DidMethod.key -> DidKeyResolver(didKeyDocumentComposer) DidMethod.web -> DidWebResolver(httpNoAuth) - DidMethod.ebsi -> DidEbsiResolver(httpNoAuth, keyService) - DidMethod.jwk -> DidJwkResolver() + DidMethod.ebsi -> DidEbsiResolver(httpNoAuth, keyService, ebsiV2DocumentComposer) + DidMethod.jwk -> DidJwkResolver(didJwkDocumentComposer) DidMethod.iota -> DidIotaResolver(iotaWrapper) DidMethod.cheqd -> DidCheqdResolver(HttpClient())//TODO: fix contentType for application/did+ld+json } diff --git a/src/test/kotlin/id/walt/cli/DidCommandTest.kt b/src/test/kotlin/id/walt/cli/DidCommandTest.kt index 429c7f6cb..0d5b7c83c 100644 --- a/src/test/kotlin/id/walt/cli/DidCommandTest.kt +++ b/src/test/kotlin/id/walt/cli/DidCommandTest.kt @@ -11,6 +11,7 @@ import id.walt.crypto.KeyId import id.walt.model.DidMethod import id.walt.servicematrix.ServiceMatrix import id.walt.services.did.DidService +import id.walt.services.did.DidWebCreateOptions import id.walt.services.key.KeyService import id.walt.test.RESOURCES_PATH import io.kotest.core.spec.style.StringSpec @@ -24,6 +25,7 @@ import kotlin.io.path.readLines class DidCommandTest : StringSpec({ ServiceMatrix("$RESOURCES_PATH/service-matrix.properties") + val webOptions = DidWebCreateOptions("walt.id") beforeTest { File("test-dest.json").delete() @@ -101,21 +103,20 @@ class DidCommandTest : StringSpec({ "8. delete did" { forAll( - row(DidMethod.key, null), - row(DidMethod.web, null), - row(DidMethod.ebsi, null), - row(DidMethod.key, KeyService.getService().generate(ECDSA_Secp256k1).id), - row(DidMethod.key, KeyService.getService().generate(EdDSA_Ed25519).id), - row(DidMethod.key, KeyService.getService().generate(RSA).id), - row(DidMethod.web, KeyService.getService().generate(ECDSA_Secp256k1).id), - row(DidMethod.web, KeyService.getService().generate(EdDSA_Ed25519).id), - row(DidMethod.web, KeyService.getService().generate(RSA).id), - row(DidMethod.ebsi, KeyService.getService().generate(ECDSA_Secp256k1).id), - row(DidMethod.ebsi, KeyService.getService().generate(EdDSA_Ed25519).id), - row(DidMethod.ebsi, KeyService.getService().generate(RSA).id), - ) { method, key -> - val did = DidService.create(method, key) -// val ids = DidService.load(did).verificationMethod?.map { it.id } + row(DidMethod.key, null, null), + row(DidMethod.web, null, webOptions), + row(DidMethod.ebsi, null, null), + row(DidMethod.key, KeyService.getService().generate(ECDSA_Secp256k1).id, null), + row(DidMethod.key, KeyService.getService().generate(EdDSA_Ed25519).id, null), + row(DidMethod.key, KeyService.getService().generate(RSA).id, null), + row(DidMethod.web, KeyService.getService().generate(ECDSA_Secp256k1).id, webOptions), + row(DidMethod.web, KeyService.getService().generate(EdDSA_Ed25519).id, webOptions), + row(DidMethod.web, KeyService.getService().generate(RSA).id, webOptions), + row(DidMethod.ebsi, KeyService.getService().generate(ECDSA_Secp256k1).id, null), + row(DidMethod.ebsi, KeyService.getService().generate(EdDSA_Ed25519).id, null), + row(DidMethod.ebsi, KeyService.getService().generate(RSA).id, null), + ) { method, key, options -> + val did = DidService.create(method, key, options) // delete DeleteDidCommand().parse(listOf("-d", did)) } diff --git a/src/test/kotlin/id/walt/rest/CoreApiTest.kt b/src/test/kotlin/id/walt/rest/CoreApiTest.kt index 7c92ea556..f1a9da88e 100644 --- a/src/test/kotlin/id/walt/rest/CoreApiTest.kt +++ b/src/test/kotlin/id/walt/rest/CoreApiTest.kt @@ -55,6 +55,7 @@ class CoreApiTest : AnnotationSpec() { private val credentialService = JsonLdCredentialService.getService() val CORE_API_URL = "http://localhost:7013" val keyService = KeyService.getService() + private val webOptions = DidWebCreateOptions("walt.id") val client = HttpClient() { install(ContentNegotiation) { @@ -356,20 +357,20 @@ class CoreApiTest : AnnotationSpec() { @Test fun testDidDelete() { forAll( - row(DidMethod.key, null), - row(DidMethod.web, null), - row(DidMethod.ebsi, null), - row(DidMethod.key, keyService.generate(KeyAlgorithm.ECDSA_Secp256k1).id), - row(DidMethod.key, keyService.generate(KeyAlgorithm.EdDSA_Ed25519).id), - row(DidMethod.key, keyService.generate(KeyAlgorithm.RSA).id), - row(DidMethod.web, keyService.generate(KeyAlgorithm.ECDSA_Secp256k1).id), - row(DidMethod.web, keyService.generate(KeyAlgorithm.EdDSA_Ed25519).id), - row(DidMethod.web, keyService.generate(KeyAlgorithm.RSA).id), - row(DidMethod.ebsi, keyService.generate(KeyAlgorithm.ECDSA_Secp256k1).id), - row(DidMethod.ebsi, keyService.generate(KeyAlgorithm.EdDSA_Ed25519).id), - row(DidMethod.ebsi, keyService.generate(KeyAlgorithm.RSA).id), - ) { method, key -> - val did = DidService.create(method, key) + row(DidMethod.key, null, null), + row(DidMethod.web, null, webOptions), + row(DidMethod.ebsi, null, null), + row(DidMethod.key, keyService.generate(KeyAlgorithm.ECDSA_Secp256k1).id, null), + row(DidMethod.key, keyService.generate(KeyAlgorithm.EdDSA_Ed25519).id, null), + row(DidMethod.key, keyService.generate(KeyAlgorithm.RSA).id, null), + row(DidMethod.web, keyService.generate(KeyAlgorithm.ECDSA_Secp256k1).id, webOptions), + row(DidMethod.web, keyService.generate(KeyAlgorithm.EdDSA_Ed25519).id, webOptions), + row(DidMethod.web, keyService.generate(KeyAlgorithm.RSA).id, webOptions), + row(DidMethod.ebsi, keyService.generate(KeyAlgorithm.ECDSA_Secp256k1).id, null), + row(DidMethod.ebsi, keyService.generate(KeyAlgorithm.EdDSA_Ed25519).id, null), + row(DidMethod.ebsi, keyService.generate(KeyAlgorithm.RSA).id, null), + ) { method, key, options -> + val did = DidService.create(method, key, options) val response = runBlocking { client.delete("$CORE_API_URL/v1/did/$did") } response.status shouldBe HttpStatusCode.OK shouldThrow { diff --git a/src/test/kotlin/id/walt/rest/CustodianApiTest.kt b/src/test/kotlin/id/walt/rest/CustodianApiTest.kt index 45685886f..3d1467298 100644 --- a/src/test/kotlin/id/walt/rest/CustodianApiTest.kt +++ b/src/test/kotlin/id/walt/rest/CustodianApiTest.kt @@ -14,6 +14,7 @@ import id.walt.rest.custodian.PresentCredentialsRequest import id.walt.servicematrix.ServiceMatrix import id.walt.services.WaltIdServices.httpNoAuth import id.walt.services.did.DidService +import id.walt.services.did.DidWebCreateOptions import id.walt.services.key.KeyFormat import id.walt.services.key.KeyService import id.walt.services.keystore.KeyType @@ -36,7 +37,7 @@ import java.io.File class CustodianApiTest : StringSpec({ ServiceMatrix("service-matrix.properties") - + val webOptions = DidWebCreateOptions("walt.id") val client = httpNoAuth println("${CustodianAPI.DEFAULT_BIND_ADDRESS}/${CustodianAPI.DEFAULT_Custodian_API_PORT}") @@ -226,20 +227,20 @@ class CustodianApiTest : StringSpec({ "Test delete did" { forAll( - row(DidMethod.key, null), - row(DidMethod.web, null), - row(DidMethod.ebsi, null), - row(DidMethod.key, KeyService.getService().generate(KeyAlgorithm.ECDSA_Secp256k1).id), - row(DidMethod.key, KeyService.getService().generate(KeyAlgorithm.EdDSA_Ed25519).id), - row(DidMethod.key, KeyService.getService().generate(KeyAlgorithm.RSA).id), - row(DidMethod.web, KeyService.getService().generate(KeyAlgorithm.ECDSA_Secp256k1).id), - row(DidMethod.web, KeyService.getService().generate(KeyAlgorithm.EdDSA_Ed25519).id), - row(DidMethod.web, KeyService.getService().generate(KeyAlgorithm.RSA).id), - row(DidMethod.ebsi, KeyService.getService().generate(KeyAlgorithm.ECDSA_Secp256k1).id), - row(DidMethod.ebsi, KeyService.getService().generate(KeyAlgorithm.EdDSA_Ed25519).id), - row(DidMethod.ebsi, KeyService.getService().generate(KeyAlgorithm.RSA).id), - ) { method, key -> - val did = DidService.create(method, key) + row(DidMethod.key, null, null), + row(DidMethod.web, null, webOptions), + row(DidMethod.ebsi, null, null), + row(DidMethod.key, KeyService.getService().generate(KeyAlgorithm.ECDSA_Secp256k1).id, null), + row(DidMethod.key, KeyService.getService().generate(KeyAlgorithm.EdDSA_Ed25519).id, null), + row(DidMethod.key, KeyService.getService().generate(KeyAlgorithm.RSA).id, null), + row(DidMethod.web, KeyService.getService().generate(KeyAlgorithm.ECDSA_Secp256k1).id, webOptions), + row(DidMethod.web, KeyService.getService().generate(KeyAlgorithm.EdDSA_Ed25519).id, webOptions), + row(DidMethod.web, KeyService.getService().generate(KeyAlgorithm.RSA).id, webOptions), + row(DidMethod.ebsi, KeyService.getService().generate(KeyAlgorithm.ECDSA_Secp256k1).id, null), + row(DidMethod.ebsi, KeyService.getService().generate(KeyAlgorithm.EdDSA_Ed25519).id, null), + row(DidMethod.ebsi, KeyService.getService().generate(KeyAlgorithm.RSA).id, null), + ) { method, key, options -> + val did = DidService.create(method, key, options) val response = runBlocking { client.delete("http://${CustodianAPI.DEFAULT_BIND_ADDRESS}:${CustodianAPI.DEFAULT_Custodian_API_PORT}/did/$did") } diff --git a/src/test/kotlin/id/walt/services/did/DidKeyCreationTest.kt b/src/test/kotlin/id/walt/services/did/DidKeyCreationTest.kt index 20098bc46..ed441eb26 100644 --- a/src/test/kotlin/id/walt/services/did/DidKeyCreationTest.kt +++ b/src/test/kotlin/id/walt/services/did/DidKeyCreationTest.kt @@ -5,19 +5,23 @@ import id.walt.crypto.KeyId import id.walt.model.DidMethod import id.walt.servicematrix.ServiceMatrix import id.walt.services.crypto.CryptoService +import id.walt.services.key.KeyService import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe class DidKeyCreationTest : StringSpec({ ServiceMatrix("service-matrix.properties") val cryptoService = CryptoService.getService() + val keyService = KeyService.getService() - fun createAndLoadDid(key: KeyId) { - val did = DidService.create(DidMethod.key, key.id) + fun createAndLoadDid(key: KeyId, options: DidOptions? = null) = let { + val did = DidService.create(DidMethod.key, key.id, options) println("Created: $did") val loaded = DidService.load(did) println("Loaded: $loaded") + loaded } "Create default did:key" { @@ -47,6 +51,10 @@ class DidKeyCreationTest : StringSpec({ createAndLoadDid(key) } - + "Create jwk_jcs-pub did:key" { + val jwkPubKey = "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"ngy44T1vxAT6Di4nr-UaM9K3Tlnz9pkoksDokKFkmNc\",\"y\":\"QCRfOKlSM31GTkb4JHx3nXB4G_jSPMsbdjzlkT_UpPc\"}" + val keyId = keyService.importKey(jwkPubKey) + val result = createAndLoadDid(keyId, DidKeyCreateOptions(isJwk = true)) + result.id shouldBe "did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbsEYvdrjxMjQ4tpnje9BDBTzuNDP3knn6qLZErzd4bJ5go2CChoPjd5GAH3zpFJP5fuwSk66U5Pq6EhF4nKnHzDnznEP8fX99nZGgwbAh1o7Gj1X52Tdhf7U4KTk66xsA5r" + } }) - diff --git a/src/test/kotlin/id/walt/services/did/DidServiceTest.kt b/src/test/kotlin/id/walt/services/did/DidServiceTest.kt index 627b8016e..19419dba2 100644 --- a/src/test/kotlin/id/walt/services/did/DidServiceTest.kt +++ b/src/test/kotlin/id/walt/services/did/DidServiceTest.kt @@ -2,6 +2,7 @@ package id.walt.services.did import com.beust.klaxon.Klaxon import id.walt.common.prettyPrint +import id.walt.common.readWhenContent import id.walt.crypto.KeyAlgorithm import id.walt.crypto.decodeBase58 import id.walt.model.Did @@ -32,6 +33,7 @@ class DidServiceTest : AnnotationSpec() { } private val keyService = KeyService.getService() + private val webOptions = DidWebCreateOptions("walt.id") fun readExampleDid(fileName: String) = File("$RESOURCES_PATH/dids/${fileName}.json").readText(Charsets.UTF_8) @@ -234,6 +236,19 @@ class DidServiceTest : AnnotationSpec() { println(didDoc.prettyPrint()) } + @Test + fun resolveDidKeyJwkJcsPub(){ + // given + val expectedResult = Did.decode(readWhenContent(File("src/test/resources/dids/did-key-jwk_jcs-pub.json")))!! + val jwkPubKey = "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"ngy44T1vxAT6Di4nr-UaM9K3Tlnz9pkoksDokKFkmNc\",\"y\":\"QCRfOKlSM31GTkb4JHx3nXB4G_jSPMsbdjzlkT_UpPc\"}" + val keyId = keyService.importKey(jwkPubKey) + val did = DidService.create(DidMethod.key, keyId.id, DidKeyCreateOptions(isJwk = true)) + // when + val result = DidService.resolve(did) + // then + result.encodePretty() shouldBe expectedResult.encodePretty() + } + @Test fun listDidsTest() { @@ -292,20 +307,20 @@ class DidServiceTest : AnnotationSpec() { @Test fun testDeleteDid() { forAll( - row(DidMethod.key, null), - row(DidMethod.web, null), - row(DidMethod.ebsi, null), - row(DidMethod.key, keyService.generate(KeyAlgorithm.ECDSA_Secp256k1).id), - row(DidMethod.key, keyService.generate(KeyAlgorithm.EdDSA_Ed25519).id), - row(DidMethod.key, keyService.generate(KeyAlgorithm.RSA).id), - row(DidMethod.web, keyService.generate(KeyAlgorithm.ECDSA_Secp256k1).id), - row(DidMethod.web, keyService.generate(KeyAlgorithm.EdDSA_Ed25519).id), - row(DidMethod.web, keyService.generate(KeyAlgorithm.RSA).id), - row(DidMethod.ebsi, keyService.generate(KeyAlgorithm.ECDSA_Secp256k1).id), - row(DidMethod.ebsi, keyService.generate(KeyAlgorithm.EdDSA_Ed25519).id), - row(DidMethod.ebsi, keyService.generate(KeyAlgorithm.RSA).id), - ) { method, kid -> - val did = ds.create(method, kid) + row(DidMethod.key, null, null), + row(DidMethod.web, null, webOptions), + row(DidMethod.ebsi, null, null), + row(DidMethod.key, keyService.generate(KeyAlgorithm.ECDSA_Secp256k1).id, null), + row(DidMethod.key, keyService.generate(KeyAlgorithm.EdDSA_Ed25519).id, null), + row(DidMethod.key, keyService.generate(KeyAlgorithm.RSA).id, null), + row(DidMethod.web, keyService.generate(KeyAlgorithm.ECDSA_Secp256k1).id, webOptions), + row(DidMethod.web, keyService.generate(KeyAlgorithm.EdDSA_Ed25519).id, webOptions), + row(DidMethod.web, keyService.generate(KeyAlgorithm.RSA).id, webOptions), + row(DidMethod.ebsi, keyService.generate(KeyAlgorithm.ECDSA_Secp256k1).id, null), + row(DidMethod.ebsi, keyService.generate(KeyAlgorithm.EdDSA_Ed25519).id, null), + row(DidMethod.ebsi, keyService.generate(KeyAlgorithm.RSA).id, null), + ) { method, kid, options -> + val did = ds.create(method, kid, options) val ids = ds.load(did).verificationMethod?.map { it.id } ds.deleteDid(did) shouldThrow { ds.load(did) } diff --git a/src/test/kotlin/id/walt/services/did/DidWebTest.kt b/src/test/kotlin/id/walt/services/did/DidWebTest.kt index 238b30a47..6d6596298 100644 --- a/src/test/kotlin/id/walt/services/did/DidWebTest.kt +++ b/src/test/kotlin/id/walt/services/did/DidWebTest.kt @@ -14,6 +14,7 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.core.test.TestCase import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import java.util.* class DidWebTest : StringSpec({ @@ -104,7 +105,7 @@ class DidWebTest : StringSpec({ fun createAndTestDidWeb(keyAlgorith: KeyAlgorithm, options: DidWebCreateOptions? = null): Did { val keyService = KeyService.getService() val keyId = keyService.generate(keyAlgorith).id - val did = DidService.create(DidMethod.web, keyId, options) + val did = DidService.create(DidMethod.web, keyId, options ?: DidWebCreateOptions("walt.id", UUID.randomUUID().toString())) println(did) val didUrl = DidUrl.from(did) did shouldBe didUrl.did diff --git a/src/test/kotlin/id/walt/services/vc/WaltIdJsonLdCredentialServiceTest.kt b/src/test/kotlin/id/walt/services/vc/WaltIdJsonLdCredentialServiceTest.kt index 6c86d5fbb..95fb5d636 100644 --- a/src/test/kotlin/id/walt/services/vc/WaltIdJsonLdCredentialServiceTest.kt +++ b/src/test/kotlin/id/walt/services/vc/WaltIdJsonLdCredentialServiceTest.kt @@ -6,6 +6,7 @@ import id.walt.credentials.w3c.schema.SchemaValidatorFactory import id.walt.model.DidMethod import id.walt.servicematrix.ServiceMatrix import id.walt.services.did.DidService +import id.walt.services.did.DidWebCreateOptions import id.walt.signatory.ProofConfig import id.walt.signatory.ProofType import id.walt.signatory.Signatory @@ -27,7 +28,7 @@ class WaltIdJsonLdCredentialServiceTest : AnnotationSpec() { } private val credentialService = JsonLdCredentialService.getService() - private val issuerWebDid = DidService.create(DidMethod.web) + private val issuerWebDid = DidService.create(DidMethod.web, options = DidWebCreateOptions("walt.id")) private val issuerKeyDid = DidService.create(DidMethod.key) private val subjectKeyDid = DidService.create(DidMethod.key) private val anotherKeyDid = DidService.create(DidMethod.key) diff --git a/src/test/resources/dids/did-key-jwk_jcs-pub.json b/src/test/resources/dids/did-key-jwk_jcs-pub.json new file mode 100644 index 000000000..ba89f279d --- /dev/null +++ b/src/test/resources/dids/did-key-jwk_jcs-pub.json @@ -0,0 +1,32 @@ +{ + "assertionMethod" : [ + "did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbsEYvdrjxMjQ4tpnje9BDBTzuNDP3knn6qLZErzd4bJ5go2CChoPjd5GAH3zpFJP5fuwSk66U5Pq6EhF4nKnHzDnznEP8fX99nZGgwbAh1o7Gj1X52Tdhf7U4KTk66xsA5r" + ], + "authentication" : [ + "did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbsEYvdrjxMjQ4tpnje9BDBTzuNDP3knn6qLZErzd4bJ5go2CChoPjd5GAH3zpFJP5fuwSk66U5Pq6EhF4nKnHzDnznEP8fX99nZGgwbAh1o7Gj1X52Tdhf7U4KTk66xsA5r" + ], + "capabilityDelegation" : [ + "did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbsEYvdrjxMjQ4tpnje9BDBTzuNDP3knn6qLZErzd4bJ5go2CChoPjd5GAH3zpFJP5fuwSk66U5Pq6EhF4nKnHzDnznEP8fX99nZGgwbAh1o7Gj1X52Tdhf7U4KTk66xsA5r" + ], + "capabilityInvocation" : [ + "did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbsEYvdrjxMjQ4tpnje9BDBTzuNDP3knn6qLZErzd4bJ5go2CChoPjd5GAH3zpFJP5fuwSk66U5Pq6EhF4nKnHzDnznEP8fX99nZGgwbAh1o7Gj1X52Tdhf7U4KTk66xsA5r" + ], + "@context" : [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id" : "did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbsEYvdrjxMjQ4tpnje9BDBTzuNDP3knn6qLZErzd4bJ5go2CChoPjd5GAH3zpFJP5fuwSk66U5Pq6EhF4nKnHzDnznEP8fX99nZGgwbAh1o7Gj1X52Tdhf7U4KTk66xsA5r", + "verificationMethod" : [ + { + "controller" : "did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbsEYvdrjxMjQ4tpnje9BDBTzuNDP3knn6qLZErzd4bJ5go2CChoPjd5GAH3zpFJP5fuwSk66U5Pq6EhF4nKnHzDnznEP8fX99nZGgwbAh1o7Gj1X52Tdhf7U4KTk66xsA5r", + "id" : "did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbsEYvdrjxMjQ4tpnje9BDBTzuNDP3knn6qLZErzd4bJ5go2CChoPjd5GAH3zpFJP5fuwSk66U5Pq6EhF4nKnHzDnznEP8fX99nZGgwbAh1o7Gj1X52Tdhf7U4KTk66xsA5r#did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbsEYvdrjxMjQ4tpnje9BDBTzuNDP3knn6qLZErzd4bJ5go2CChoPjd5GAH3zpFJP5fuwSk66U5Pq6EhF4nKnHzDnznEP8fX99nZGgwbAh1o7Gj1X52Tdhf7U4KTk66xsA5r", + "publicKeyJwk" : { + "crv" : "P-256", + "kty" : "EC", + "x" : "ngy44T1vxAT6Di4nr-UaM9K3Tlnz9pkoksDokKFkmNc", + "y" : "QCRfOKlSM31GTkb4JHx3nXB4G_jSPMsbdjzlkT_UpPc" + }, + "type" : "JsonWebKey2020" + } + ] +}