Skip to content

Commit

Permalink
[ATL-1926] feat(castor): implement createPublishedDID (2) (#53)
Browse files Browse the repository at this point in the history
* feat(castor): partially implement createPublishedDID

* feat(castor): add missing layer in prism-agent

* feat(castor): fix typo

* feat(castor): move common primitives to shared

* feat(castor): move common primitives to shared

* feat(agent): integrate castor createPublishedDID with HTTP endpoint

* feat(castor): add validation for unique id

* chore(iris): format source

* feat(castor): add operation validator unique key tests

* feat(castor): add exisitng did check when create new did

* feat(castor): implement data access for castor

* chore(agent): use provided helper functions for model conversion

* chore: remove wrongly added directory

* feat(castor): use OperationOutcome as operation result

* feat(castor): simplify OAS to only account for PublishedDID

* feat(castor): remove unnecessary fields in OAS

* feat(castor): add PrismDID model

* feat(agent): convert castor operation outcome to http response

* chore(agent): remove inaccurate comment

* chore: pr diff cleanup

* feat(infra): make agent uses DB in local deployment

* chore: pr diff cleanup
  • Loading branch information
patlo-iog authored Oct 17, 2022
1 parent 7108401 commit f6afd38
Show file tree
Hide file tree
Showing 37 changed files with 798 additions and 267 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
package io.iohk.atala.castor.core.model

import java.net.URI
import com.google.protobuf.ByteString
import io.iohk.atala.shared.models.HexStrings.*
import io.iohk.atala.shared.models.Base64UrlStrings.*
import io.iohk.atala.shared.utils.Traverse.*
import io.iohk.atala.castor.core.model.did.{
DIDDocument,
DIDStorage,
EllipticCurve,
PublicKey,
PublicKeyJwk,
PublishedDIDOperation,
PublishedDIDOperationOutcome,
Service,
ServiceType,
VerificationRelationship
}
import io.iohk.atala.iris.proto as iris_proto
import io.iohk.atala.iris.proto.did_operations.DocumentDefinition.PublicKey.Purpose
import io.iohk.atala.iris.proto.did_operations.DocumentDefinition.Service.Type
import io.iohk.atala.iris.proto.did_operations.PublicKeyJwk.{Curve, Key}

import scala.util.Try

private[castor] trait ProtoModelHelper {

Expand All @@ -21,16 +31,12 @@ private[castor] trait ProtoModelHelper {
}

extension (operation: PublishedDIDOperation.Create) {
def toProto: iris_proto.dlt.IrisOperation = {
iris_proto.dlt.IrisOperation(
operation = iris_proto.dlt.IrisOperation.Operation.CreateDid(
value = iris_proto.did_operations.CreateDid(
initialUpdateCommitment = operation.updateCommitment.toByteArray.toProto,
initialRecoveryCommitment = operation.recoveryCommitment.toByteArray.toProto,
ledger = operation.storage.ledgerName,
document = Some(operation.document.toProto)
)
)
def toProto: iris_proto.did_operations.CreateDid = {
iris_proto.did_operations.CreateDid(
initialUpdateCommitment = operation.updateCommitment.toByteArray.toProto,
initialRecoveryCommitment = operation.recoveryCommitment.toByteArray.toProto,
ledger = operation.storage.ledgerName,
document = Some(operation.document.toProto)
)
}
}
Expand Down Expand Up @@ -114,4 +120,101 @@ private[castor] trait ProtoModelHelper {
}
}

extension (op: iris_proto.did_operations.CreateDid) {
def toDomain: Either[String, PublishedDIDOperation.Create] =
for {
document <- op.document.toRight("expected a DIDDocument in the protobuf message").flatMap(_.toDomain)
} yield PublishedDIDOperation.Create(
updateCommitment = HexString.fromByteArray(op.initialUpdateCommitment.toByteArray),
recoveryCommitment = HexString.fromByteArray(op.initialRecoveryCommitment.toByteArray),
storage = DIDStorage.Cardano(op.ledger),
document = document
)
}

extension (doc: iris_proto.did_operations.DocumentDefinition) {
def toDomain: Either[String, DIDDocument] =
for {
publicKeys <- doc.publicKeys.traverse(_.toDomain)
services <- doc.services.traverse(_.toDomain)
} yield DIDDocument(
publicKeys = publicKeys,
services = services
)
}

extension (service: iris_proto.did_operations.DocumentDefinition.Service) {
def toDomain: Either[String, Service] = {
for {
serviceType <- service.`type`.toDomain
serviceEndpoint <- Try(URI.create(service.serviceEndpoint)).toEither.left.map(_ =>
s"unable to parse serviceEndpoint ${service.serviceEndpoint} as URI"
)
} yield Service(
id = service.id,
`type` = serviceType,
serviceEndpoint = serviceEndpoint
)
}
}

extension (serviceType: iris_proto.did_operations.DocumentDefinition.Service.Type) {
def toDomain: Either[String, ServiceType] = {
serviceType match {
case Type.MEDIATOR_SERVICE => Right(ServiceType.MediatorService)
case Type.Unrecognized(value) => Left(s"unrecognized serviceType value $value")
}
}
}

extension (publicKey: iris_proto.did_operations.DocumentDefinition.PublicKey) {
def toDomain: Either[String, PublicKey] =
for {
purposes <- publicKey.purposes.traverse(_.toDomain)
publicKeysJwk <- publicKey.jwk
.toRight(s"publicKeyJwk does not exist on key id ${publicKey.id}")
.flatMap(_.toDomain)
} yield PublicKey.JsonWebKey2020(
id = publicKey.id,
purposes = purposes,
publicKeyJwk = publicKeysJwk
)
}

extension (purpose: iris_proto.did_operations.DocumentDefinition.PublicKey.Purpose) {
def toDomain: Either[String, VerificationRelationship] = {
purpose match {
case Purpose.AUTHENTICATION => Right(VerificationRelationship.Authentication)
case Purpose.KEY_AGREEMENT => Right(VerificationRelationship.KeyAgreement)
case Purpose.ASSERTION_METHOD => Right(VerificationRelationship.AssertionMethod)
case Purpose.CAPABILITY_INVOCATION => Right(VerificationRelationship.CapabilityInvocation)
case Purpose.Unrecognized(value) => Left(s"unrecognized publicKey purpose $value")
}
}
}

extension (jwk: iris_proto.did_operations.PublicKeyJwk) {
def toDomain: Either[String, PublicKeyJwk] = {
val errorOrEcKey = jwk.key match {
case Key.Empty => Left("publicKeyJwk value does not exist")
case Key.EcKey(value) => Right(value)
}
for {
ecKey <- errorOrEcKey
curve <- ecKey.curve.toDomain
} yield PublicKeyJwk.ECPublicKeyData(
crv = curve,
x = Base64UrlString.fromByteArray(ecKey.x.toByteArray),
y = Base64UrlString.fromByteArray(ecKey.y.toByteArray)
)
}
}

extension (curve: iris_proto.did_operations.PublicKeyJwk.Curve) {
def toDomain: Either[String, EllipticCurve] = curve match {
case Curve.SECP256K1 => Right(EllipticCurve.SECP256K1)
case Curve.Unrecognized(value) => Left(s"unrecognized elliptic-curve value $value")
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.iohk.atala.castor.core.model.did

import java.time.Instant

final case class ConfirmedPublishedDIDOperation(
operation: PublishedDIDOperation,
anchoredAt: Instant,
blockNumber: Int,
blockIndex: Int
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
package io.iohk.atala.castor.core.model.did

sealed trait EllipticCurve
// EC Name is used in JWK https://w3c-ccg.github.io/security-vocab/#publicKeyJwk
// It MUST match the curve name in https://www.iana.org/assignments/jose/jose.xhtml
// in the "JSON Web Key Elliptic Curve" section
enum EllipticCurve(val name: String) {
case SECP256K1 extends EllipticCurve("secp256k1")
}

object EllipticCurve {
case object SECP256K1 extends EllipticCurve

private val lookup = EllipticCurve.values.map(i => i.name -> i).toMap

def parseString(s: String): Option[EllipticCurve] = lookup.get(s)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.iohk.atala.castor.core.model.did

import io.iohk.atala.castor.core.model.ProtoModelHelper
import io.iohk.atala.prism.crypto.Sha256
import io.iohk.atala.shared.models.HexStrings.HexString

enum PrismDIDVersion(val id: Int) {
case V0 extends PrismDIDVersion(0)
case V1 extends PrismDIDVersion(1)
}

/** Represents a [Did] used in PRISM with prism-specific method and keys as [PrismDid]
*/
sealed trait PrismDID {

val version: PrismDIDVersion

val versionSpecificId: String

override def toString: String = did.toString

def did: DID = DID(
method = "prism",
methodSpecificId = version match {
case PrismDIDVersion.V0 => versionSpecificId
case _ => s"${version.id}:$versionSpecificId"
}
)

}

final case class PrismDIDV1 private (suffix: HexString) extends PrismDID {

override val version: PrismDIDVersion = PrismDIDVersion.V1

override val versionSpecificId: String = suffix.toString

}

object PrismDIDV1 extends ProtoModelHelper {
def fromCreateOperation(op: PublishedDIDOperation.Create): PrismDIDV1 = {
val createDIDProto = op.toProto
val initialState = createDIDProto.toByteArray
val suffix = HexString.fromByteArray(Sha256.compute(initialState).getValue)
PrismDIDV1(suffix)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ object PublishedDIDOperation {
document: DIDDocument
) extends PublishedDIDOperation
}

final case class PublishedDIDOperationOutcome(
did: PrismDID,
operation: PublishedDIDOperation,
operationId: HexString
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package io.iohk.atala.castor.core.model.did

sealed trait ServiceType
enum ServiceType(val name: String) {
case MediatorService extends ServiceType("MediatorService")
}

object ServiceType {
case object MediatorService extends ServiceType

private val lookup = ServiceType.values.map(i => i.name -> i).toMap

def parseString(s: String): Option[ServiceType] = lookup.get(s)

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package io.iohk.atala.castor.core.model.did

sealed trait VerificationRelationship
enum VerificationRelationship(val name: String) {
case Authentication extends VerificationRelationship("authentication")
case AssertionMethod extends VerificationRelationship("assertionMethod")
case KeyAgreement extends VerificationRelationship("keyAgreement")
case CapabilityInvocation extends VerificationRelationship("capabilityInvocation")
}

object VerificationRelationship {
case object Authentication extends VerificationRelationship

case object AssertionMethod extends VerificationRelationship
private val lookup = VerificationRelationship.values.map(i => i.name -> i).toMap

case object KeyAgreement extends VerificationRelationship
def parseString(s: String): Option[VerificationRelationship] = lookup.get(s)

case object CapabilityInvocation extends VerificationRelationship
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ package object error {
object DIDOperationError {
final case class DLTProxyError(cause: Throwable) extends DIDOperationError
final case class InvalidArgument(msg: String) extends DIDOperationError
final case class InvalidPrecondition(msg: String) extends DIDOperationError
final case class TooManyDidPublicKeyAccess(limit: Int, access: Option[Int]) extends DIDOperationError
final case class TooManyDidServiceAccess(limit: Int, access: Option[Int]) extends DIDOperationError
final case class InternalErrorDB(cause: Throwable) extends DIDOperationError
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package io.iohk.atala.castor.core.repository

import io.iohk.atala.castor.core.model.IrisNotification
import io.iohk.atala.castor.core.model.did.{ConfirmedPublishedDIDOperation, PrismDIDV1}
import zio.*

// TODO: replace with actual implementation
trait DIDOperationRepository[F[_]] {
def getIrisNotification: F[Seq[IrisNotification]]
def getConfirmedPublishedDIDOperations(did: PrismDIDV1): F[Seq[ConfirmedPublishedDIDOperation]]
}
Original file line number Diff line number Diff line change
@@ -1,44 +1,77 @@
package io.iohk.atala.castor.core.service

import io.iohk.atala.castor.core.model.did.{DIDDocument, PublishedDIDOperation}
import io.iohk.atala.castor.core.model.did.{
DIDDocument,
PrismDIDV1,
PublishedDIDOperation,
PublishedDIDOperationOutcome
}
import zio.*
import io.iohk.atala.castor.core.model.ProtoModelHelper
import io.iohk.atala.castor.core.model.error.DIDOperationError
import io.iohk.atala.castor.core.repository.DIDOperationRepository
import io.iohk.atala.castor.core.util.DIDOperationValidator
import io.iohk.atala.iris.proto.service.IrisServiceGrpc.IrisServiceStub
import io.iohk.atala.prism.crypto.Sha256
import io.iohk.atala.shared.models.HexStrings.HexString
import io.iohk.atala.iris.proto as iris_proto

trait DIDService {
def createPublishedDID(operation: PublishedDIDOperation.Create): IO[DIDOperationError, DIDDocument]
def createPublishedDID(operation: PublishedDIDOperation.Create): IO[DIDOperationError, PublishedDIDOperationOutcome]
}

object MockDIDService {
val layer: ULayer[DIDService] = ZLayer.succeed {
new DIDService {
def createPublishedDID(operation: PublishedDIDOperation.Create): IO[DIDOperationError, DIDDocument] =
def createPublishedDID(
operation: PublishedDIDOperation.Create
): IO[DIDOperationError, PublishedDIDOperationOutcome] =
ZIO.fail(DIDOperationError.InvalidArgument("mocked error"))
}
}
}

object DIDServiceImpl {
val layer: URLayer[IrisServiceStub & DIDOperationValidator, DIDService] = ZLayer.fromFunction(DIDServiceImpl(_, _))
val layer: URLayer[IrisServiceStub & DIDOperationValidator & DIDOperationRepository[Task], DIDService] =
ZLayer.fromFunction(DIDServiceImpl(_, _, _))
}

private class DIDServiceImpl(irisClient: IrisServiceStub, operationValidator: DIDOperationValidator)
extends DIDService,
private class DIDServiceImpl(
irisClient: IrisServiceStub,
operationValidator: DIDOperationValidator,
didOpRepo: DIDOperationRepository[Task]
) extends DIDService,
ProtoModelHelper {

// TODO:
// 1. generate DID identifier from operation
// 2. check if DID already exists
// 3. persist state
override def createPublishedDID(operation: PublishedDIDOperation.Create): IO[DIDOperationError, DIDDocument] = {
override def createPublishedDID(
operation: PublishedDIDOperation.Create
): IO[DIDOperationError, PublishedDIDOperationOutcome] = {
val prismDID = PrismDIDV1.fromCreateOperation(operation)
val irisOpProto = iris_proto.dlt.IrisOperation(
operation = iris_proto.dlt.IrisOperation.Operation.CreateDid(operation.toProto)
)
for {
_ <- ZIO.fromEither(operationValidator.validate(operation))
_ <- ZIO
.fromFuture(_ => irisClient.scheduleOperation(operation.toProto))
confirmedOps <- didOpRepo
.getConfirmedPublishedDIDOperations(prismDID)
.mapError(DIDOperationError.InternalErrorDB.apply)
_ <- confirmedOps
.map(_.operation)
.collectFirst { case op: PublishedDIDOperation.Create => op }
.fold(ZIO.unit)(_ =>
ZIO.fail(
DIDOperationError
.InvalidPrecondition(s"PublishedDID with suffix ${prismDID.did} has already been created and confirmed")
)
)
irisOutcome <- ZIO
.fromFuture(_ => irisClient.scheduleOperation(irisOpProto))
.mapError(DIDOperationError.DLTProxyError.apply)
} yield operation.document
} yield PublishedDIDOperationOutcome(
did = prismDID,
operation = operation,
operationId = HexString.fromByteArray(irisOutcome.operationId.toByteArray)
)
}

}
Loading

0 comments on commit f6afd38

Please sign in to comment.