Skip to content

Commit

Permalink
feat(agent): integrate key-manage into prism-agent server (#77)
Browse files Browse the repository at this point in the history
* feat(agent): add keystore subproject

* feat(agent): add intefaces & models for custodian layer

* docs(agent): add readme about key-mangement

* feat(agent): fix as reviewed

* feat(agent): implement InMemoryDIDKeyStorage

* feat(agent): add tests for InMemoryDIDKeyStorage

* feat(agent): add createCustodialDID endpoint

* feat(agent): refine createCustodialDID endpoint

* feat(agent): refine createCustodialDID endpoint

* feat(agent): make DIDKeyStorage use PrismDID

* feat(castor): add LongFormPrismDID model

* feat(agent): add DID commitment storage

* feat(agent): add key generation in ManagedDIDService

* feat(agent): fix failing test

* feat(agent): resolve merge conflict

* feat(agent): rename OAS model to avoid confusion

* feat(agent): rename subproject away from custodian

* feat(agent): make prism-crypto key generator works

* feat(agent): add tests for prism-crypto int conversion

* feat(agent): store DID secret when create

* feat(agent): add validation when createManagedDID

* feat(agent): add DIDNonSecretStorage & duplicated DID validation

* feat(agent): add ability to publish stored did

* feat(agent): add commitment value InMemorySecretStorage tests

* feat(agent): add createAndStoreDID tests

* feat(agent): add tests for publishStoredDID

* feat(prism-agent): pr diff cleanup

* style(shared): run formatter on shared

* fix(iris): align type signature

* feat(agent): rename keymanagement to walletapi

* feat(agent): add mock stub & link ManagedDIDService to HTTP server

* feat(agent): integrate publishStoredDID in HTTP server

* feat(agent): integrate createManagedDID in HTTP server
  • Loading branch information
patlo-iog authored Oct 25, 2022
1 parent d983019 commit 4a88ded
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@ sealed trait PrismDID {

}

object PrismDID {
// TODO: implement a proper DID parser (ATL-2031)
// For now, just make it work with a simple case of Prism DID V1
def parse(didRef: String): Either[String, PrismDID] = {
if (didRef.startsWith("did:prism:1:")) {
val suffix = didRef.drop("did:prism:1:".length)
suffix.split(':').toList match {
case network :: suffix :: encodedState :: Nil =>
Right(
LongFormPrismDIDV1(
network,
HexString.fromStringUnsafe(suffix),
Base64UrlString.fromStringUnsafe(encodedState)
)
)
case network :: suffix :: Nil => Right(PrismDIDV1(network, HexString.fromStringUnsafe(suffix)))
case _ => Left("Invalid DID syntax")
}
} else {
Left("DID parsing only supports Prism DID with did:prism:1 prefix")
}
}
}

final case class PrismDIDV1 private[did] (network: String, suffix: HexString) extends PrismDID {

override val version: PrismDIDVersion = PrismDIDVersion.V1
Expand Down
26 changes: 11 additions & 15 deletions prism-agent/api/http/castor/schemas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -206,24 +206,22 @@ components:
type: object
required:
- id
- purposes
- purpose
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",
]
purpose:
type: string
enum:
[
"authentication",
"assertionMethod",
"keyAgreement",
"capabilityInvocation",
"capabilityDelegation",
]
example: [ "authentication", "assertionMethod" ]
services:
type: array
Expand All @@ -236,8 +234,6 @@ components:
- did
- longFormDid
properties:
did:
$ref: "#/components/schemas/DID"
longFormDid:
type: string
description: A long-form DID for the created DID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,32 @@ import io.iohk.atala.agent.server.http.marshaller.{
DIDApiMarshallerImpl,
DIDAuthenticationApiMarshallerImpl,
DIDOperationsApiMarshallerImpl,
DIDRegistrarApiMarshallerImpl,
IssueCredentialsApiMarshallerImpl
}
import io.iohk.atala.agent.server.http.service.{
DIDApiServiceImpl,
DIDAuthenticationApiServiceImpl,
DIDOperationsApiServiceImpl,
DIDRegistrarApiServiceImpl,
IssueCredentialsApiServiceImpl
}
import io.iohk.atala.castor.core.repository.DIDOperationRepository
import io.iohk.atala.agent.openapi.api.{DIDApi, DIDAuthenticationApi, DIDOperationsApi, IssueCredentialsApi}
import io.iohk.atala.agent.openapi.api.{
DIDApi,
DIDAuthenticationApi,
DIDOperationsApi,
DIDRegistrarApi,
IssueCredentialsApi
}
import io.iohk.atala.castor.sql.repository.{JdbcDIDOperationRepository, TransactorLayer}
import zio.*
import zio.interop.catz.*
import cats.effect.std.Dispatcher
import com.typesafe.config.ConfigFactory
import io.grpc.ManagedChannelBuilder
import io.iohk.atala.agent.server.config.AppConfig
import io.iohk.atala.agent.walletapi.service.ManagedDIDService
import io.iohk.atala.castor.core.util.DIDOperationValidator
import io.iohk.atala.iris.proto.service.IrisServiceGrpc
import io.iohk.atala.iris.proto.service.IrisServiceGrpc.IrisServiceStub
Expand Down Expand Up @@ -79,8 +88,12 @@ object AppModule {
serviceLimit = 50
)
)

