Skip to content

Commit

Permalink
feat: DIDPeer implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
hamada147 committed Dec 8, 2022
1 parent 0d0545a commit 1e93383
Show file tree
Hide file tree
Showing 13 changed files with 945 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package io.iohk.atala.prism.mercury.didpeer

import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

const val SERVICE_ID = "id"
const val SERVICE_TYPE = "type"
const val SERVICE_ENDPOINT = "serviceEndpoint"
const val SERVICE_DIDCOMM_MESSAGING = "DIDCommMessaging"
const val SERVICE_ROUTING_KEYS = "routingKeys"
const val SERVICE_ACCEPT = "accept"

@Serializable
data class DIDDocPeerDID(
val did: String,
val authentication: List<VerificationMethodPeerDID>,
val keyAgreement: List<VerificationMethodPeerDID> = emptyList(),
val service: List<Service>? = null
) {
val authenticationKids get() = authentication.map { it.id }
val agreementKids get() = keyAgreement.map { it.id }

fun toDict(): Map<String, Any> {
val res = mutableMapOf(
"id" to did,
"authentication" to authentication.map { it.toDict() },
)
if (keyAgreement.isNotEmpty()) {
res["keyAgreement"] = keyAgreement.map { it.toDict() }
}
service?.let {
res["service"] = service.map {
when (it) {
is OtherService -> it.data
is DIDCommServicePeerDID -> it.toDict()
}
}
}
return res
}

fun toJson(): String {
// GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(toDict())
return Json.encodeToString(toDict())
}

companion object {
/**
* Creates a new instance of DIDDocPeerDID from the given DID Doc JSON.
*
* @param value DID Doc JSON
* @throws MalformedPeerDIDDOcException if the input DID Doc JSON is not a valid peerdid DID Doc
* @return [DIDDocPeerDID] instance
*/
fun fromJson(value: JSON): DIDDocPeerDID {
try {
// Two ways
// return didDocFromJson(Json.parseToJsonElement(value).jsonObject)
return Json.decodeFromString<DIDDocPeerDID>(value)
} catch (e: Exception) {
throw MalformedPeerDIDDOcException(e)
}
}
}
}

@Serializable
data class VerificationMethodPeerDID(
val id: String,
val controller: String,
val verMaterial: VerificationMaterialPeerDID<out VerificationMethodTypePeerDID>
) {

private fun publicKeyField() =
when (verMaterial.format) {
VerificationMaterialFormatPeerDID.BASE58 -> PublicKeyField.BASE58
VerificationMaterialFormatPeerDID.JWK -> PublicKeyField.JWK
VerificationMaterialFormatPeerDID.MULTIBASE -> PublicKeyField.MULTIBASE
}

fun toDict() = mapOf(
"id" to id,
"type" to verMaterial.type.value,
"controller" to controller,
publicKeyField().value to verMaterial.value,
)
}

sealed interface Service

data class OtherService(val data: Map<String, Any>) : Service

data class DIDCommServicePeerDID(
val id: String,
val type: String,
val serviceEndpoint: String,
val routingKeys: List<String>,
val accept: List<String>
) : Service {

fun toDict(): MutableMap<String, Any> {
val res = mutableMapOf<String, Any>(
SERVICE_ID to id,
SERVICE_TYPE to type,
)
res[SERVICE_ENDPOINT] = serviceEndpoint
res[SERVICE_ROUTING_KEYS] = routingKeys
res[SERVICE_ACCEPT] = accept
return res
}
}

