diff --git a/prism-agent/api/http/castor/schemas.yaml b/prism-agent/api/http/castor/schemas.yaml index 77390dda29..4f4206c26f 100644 --- a/prism-agent/api/http/castor/schemas.yaml +++ b/prism-agent/api/http/castor/schemas.yaml @@ -15,9 +15,6 @@ components: DIDOperationResponse: type: object required: - - did - - type - - deactivated - scheduledOperation properties: scheduledOperation: @@ -188,6 +185,64 @@ components: example: "eyJhbGciOiJFUzI1NksifQ.eyJ1cGRhdGVLZXkiOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4Ijoid2Z3UUNKM09ScVZkbkhYa1Q4UC1MZ19HdHhCRWhYM3R5OU5VbnduSHJtdyIsInkiOiJ1aWU4cUxfVnVBblJEZHVwaFp1eExPNnFUOWtQcDNLUkdFSVJsVHBXcmZVIn0sImRlbHRhSGFzaCI6IkVpQ3BqTjQ3ZjBNcTZ4RE5VS240aFNlZ01FcW9EU19ycFEyOVd5MVY3M1ZEYncifQ.RwZK1DG5zcr4EsrRImzStb0VX5j2ZqApXZnuoAkA3IoRdErUscNG8RuxNZ0FjlJtjMJ0a-kn-_MdtR0wwvWVgg" description: "Base64 of signature of data in the request" + CreateCustodialDidRequest: + type: object + required: + - documentTemplate + properties: + documentTemplate: + type: object + required: + - storage + - publicKeys + - services + properties: + storage: + type: string + example: "mainnet" + publicKeys: + type: array + items: + type: object + required: + - id + - purposes + properties: + id: + type: string + description: Identifier of a verification material in the DID Document + example: key-01 + purposes: + type: array + items: + type: string + enum: + [ + "authentication", + "assertionMethod", + "keyAgreement", + "capabilityInvocation", + "capabilityDelegation", + ] + example: [ "authentication", "assertionMethod" ] + services: + type: array + items: + $ref: "#/components/schemas/Service" + + CreateCustodialDIDResponse: + type: object + required: + - did + - longFormDid + properties: + did: + $ref: "#/components/schemas/DID" + longFormDid: + type: string + description: A long-form DID for the created DID + example: did:prism:1:abc123:abc123 + Delta: type: object required: @@ -311,7 +366,6 @@ components: publicKeys: $ref: "#/components/schemas/PublicKey" - # ---------------------------------- # ASYNC OPERATIONS # ---------------------------------- diff --git a/prism-agent/api/http/prism-agent-openapi-spec.yaml b/prism-agent/api/http/prism-agent-openapi-spec.yaml index 39d9c9c962..97c4665c0c 100644 --- a/prism-agent/api/http/prism-agent-openapi-spec.yaml +++ b/prism-agent/api/http/prism-agent-openapi-spec.yaml @@ -16,6 +16,8 @@ tags: description: DID Operations REST API - name: DID Authentication description: DID Authentication REST API + - name: DID Registrar + description: DID Registrar REST API # Pollux - name: Schema Registry description: Schema Registry REST API @@ -36,8 +38,8 @@ paths: post: tags: [ "DID" ] operationId: createDid - summary: Create DID with the associated DID Document with the option to publish to blockchain. - description: Create DID with the associated DID Document with the option to publish to blockchain. + summary: Publish DID create operation to blockchain. + description: Publish DID create operation to blockchain. requestBody: required: true content: @@ -67,8 +69,8 @@ paths: get: tags: [ "DID" ] operationId: getDid - summary: Get DID. - description: Get DID. + summary: Resolve DID. + description: Resolve DID. parameters: - $ref: "./castor/parameters.yaml#/components/parameters/didRefInPath" responses: @@ -89,8 +91,8 @@ paths: post: tags: [ "DID" ] operationId: updateDid - summary: Update DID. - description: Update DID. + summary: Publish DID update operation to blockchain. + description: Publish DID update operation to blockchain. parameters: - $ref: "./castor/parameters.yaml#/components/parameters/didRefInPath" requestBody: @@ -128,8 +130,8 @@ paths: post: tags: [ "DID" ] operationId: deactivateDID - summary: Deactivates either published or unpublished DID. - description: Deactivates either published or unpublished DID. + summary: Publish DID deactivate operation to blockchain. + description: Publish DID deactivate operation to blockchain. parameters: - $ref: "./castor/parameters.yaml#/components/parameters/didRefInPath" requestBody: @@ -167,8 +169,8 @@ paths: post: tags: [ "DID" ] operationId: recoverDid - summary: Recover DID. - description: Recover DID. + summary: Publish DID recover operation to blockchain. + description: Publish DID recover operation to blockchain. parameters: - $ref: "./castor/parameters.yaml#/components/parameters/didRefInPath" requestBody: @@ -251,6 +253,7 @@ paths: tags: [ "DID Authentication" ] operationId: createDidAuthenticationChallenge summary: Create a new authentication challenge + deprecated: true description: | Create a new authentication challenge that will be later verified by Castor for a relying-party. @@ -279,6 +282,7 @@ paths: tags: [ "DID Authentication" ] operationId: createDidAuthenticationChallengeSubmission summary: Create a verification from challenge + deprecated: true description: | Submit a challenge submission that will be verified by Castor for a relying-party. requestBody: @@ -301,6 +305,54 @@ paths: schema: $ref: "./castor/schemas.yaml#/components/schemas/ErrorResponse" + /did-registrar/dids: + post: + tags: [ "DID Registrar" ] + operationId: createCustodialDid + summary: Create DID with the DID Document template where keys are managed by PrismAgent. + description: Create DID with the DID Document template where keys are managed by PrismAgent. + requestBody: + required: true + content: + application/json: + schema: + $ref: "./castor/schemas.yaml#/components/schemas/CreateCustodialDidRequest" + responses: + "200": + description: Created unpublished DID. + content: + application/json: + schema: + $ref: "./castor/schemas.yaml#/components/schemas/CreateCustodialDIDResponse" + "422": + description: The DID creation failed. + content: + application/json: + schema: + $ref: "./castor/schemas.yaml#/components/schemas/ErrorResponse" + + /did-registrar/dids/{didRef}/publications: + post: + tags: [ "DID Registrar" ] + operationId: publishCustodialDid + summary: Publish DID stored in PrismAgent wallet to blockchain + description: Publish DID stored in PrismAgent wallet to blockchain + parameters: + - $ref: "./castor/parameters.yaml#/components/parameters/didRefInPath" + responses: + "202": + description: Publishing DID to Blockchain. + content: + application/json: + schema: + $ref: "./castor/schemas.yaml#/components/schemas/DIDOperationResponse" + "422": + description: The DID publication failed. + content: + application/json: + schema: + $ref: "./castor/schemas.yaml#/components/schemas/ErrorResponse" + # ---------------------------------- # Pollux # ---------------------------------- diff --git a/prism-agent/service/build.sbt b/prism-agent/service/build.sbt index f3b9987028..8550c72c0a 100644 --- a/prism-agent/service/build.sbt +++ b/prism-agent/service/build.sbt @@ -10,6 +10,7 @@ val apiBaseDirectory = settingKey[File]("The base directory for PrismAgent API s ThisBuild / apiBaseDirectory := baseDirectory.value / "../api" val commonSettings = Seq( + testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")), githubTokenSource := TokenSource.Environment("ATALA_GITHUB_TOKEN"), resolvers += Resolver.githubPackages("input-output-hk", "atala-prism-sdk"), // Needed for Kotlin coroutines that support new memory management mode diff --git a/prism-agent/service/custodian/src/main/scala/io/iohk/atala/agent/custodian/keystore/DIDKeyStorage.scala b/prism-agent/service/custodian/src/main/scala/io/iohk/atala/agent/custodian/keystore/DIDKeyStorage.scala index c919258226..18ff7a6d40 100644 --- a/prism-agent/service/custodian/src/main/scala/io/iohk/atala/agent/custodian/keystore/DIDKeyStorage.scala +++ b/prism-agent/service/custodian/src/main/scala/io/iohk/atala/agent/custodian/keystore/DIDKeyStorage.scala @@ -18,23 +18,3 @@ private[custodian] trait DIDKeyStorage { def removeKey(did: DID, keyId: String): Task[Option[ECKeyPair]] } - -// TODO: implement -private[custodian] class InMemoryDIDKeyStorage(store: Ref[Map[DID, Map[String, ECKeyPair]]]) extends DIDKeyStorage { - override def listKeys(did: DID): Task[Map[String, ECKeyPair]] = ??? - - override def getKey(did: DID, keyId: String): Task[Option[ECKeyPair]] = ??? - - override def upsertKey(did: DID, keyId: String, keyPair: ECKeyPair): Task[Unit] = ??? - - override def removeKey(did: DID, keyId: String): Task[Option[ECKeyPair]] = ??? - -} - -private[custodian] object InMemoryDIDKeyStorage { - val layer: ULayer[DIDKeyStorage] = { - ZLayer.fromZIO( - Ref.make(Map.empty[DID, Map[String, ECKeyPair]]).map(store => InMemoryDIDKeyStorage(store)) - ) - } -} diff --git a/prism-agent/service/custodian/src/main/scala/io/iohk/atala/agent/custodian/keystore/InMemoryDIDKeyStorage.scala b/prism-agent/service/custodian/src/main/scala/io/iohk/atala/agent/custodian/keystore/InMemoryDIDKeyStorage.scala new file mode 100644 index 0000000000..9cc3ceb2b3 --- /dev/null +++ b/prism-agent/service/custodian/src/main/scala/io/iohk/atala/agent/custodian/keystore/InMemoryDIDKeyStorage.scala @@ -0,0 +1,35 @@ +package io.iohk.atala.agent.custodian.keystore + +import io.iohk.atala.agent.custodian.model.ECKeyPair +import io.iohk.atala.castor.core.model.did.DID +import zio.{Ref, Task, ULayer, ZLayer} + +private[custodian] class InMemoryDIDKeyStorage(store: Ref[Map[DID, Map[String, ECKeyPair]]]) extends DIDKeyStorage { + override def listKeys(did: DID): Task[Map[String, ECKeyPair]] = store.get.map(_.getOrElse(did, Map.empty)) + + override def getKey(did: DID, keyId: String): Task[Option[ECKeyPair]] = listKeys(did).map(_.get(keyId)) + + override def upsertKey(did: DID, keyId: String, keyPair: ECKeyPair): Task[Unit] = store + .update { currentStore => + val currentStoredKeys = currentStore.getOrElse(did, Map.empty) + val updatedStoredKeys = currentStoredKeys.updated(keyId, keyPair) + currentStore.updated(did, updatedStoredKeys) + } + + override def removeKey(did: DID, keyId: String): Task[Option[ECKeyPair]] = store + .getAndUpdate { currentStore => + val currentStoredKeys = currentStore.getOrElse(did, Map.empty) + val updatedStoredKeys = currentStoredKeys.removed(keyId) + currentStore.updated(did, updatedStoredKeys) + } + .map(_.getOrElse(did, Map.empty).get(keyId)) + +} + +private[custodian] object InMemoryDIDKeyStorage { + val layer: ULayer[DIDKeyStorage] = { + ZLayer.fromZIO( + Ref.make(Map.empty[DID, Map[String, ECKeyPair]]).map(store => InMemoryDIDKeyStorage(store)) + ) + } +} diff --git a/prism-agent/service/custodian/src/test/scala/io/iohk/atala/agent/custodian/keystore/InMemoryDIDKeyStorageSpec.scala b/prism-agent/service/custodian/src/test/scala/io/iohk/atala/agent/custodian/keystore/InMemoryDIDKeyStorageSpec.scala new file mode 100644 index 0000000000..a451589016 --- /dev/null +++ b/prism-agent/service/custodian/src/test/scala/io/iohk/atala/agent/custodian/keystore/InMemoryDIDKeyStorageSpec.scala @@ -0,0 +1,130 @@ +package io.iohk.atala.agent.custodian.keystore + +import io.iohk.atala.castor.core.model.did.DID +import io.iohk.atala.agent.custodian.model.* +import io.iohk.atala.agent.custodian.model.ECCoordinates.* +import zio.* +import zio.test.* +import zio.test.Assertion.* + +object InMemoryDIDKeyStorageSpec extends ZIOSpecDefault { + + private val didExample = DID( + method = "example", + methodSpecificId = "abc" + ) + + def generateKeyPair(publicKey: (Int, Int) = (0, 0), privateKey: (Int, Int) = (0, 0)): ECKeyPair = ECKeyPair( + publicKey = ECPublicKey(ECPoint(ECCoordinate.fromBigInt(publicKey._1), ECCoordinate.fromBigInt(publicKey._2))), + privateKey = ECPrivateKey(ECPoint(ECCoordinate.fromBigInt(privateKey._1), ECCoordinate.fromBigInt(privateKey._2))) + ) + + override def spec = suite("InMemoryDIDKeyStorage")( + listKeySpec, + getKeySpec, + upsertKeySpec, + removeKeySpec + ).provideLayer(InMemoryDIDKeyStorage.layer) + + private val listKeySpec = suite("listKeys")( + test("initialize with empty list") { + val result = for { + storage <- ZIO.service[DIDKeyStorage] + keys <- storage.listKeys(didExample) + } yield keys + assertZIO(result)(isEmpty) + }, + test("list all existing keys") { + val keyPairs = Map( + "key-1" -> generateKeyPair(publicKey = (1, 1)), + "key-2" -> generateKeyPair(publicKey = (2, 2)), + "key-3" -> generateKeyPair(publicKey = (3, 3)) + ) + val result = for { + storage <- ZIO.service[DIDKeyStorage] + _ <- ZIO.foreachDiscard(keyPairs) { case (keyId, keyPair) => + storage.upsertKey(didExample, keyId, keyPair) + } + keys <- storage.listKeys(didExample) + } yield keys + assertZIO(result)(hasSameElements(keyPairs)) + } + ) + + private val getKeySpec = suite("getKey")( + test("return stored key if exist") { + val keyPair = generateKeyPair(publicKey = (1, 1)) + val result = for { + storage <- ZIO.service[DIDKeyStorage] + _ <- storage.upsertKey(didExample, "key-1", keyPair) + key <- storage.getKey(didExample, "key-1") + } yield key + assertZIO(result)(isSome(equalTo(keyPair))) + }, + test("return None if stored key doesn't exist") { + val keyPair = generateKeyPair(publicKey = (1, 1)) + val result = for { + storage <- ZIO.service[DIDKeyStorage] + _ <- storage.upsertKey(didExample, "key-1", keyPair) + key <- storage.getKey(didExample, "key-2") + } yield key + assertZIO(result)(isNone) + } + ) + + private val upsertKeySpec = suite("upsertKey")( + test("replace value for existing key") { + val keyPair1 = generateKeyPair(publicKey = (1, 1)) + val keyPair2 = generateKeyPair(publicKey = (2, 2)) + val result = for { + storage <- ZIO.service[DIDKeyStorage] + _ <- storage.upsertKey(didExample, "key-1", keyPair1) + _ <- storage.upsertKey(didExample, "key-1", keyPair2) + key <- storage.getKey(didExample, "key-1") + } yield key + assertZIO(result)(isSome(equalTo(keyPair2))) + } + ) + + private val removeKeySpec = suite("removeKey")( + test("remove existing key and return removed value") { + val keyPair = generateKeyPair(publicKey = (1, 1)) + val result = for { + storage <- ZIO.service[DIDKeyStorage] + _ <- storage.upsertKey(didExample, "key-1", keyPair) + removedKey <- storage.removeKey(didExample, "key-1") + keys <- storage.listKeys(didExample) + } yield (removedKey, keys) + assertZIO(result.map(_._1))(isSome(equalTo(keyPair))) && + assertZIO(result.map(_._2))(isEmpty) + }, + test("remove non-existing key and return None for the removed value") { + val keyPair = generateKeyPair(publicKey = (1, 1)) + val result = for { + storage <- ZIO.service[DIDKeyStorage] + _ <- storage.upsertKey(didExample, "key-1", keyPair) + removedKey <- storage.removeKey(didExample, "key-2") + keys <- storage.listKeys(didExample) + } yield (removedKey, keys) + assertZIO(result.map(_._1))(isNone) && + assertZIO(result.map(_._2))(hasSize(equalTo(1))) + }, + test("remove some of existing keys and keep other keys") { + val keyPairs = Map( + "key-1" -> generateKeyPair(publicKey = (1, 1)), + "key-2" -> generateKeyPair(publicKey = (2, 2)), + "key-3" -> generateKeyPair(publicKey = (3, 3)) + ) + val result = for { + storage <- ZIO.service[DIDKeyStorage] + _ <- ZIO.foreachDiscard(keyPairs) { case (keyId, keyPair) => + storage.upsertKey(didExample, keyId, keyPair) + } + _ <- storage.removeKey(didExample, "key-1") + keys <- storage.listKeys(didExample) + } yield keys + assertZIO(result.map(_.keys))(hasSameElements(Seq("key-2", "key-3"))) + } + ) + +} diff --git a/prism-agent/service/project/Dependencies.scala b/prism-agent/service/project/Dependencies.scala index 591bdf4091..db38600a8b 100644 --- a/prism-agent/service/project/Dependencies.scala +++ b/prism-agent/service/project/Dependencies.scala @@ -15,6 +15,10 @@ object Dependencies { private lazy val zioConfigMagnolia = "dev.zio" %% "zio-config-magnolia" % Versions.zioConfig private lazy val zioConfigTypesafe = "dev.zio" %% "zio-config-typesafe" % Versions.zioConfig + private lazy val zioTest = "dev.zio" %% "zio-test" % Versions.zio % Test + private lazy val zioTestSbt = "dev.zio" %% "zio-test-sbt" % Versions.zio % Test + private lazy val zioTestMagnolia = "dev.zio" %% "zio-test-magnolia" % Versions.zio % Test + private lazy val akkaTyped = "com.typesafe.akka" %% "akka-actor-typed" % Versions.akka private lazy val akkaStream = "com.typesafe.akka" %% "akka-stream" % Versions.akka private lazy val akkaHttp = "com.typesafe.akka" %% "akka-http" % Versions.akkaHttp @@ -27,7 +31,7 @@ object Dependencies { private lazy val polluxSqlDoobie = "io.iohk.atala" %% "pollux-sql-doobie" % Versions.pollux // Dependency Modules - private lazy val baseDependencies: Seq[ModuleID] = Seq(zio, zioConfig, zioConfigMagnolia, zioConfigTypesafe) + private lazy val baseDependencies: Seq[ModuleID] = Seq(zio, zioTest, zioTestSbt, zioTestMagnolia, zioConfig, zioConfigMagnolia, zioConfigTypesafe) private lazy val castorDependencies: Seq[ModuleID] = Seq(castorCore, castorSqlDoobie) private lazy val polluxDependencies: Seq[ModuleID] = Seq(polluxCore, polluxSqlDoobie) private lazy val akkaHttpDependencies: Seq[ModuleID] = Seq(akkaTyped, akkaStream, akkaHttp, akkaSprayJson).map(_.cross(CrossVersion.for3Use2_13)) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/service/DIDRegistrarApiServiceImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/service/DIDRegistrarApiServiceImpl.scala new file mode 100644 index 0000000000..0d9fed2012 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/service/DIDRegistrarApiServiceImpl.scala @@ -0,0 +1,45 @@ +package io.iohk.atala.agent.server.http.service + +import akka.http.scaladsl.marshalling.ToEntityMarshaller +import akka.http.scaladsl.server.Route +import io.iohk.atala.agent.custodian.service.CustodialDIDService +import zio.* +import io.iohk.atala.agent.openapi.api.DIDRegistrarApiService +import io.iohk.atala.agent.openapi.model.{ + CreateCustodialDIDResponse, + CreateCustodialDidRequest, + CreateDIDRequest, + DIDOperationResponse, + DIDResponse, + ErrorResponse +} +import io.iohk.atala.agent.server.http.model.{OASDomainModelHelper, OASErrorModelHelper} + +class DIDRegistrarApiServiceImpl(service: CustodialDIDService)(using runtime: Runtime[Any]) + extends DIDRegistrarApiService, + AkkaZioSupport, + OASDomainModelHelper, + OASErrorModelHelper { + + // TODO: implement + override def createCustodialDid(createCustodialDidRequest: CreateCustodialDidRequest)(implicit + toEntityMarshallerCreateCustodialDIDResponse: ToEntityMarshaller[CreateCustodialDIDResponse], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = ??? + + // TODO: implement + override def publishCustodialDid(didRef: String)(implicit + toEntityMarshallerDIDOperationResponse: ToEntityMarshaller[DIDOperationResponse], + toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] + ): Route = ??? + +} + +object DIDRegistrarApiServiceImpl { + val layer: URLayer[CustodialDIDService, DIDRegistrarApiService] = ZLayer.fromZIO { + for { + rt <- ZIO.runtime[Any] + svc <- ZIO.service[CustodialDIDService] + } yield DIDRegistrarApiServiceImpl(svc)(using rt) + } +}