val didServiceLayer: TaskLayer[DIDService] =
(GrpcModule.layers ++ RepoModule.layers ++ didOpValidatorLayer) >>> DIDServiceImpl.layer

val manageDIDServiceLayer: TaskLayer[ManagedDIDService] =
(didOpValidatorLayer ++ didServiceLayer) >>> ManagedDIDService.inMemoryStorage()
}

object GrpcModule {
Expand Down Expand Up @@ -121,13 +134,21 @@ object HttpModule {
(apiServiceLayer ++ apiMarshallerLayer) >>> ZLayer.fromFunction(new DIDAuthenticationApi(_, _))
}

val didRegistrarApiLayer: TaskLayer[DIDRegistrarApi] = {
val serviceLayer = AppModule.manageDIDServiceLayer
val apiServiceLayer = serviceLayer >>> DIDRegistrarApiServiceImpl.layer
val apiMarshallerLayer = DIDRegistrarApiMarshallerImpl.layer
(apiServiceLayer ++ apiMarshallerLayer) >>> ZLayer.fromFunction(new DIDRegistrarApi(_, _))
}

val issueCredentialsApiLayer: ULayer[IssueCredentialsApi] = {
val apiServiceLayer = IssueCredentialsApiServiceImpl.layer
val apiMarshallerLayer = IssueCredentialsApiMarshallerImpl.layer
(apiServiceLayer ++ apiMarshallerLayer) >>> ZLayer.fromFunction(new IssueCredentialsApi(_, _))
}

val layers = didApiLayer ++ didOperationsApiLayer ++ didAuthenticationApiLayer ++ issueCredentialsApiLayer
val layers =
didApiLayer ++ didOperationsApiLayer ++ didAuthenticationApiLayer ++ didRegistrarApiLayer ++ issueCredentialsApiLayer
}