enum class PublicKeyField(val value: String) {
BASE58("publicKeyBase58"),
MULTIBASE("publicKeyMultibase"),
JWK("publicKeyJwk");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.iohk.atala.prism.mercury.didpeer

/**
* The base class for all PeerDID errors and exceptions.
*
* @param message - the detail message.
* @param cause - the cause of this.
*/
open class PeerDIDException(message: String, cause: Throwable? = null) : Throwable(message, cause)

/**
* Raised if the peer DID to be resolved in not a valid peer DID.
*
* @param message - the detail message.
* @param cause - the cause of this.
*/
class MalformedPeerDIDException(message: String, cause: Throwable? = null) :
PeerDIDException("Invalid peer DID provided. $message", cause)

/**
* Raised if the resolved peer DID Doc to be resolved in not a valid peer DID.
*
* @param cause - the cause of this.
*/
class MalformedPeerDIDDOcException(cause: Throwable? = null) :
PeerDIDException("Invalid peer DID Doc", cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
@file:JvmName("PeerDIDCreator")
package io.iohk.atala.prism.mercury.didpeer

import io.iohk.atala.prism.mercury.didpeer.core.Numalgo2Prefix
import io.iohk.atala.prism.mercury.didpeer.core.createMultibaseEncnumbasis
import io.iohk.atala.prism.mercury.didpeer.core.encodeService
import io.iohk.atala.prism.mercury.didpeer.core.validateAgreementMaterialType
import io.iohk.atala.prism.mercury.didpeer.core.validateAuthenticationMaterialType
import kotlin.jvm.JvmName

/**
* Checks if [peerDID] param matches PeerDID spec
* @see
* <a href="https://identity.foundation/peer-did-method-spec/index.html#matching-regex">Specification</a>
* @param [peerDID] PeerDID to check
* @return true if [peerDID] matches spec, otherwise false
*/
fun isPeerDID(peerDID: String): Boolean {
val regex =
(
"^did:peer:(([0](z)([1-9a-km-zA-HJ-NP-Z]{46,47}))" +
"|(2((.[AEVID](z)([1-9a-km-zA-HJ-NP-Z]{46,47}))+(.(S)[0-9a-zA-Z=]*)?)))$"
).toRegex()
return regex.matches(peerDID)
}

/**
* Generates PeerDID according to the zero algorithm
* For this type of algorithm DIDDoc can be obtained from PeerDID
* @see
* <a href="https://identity.foundation/peer-did-method-spec/index.html#generation-method">Specification</a>
* @param [inceptionKey] the key that creates the DID and authenticates when exchanging it with the first peer
* @throws IllegalArgumentException if the [inceptionKey] is not correctly encoded
* @return generated PeerDID
*/
fun createPeerDIDNumalgo0(inceptionKey: VerificationMaterialAuthentication): PeerDID {
validateAuthenticationMaterialType(inceptionKey)
return "did:peer:0${createMultibaseEncnumbasis(inceptionKey)}"
}

/**
* Generates PeerDID according to the second algorithm
* For this type of algorithm DIDDoc can be obtained from PeerDID
* @see
* <a href="https://identity.foundation/peer-did-method-spec/index.html#generation-method">Specification</a>
* @param [encryptionKeys] list of encryption keys
* @param [signingKeys] list of signing keys
* @param [service] JSON string conforming to the DID specification
* @throws IllegalArgumentException
* - if at least one of keys is not properly encoded
* - if service is not a valid JSON
* @return generated PeerDID
*/
fun createPeerDIDNumalgo2(
encryptionKeys: List<VerificationMaterialAgreement>,
signingKeys: List<VerificationMaterialAuthentication>,
service: JSON?
): PeerDID {
encryptionKeys.forEach { validateAgreementMaterialType(it) }
signingKeys.forEach { validateAuthenticationMaterialType(it) }

val encodedEncryptionKeysStr = encryptionKeys
.map { createMultibaseEncnumbasis(it) }
.map { ".${Numalgo2Prefix.KEY_AGREEMENT.prefix}$it" }
.joinToString("")
val encodedSigningKeysStr = signingKeys
.map { createMultibaseEncnumbasis(it) }
.map { ".${Numalgo2Prefix.AUTHENTICATION.prefix}$it" }
.joinToString("")
val encodedService = if (service.isNullOrEmpty()) "" else encodeService(service)

return "did:peer:2$encodedEncryptionKeysStr$encodedSigningKeysStr$encodedService"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
@file:JvmName("PeerDIDResolver")
package io.iohk.atala.prism.mercury.didpeer

import io.iohk.atala.prism.mercury.didpeer.core.DecodedEncumbasis
import io.iohk.atala.prism.mercury.didpeer.core.Numalgo2Prefix
import io.iohk.atala.prism.mercury.didpeer.core.decodeMultibaseEncnumbasis
import io.iohk.atala.prism.mercury.didpeer.core.decodeService
import io.iohk.atala.prism.mercury.didpeer.core.getVerificationMethod
import io.iohk.atala.prism.mercury.didpeer.core.validateAgreementMaterialType
import io.iohk.atala.prism.mercury.didpeer.core.validateAuthenticationMaterialType
import kotlin.jvm.JvmName

/** Resolves [DIDDocPeerDID] from [PeerDID]
* @param [peerDID] PeerDID to resolve
* @param [format] The format of public keys in the DID DOC. Default format is multibase.
* @throws MalformedPeerDIDException
* - if [peerDID] parameter does not match [peerDID] spec
* - if a valid DIDDoc cannot be produced from the [peerDID]
* @return resolved [DIDDocPeerDID] as JSON string
*/
fun resolvePeerDID(
peerDID: PeerDID,
format: VerificationMaterialFormatPeerDID = VerificationMaterialFormatPeerDID.MULTIBASE
): String {
if (!isPeerDID(peerDID)) {
throw MalformedPeerDIDException("Does not match peer DID regexp: $peerDID")
}
val didDoc = when (peerDID[9]) {
'0' -> buildDIDDocNumalgo0(peerDID, format)
'2' -> buildDIDDocNumalgo2(peerDID, format)
else -> throw IllegalArgumentException("Invalid numalgo of Peer DID: $peerDID")
}
return didDoc.toJson()
}

private fun buildDIDDocNumalgo0(peerDID: PeerDID, format: VerificationMaterialFormatPeerDID): DIDDocPeerDID {
val inceptionKey = peerDID.substring(10)
val decodedEncumbasis = decodeMultibaseEncnumbasisAuth(inceptionKey, format)
return DIDDocPeerDID(
did = peerDID,
authentication = listOf(getVerificationMethod(peerDID, decodedEncumbasis))
)
}

private fun buildDIDDocNumalgo2(peerDID: PeerDID, format: VerificationMaterialFormatPeerDID): DIDDocPeerDID {
val keys = peerDID.drop(11)

var service = ""
val authentications = mutableListOf<VerificationMethodPeerDID>()
val keyAgreement = mutableListOf<VerificationMethodPeerDID>()

keys.split(".").forEach {
val prefix = it[0]
val value = it.drop(1)

when (prefix) {
Numalgo2Prefix.SERVICE.prefix -> service = value

Numalgo2Prefix.AUTHENTICATION.prefix -> {
val decodedEncumbasis = decodeMultibaseEncnumbasisAuth(value, format)
authentications.add(getVerificationMethod(peerDID, decodedEncumbasis))
}

Numalgo2Prefix.KEY_AGREEMENT.prefix -> {
val decodedEncumbasis = decodeMultibaseEncnumbasisAgreement(value, format)
keyAgreement.add(getVerificationMethod(peerDID, decodedEncumbasis))
}

else -> throw IllegalArgumentException("Unsupported transform part of PeerDID: $prefix")
}
}

val decodedService = doDecodeService(service, peerDID)

return DIDDocPeerDID(
did = peerDID,
authentication = authentications,
keyAgreement = keyAgreement,
service = decodedService
)
}

private fun decodeMultibaseEncnumbasisAuth(
multibase: String,
format: VerificationMaterialFormatPeerDID
): DecodedEncumbasis {
try {
val decodedEncumbasis = decodeMultibaseEncnumbasis(multibase, format)
validateAuthenticationMaterialType(decodedEncumbasis.verMaterial)
return decodedEncumbasis
} catch (e: IllegalArgumentException) {
throw MalformedPeerDIDException("Invalid key $multibase", e)
}
}

private fun decodeMultibaseEncnumbasisAgreement(
multibase: String,
format: VerificationMaterialFormatPeerDID
): DecodedEncumbasis {
try {
val decodedEncumbasis = decodeMultibaseEncnumbasis(multibase, format)
validateAgreementMaterialType(decodedEncumbasis.verMaterial)
return decodedEncumbasis
} catch (e: IllegalArgumentException) {
throw MalformedPeerDIDException("Invalid key $multibase", e)
}
}

private fun doDecodeService(service: String, peerDID: String): List<Service>? {
try {
return decodeService(service, peerDID)
} catch (e: IllegalArgumentException) {
throw MalformedPeerDIDException("Invalid service", e)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.iohk.atala.prism.mercury.didpeer

import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable

enum class VerificationMaterialFormatPeerDID {
JWK,
BASE58,
MULTIBASE;
}

@Serializable
sealed class VerificationMethodTypePeerDID(val value: String)

sealed class VerificationMethodTypeAgreement(value: String) : VerificationMethodTypePeerDID(value) {
object JSON_WEB_KEY_2020 : VerificationMethodTypeAgreement("JsonWebKey2020")
object X25519_KEY_AGREEMENT_KEY_2019 : VerificationMethodTypeAgreement("X25519KeyAgreementKey2019")
object X25519_KEY_AGREEMENT_KEY_2020 : VerificationMethodTypeAgreement("X25519KeyAgreementKey2020")
}

sealed class VerificationMethodTypeAuthentication(value: String) : VerificationMethodTypePeerDID(value) {
object JSON_WEB_KEY_2020 : VerificationMethodTypeAuthentication("JsonWebKey2020")
object ED25519_VERIFICATION_KEY_2018 : VerificationMethodTypeAuthentication("Ed25519VerificationKey2018")
object ED25519_VERIFICATION_KEY_2020 : VerificationMethodTypeAuthentication("Ed25519VerificationKey2020")
}

@Serializable
data class VerificationMaterialPeerDID<T : VerificationMethodTypePeerDID>(
val format: VerificationMaterialFormatPeerDID,
@Contextual val value: Any,
val type: T
)

typealias VerificationMaterialAgreement = VerificationMaterialPeerDID<VerificationMethodTypeAgreement>
typealias VerificationMaterialAuthentication = VerificationMaterialPeerDID<VerificationMethodTypeAuthentication>

typealias JSON = String
typealias PeerDID = String
Loading

0 comments on commit 1e93383

Please sign in to comment.