object RepoModule {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@ package io.iohk.atala.agent.server.http
import akka.http.scaladsl.model.ContentType
import akka.http.scaladsl.server.Route
import akka.http.scaladsl.server.Directives.*
import io.iohk.atala.agent.openapi.api.{DIDApi, DIDAuthenticationApi, DIDOperationsApi, IssueCredentialsApi}
import io.iohk.atala.agent.openapi.api.{
DIDApi,
DIDAuthenticationApi,
DIDOperationsApi,
DIDRegistrarApi,
IssueCredentialsApi
}
import zio.*

object HttpRoutes {

def routes: URIO[DIDApi & DIDOperationsApi & DIDAuthenticationApi & IssueCredentialsApi, Route] =
def routes: URIO[DIDApi & DIDOperationsApi & DIDAuthenticationApi & DIDRegistrarApi & IssueCredentialsApi, Route] =
for {
didApi <- ZIO.service[DIDApi]
didOperationsApi <- ZIO.service[DIDOperationsApi]
didAuthApi <- ZIO.service[DIDAuthenticationApi]
disRegistrarApi <- ZIO.service[DIDRegistrarApi]
issueCredentialApi <- ZIO.service[IssueCredentialsApi]
} yield didApi.route ~ didOperationsApi.route ~ didAuthApi.route ~ issueCredentialApi.route ~ additionalRoute
} yield didApi.route ~ didOperationsApi.route ~ didAuthApi.route ~ disRegistrarApi.route ~ issueCredentialApi.route ~ additionalRoute

private def additionalRoute: Route = {
// swagger-ui expects this particular header when resolving relative $ref
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.iohk.atala.agent.server.http.marshaller

import akka.http.scaladsl.marshalling.ToEntityMarshaller
import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller
import io.iohk.atala.agent.openapi.api.DIDRegistrarApiMarshaller
import io.iohk.atala.agent.openapi.model.{
CreateManagedDIDResponse,
CreateManagedDidRequest,
DIDOperationResponse,
ErrorResponse
}
import spray.json.RootJsonFormat
import zio.*

object DIDRegistrarApiMarshallerImpl extends JsonSupport {

val layer: ULayer[DIDRegistrarApiMarshaller] = ZLayer.succeed {
new DIDRegistrarApiMarshaller:
override implicit def fromEntityUnmarshallerCreateManagedDidRequest
: FromEntityUnmarshaller[CreateManagedDidRequest] = summon[RootJsonFormat[CreateManagedDidRequest]]

override implicit def toEntityMarshallerDIDOperationResponse: ToEntityMarshaller[DIDOperationResponse] =
summon[RootJsonFormat[DIDOperationResponse]]

override implicit def toEntityMarshallerCreateManagedDIDResponse: ToEntityMarshaller[CreateManagedDIDResponse] =
summon[RootJsonFormat[CreateManagedDIDResponse]]

override implicit def toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] =
summon[RootJsonFormat[ErrorResponse]]
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,24 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
given RootJsonFormat[CreateAuthenticationChallengeResponse] = jsonFormat2(CreateAuthenticationChallengeResponse.apply)
given RootJsonFormat[CreateDIDRequest] = jsonFormat4(CreateDIDRequest.apply)
given RootJsonFormat[CreateDIDRequestDocument] = jsonFormat2(CreateDIDRequestDocument.apply)
given RootJsonFormat[CreateManagedDidRequest] = jsonFormat1(CreateManagedDidRequest.apply)
given RootJsonFormat[CreateManagedDidRequestDocumentTemplate] = jsonFormat3(
CreateManagedDidRequestDocumentTemplate.apply
)
given RootJsonFormat[CreateManagedDidRequestDocumentTemplatePublicKeysInner] = jsonFormat2(
CreateManagedDidRequestDocumentTemplatePublicKeysInner.apply
)
given RootJsonFormat[CreateManagedDIDResponse] = jsonFormat1(CreateManagedDIDResponse.apply)
given RootJsonFormat[DeactivateDIDRequest] = jsonFormat4(DeactivateDIDRequest.apply)
given RootJsonFormat[Delta] = jsonFormat2(Delta.apply)
given RootJsonFormat[DeltaUpdate] = jsonFormat2(DeltaUpdate.apply)
given RootJsonFormat[DID] = jsonFormat8(DID.apply)
given RootJsonFormat[DidOperation] = jsonFormat4(DidOperation.apply)
given RootJsonFormat[DIDOperationResponse] = jsonFormat1(DIDOperationResponse.apply)
given RootJsonFormat[DidOperationStatus] = jsonFormat0(DidOperationStatus.apply)
given RootJsonFormat[DidOperationSubmission] = jsonFormat2(DidOperationSubmission.apply)
given RootJsonFormat[DidOperationType] = jsonFormat0(DidOperationType.apply)
given RootJsonFormat[DIDResponse] = jsonFormat2(DIDResponse.apply)
given RootJsonFormat[DIDOperationResponse] = jsonFormat1(DIDOperationResponse.apply)
given RootJsonFormat[DidOperationSubmission] = jsonFormat2(DidOperationSubmission.apply)
given RootJsonFormat[ErrorResponse] = jsonFormat5(ErrorResponse.apply)
given RootJsonFormat[JsonWebKey2020] = jsonFormat1(JsonWebKey2020.apply)
given RootJsonFormat[PublicKey] = jsonFormat5(PublicKey.apply)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ package io.iohk.atala.agent.server.http.model

import io.iohk.atala.agent.openapi.model.{
CreateDIDRequest,
CreateManagedDidRequestDocumentTemplate,
CreateManagedDidRequestDocumentTemplatePublicKeysInner,
DIDOperationResponse,
DidOperation,
DidOperationSubmission,
JsonWebKey2020,
PublicKey,
PublicKeyJwk,
Service,
DidOperationSubmission
Service
}
import io.iohk.atala.castor.core.model.did as domain
import io.iohk.atala.castor.core.model.did as castorDomain
import io.iohk.atala.castor.core.model.did.PublishedDIDOperation
import io.iohk.atala.agent.walletapi.model as walletDomain
import io.iohk.atala.shared.models.HexStrings.*
import io.iohk.atala.shared.models.Base64UrlStrings.*
import io.iohk.atala.shared.utils.Traverse.*
Expand All @@ -22,7 +25,7 @@ import scala.util.Try
trait OASDomainModelHelper {

extension (req: CreateDIDRequest) {
def toDomain: Either[String, domain.PublishedDIDOperation.Create] = {
def toDomain: Either[String, castorDomain.PublishedDIDOperation.Create] = {
for {
updateCommitmentHex <- HexString
.fromString(req.updateCommitment)
Expand All @@ -36,25 +39,25 @@ trait OASDomainModelHelper {
.map(_ => "unable to convert recoveryCommitment to hex string")
publicKeys <- req.document.publicKeys.getOrElse(Nil).traverse(_.toDomain)
services <- req.document.services.getOrElse(Nil).traverse(_.toDomain)
} yield domain.PublishedDIDOperation.Create(
} yield castorDomain.PublishedDIDOperation.Create(
updateCommitment = updateCommitmentHex,
recoveryCommitment = recoveryCommitmentHex,
storage = domain.DIDStorage.Cardano(req.storage),
document = domain.DIDDocument(publicKeys = publicKeys, services = services)
storage = castorDomain.DIDStorage.Cardano(req.storage),
document = castorDomain.DIDDocument(publicKeys = publicKeys, services = services)
)
}
}

extension (service: Service) {
def toDomain: Either[String, domain.Service] = {
def toDomain: Either[String, castorDomain.Service] = {
for {
serviceEndpoint <- Try(URI.create(service.serviceEndpoint)).toEither.left.map(_ =>
s"unable to parse serviceEndpoint ${service.serviceEndpoint} as URI"
)
serviceType <- domain.ServiceType
serviceType <- castorDomain.ServiceType
.parseString(service.`type`)
.toRight(s"unsupported serviceType ${service.`type`}")
} yield domain.Service(
} yield castorDomain.Service(
id = service.id,
`type` = serviceType,
serviceEndpoint = serviceEndpoint
Expand All @@ -63,24 +66,24 @@ trait OASDomainModelHelper {
}

extension (key: PublicKey) {
def toDomain: Either[String, domain.PublicKey] = {
def toDomain: Either[String, castorDomain.PublicKey] = {
for {
purposes <- key.purposes.traverse(i =>
domain.VerificationRelationship
castorDomain.VerificationRelationship
.parseString(i)
.toRight(s"unsupported verificationRelationship $i")
)
publicKeyJwk <- key.jsonWebKey2020.publicKeyJwk.toDomain
} yield domain.PublicKey.JsonWebKey2020(id = key.id, purposes = purposes, publicKeyJwk = publicKeyJwk)
} yield castorDomain.PublicKey.JsonWebKey2020(id = key.id, purposes = purposes, publicKeyJwk = publicKeyJwk)
}
}

extension (jwk: PublicKeyJwk) {
def toDomain: Either[String, domain.PublicKeyJwk] = {
def toDomain: Either[String, castorDomain.PublicKeyJwk] = {
for {
crv <- jwk.crv
.toRight("expected crv field in JWK")
.flatMap(i => domain.EllipticCurve.parseString(i).toRight(s"unsupported curve $i"))
.flatMap(i => castorDomain.EllipticCurve.parseString(i).toRight(s"unsupported curve $i"))
x <- jwk.x
.toRight("expected x field in JWK")
.flatMap(
Expand All @@ -91,11 +94,37 @@ trait OASDomainModelHelper {
.flatMap(
Base64UrlString.fromString(_).toEither.left.map(_ => "unable to convert y coordinate to base64url string")
)
} yield domain.PublicKeyJwk.ECPublicKeyData(crv = crv, x = x, y = y)
} yield castorDomain.PublicKeyJwk.ECPublicKeyData(crv = crv, x = x, y = y)
}
}

extension (outcome: domain.PublishedDIDOperationOutcome) {
extension (template: CreateManagedDidRequestDocumentTemplate) {
def toDomain: Either[String, walletDomain.ManagedDIDTemplate] = {
for {
services <- template.services.traverse(_.toDomain)
publicKeys <- template.publicKeys.traverse(_.toDomain)
} yield walletDomain.ManagedDIDTemplate(
storage = template.storage,
publicKeys = publicKeys,
services = services
)
}
}

extension (publicKeyTemplate: CreateManagedDidRequestDocumentTemplatePublicKeysInner) {
def toDomain: Either[String, walletDomain.DIDPublicKeyTemplate] = {
for {
purpose <- castorDomain.VerificationRelationship
.parseString(publicKeyTemplate.purpose)
.toRight(s"unsupported verificationRelationship ${publicKeyTemplate.purpose}")
} yield walletDomain.DIDPublicKeyTemplate(
id = publicKeyTemplate.id,
purpose = purpose
)
}
}

extension (outcome: castorDomain.PublishedDIDOperationOutcome) {
def toOAS: DIDOperationResponse = DIDOperationResponse(
scheduledOperation = DidOperationSubmission(
id = outcome.operationId.toString,
Expand Down
Loading

0 comments on commit 4a88ded

Please sign in to comment.