Skip to content

Commit

Permalink
feat(prism-agent): implement Issue Credential v2 protocol (#146)
Browse files Browse the repository at this point in the history
* feat(pollux): add 'CredentialGenerated' state

* chore(pollux): disable Doobie log handler

* chore(pollux): move DIDComm message creation and storage in credential service

* mend

chore(pollux): move DIDComm message creation and storage in credential service

* chore(prism-agent): inject DidComm layer in credential service

* chore(pollux): generate DidComm IssueCredential message when accepting credential request

* chore(prism-agent): do not create DidComm messages in background job

* chore(prism-agent): wip

* chore(prism-agent): add missing import

* chore(prism-agent): bump pollux dependency version to 0.2.0-SNAPSHOT

* fix(prism-agent): fix pollux DbConfig package change

* fix(pollux): populate record claims in DB when receiving the DIDComm OfferCredential message

* chore(pollux): remove obsolete claims attribute from DB and retrieve them from the offer

* chore(pollux): reinstate 'markCredentialGenerated' method

* feat(prism-agent): return JWT credential based on issue-credential and claims based on offer-credential from REST API calls

* chore(prism-agent): get rid of 'claims' attribute and make sure whole flow is working waiting for publication to be integrated

* chore(pollux): add 'automatic-issuance' flag to support auto request acceptance by issuer

* chore(prism-agent): add 'automaticIssuance' attribute to issue REST API

* chore(prism-agent): bump pollux dependency version to 0.3.0-SNAPSHOT

* chore(pollux): bump version to 0.3.0-SNAPSHOT

* chore(pollux): introduce 'awaitConfirmation' flag in issue credential record

* chore(pollux): introduce 'awaitConfirmation' flag in issue credential protocol REST API

* chore(prism-agent): limit pollux DB transactor connection pool

* feature(pollux): add creationDateTime attribute to issue credential record

* feature(prism-agent): add creationDateTime in issue credential protocol REST API

* feature(prism-agent): add 'updateAt' field to issue credential record

* feat(pollux): Add job that publishes credentials to DLT (#92)

* Remove SNAPSHOT from versions, fix some errirs with scala version mismatch, make local docker compose runnable
* Add getCredentialRecordsByState
* Add function to make credential from issue credential record
* WIP: implement the job
* Add inclusion proof to IssueCredentialRecord
* Add serializer and decserializer for inclusion proofs
* WIP: Add updateCredentialRecordStateAndProofByCredentialIdBulk
* Add updateCredentialRecordStateAndProofByCredentialIdBulk
* Fix merge conflict erros
* Remove MockCredentialService
* Fix errors caused by conflicts
* Edit createCredentialPayloadFromRecord, get credential from requestCredential didcome message
* Revert remove "extractIdFromCredential"
* Make it compile
* Update mercyry to 0.6.0
* Finalize protocol
* remove confusing response texts
* Format
* format prism agent
* add my branch to releases
* remove my branch from release

Co-authored-by: FabioPinheiro <[email protected]>

* feat(prism-agent): return JWT credential from API call

* chore(prism-agent): use release pollux lib 0.3.0

* chore(prism-agent): run 'scalafmtAll'

Co-authored-by: shota jolbordi <[email protected]>
Co-authored-by: FabioPinheiro <[email protected]>
  • Loading branch information
3 people authored Nov 21, 2022
1 parent 96b0fbc commit f3cb60e
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 106 deletions.
52 changes: 42 additions & 10 deletions prism-agent/api/http/pollux/schemas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ components:
description: Claims that will be associated with given verifiable credentials
additionalProperties:
type: string
automaticIssuance:
type: boolean
default: true
awaitConfirmation:
type: boolean
default: true

IssueCredentialRecord:
description: An issue credential record to store the state of the protocol execution
Expand All @@ -177,21 +183,47 @@ components:
- type: object
required:
- recordId
- state
- createdAt
- role
- protocolState
properties:
recordId:
type: string
format: uuid
state:
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
role:
type: string
enum:
- Issuer
- Holder
protocolState:
type: string
enum:
- OfferPending
- OfferSent
- OfferReceived
- RequestPending
- RequestSent
- RequestReceived
- ProblemReportPending
- ProblemReportSent
- ProblemReportReceived
- CredentialPending
- CredentialSent
- CredentialReceived
publicationState:
type: string
enum:
- PublicationPending
- PublicationQueued
- Published
jwtCredential:
type: string
enum:
- offer-sent
- offer-received
- request-sent
- request-received
- credential-issued
- credential-received
- done

IssueCredentialRecordCollection:
description: A collection of issue credential records
Expand Down
4 changes: 2 additions & 2 deletions prism-agent/service/project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ object Dependencies {
val akka = "2.6.20"
val akkaHttp = "10.2.9"
val castor = "0.2.0"
val pollux = "0.2.0"
val pollux = "0.3.0"
val bouncyCastle = "1.70"
val logback = "1.4.4"
val mercury = "0.5.0"
val mercury = "0.6.0"
}

private lazy val zio = "dev.zio" %% "zio" % Versions.zio
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package io.iohk.atala.agent.server

import zio.*
import io.iohk.atala.mercury.PeerDID
import io.iohk.atala.mercury.AgentCli
import io.iohk.atala.mercury._
import org.didcommx.didcomm.DIDComm
import io.iohk.atala.resolvers.UniversalDidResolver
import io.iohk.atala.castor.sql.repository.{Migrations => CastorMigrations}
import io.iohk.atala.pollux.sql.repository.{Migrations => PolluxMigrations}

object Main extends ZIOAppDefault {
def agentLayer(peer: PeerDID): ZLayer[Any, Nothing, AgentServiceAny] = ZLayer.succeed(
io.iohk.atala.mercury.AgentServiceAny(
new DIDComm(UniversalDidResolver, peer.getSecretResolverInMemory),
peer.did
)
)

override def run: ZIO[Any, Throwable, Unit] =
for {
_ <- Console
Expand Down Expand Up @@ -54,25 +62,20 @@ object Main extends ZIOAppDefault {
ZIO.logInfo(s"JWK for KeyAuthentication: ${peer.jwkForKeyAuthentication.toJSONString}")
} yield (peer)

didCommLayer = AgentCli.agentLayer(agentDID)
didCommLayer = agentLayer(agentDID)

didCommExchangesFiber <- Modules.didCommExchangesJob
.provide(
didCommLayer
)
.provide(didCommLayer)
.debug
.fork

didCommServiceFiber <- Modules
.didCommServiceEndpoint(didCommServicePort)
.provide(
didCommLayer,
AppModule.credentialServiceLayer
)
.provide(didCommLayer, AppModule.credentialServiceLayer)
.debug
.fork

_ <- Modules.app(restServicePort)
_ <- Modules.app(restServicePort).provide(didCommLayer)
} yield ()

}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ import zhttp.service.Server
import java.util.concurrent.Executors

import io.iohk.atala.mercury._
import io.iohk.atala.mercury.AgentCli.sendMessage //TODO REMOVE
import io.iohk.atala.mercury.model._
import io.iohk.atala.mercury.model.error._
import io.iohk.atala.mercury.protocol.issuecredential._
Expand All @@ -77,7 +76,7 @@ import java.io.IOException

object Modules {

def app(port: Int): Task[Unit] = {
def app(port: Int): RIO[DidComm, Unit] = {
val httpServerApp = HttpRoutes.routes.flatMap(HttpServer.start(port, _))

httpServerApp
Expand Down Expand Up @@ -109,7 +108,7 @@ object Modules {
ZIO.fail(error)
}
}
.map(str => Response.text(str))
.map(str => Response.ok)

}
Server.start(port, app)
Expand All @@ -123,7 +122,7 @@ object Modules {

def webServerProgram(
jsonString: String
): ZIO[DidComm with CredentialService, MercuryThrowable, String] = {
): ZIO[DidComm with CredentialService, MercuryThrowable, Unit] = {
import io.iohk.atala.mercury.DidComm.*
ZIO.logAnnotate("request-id", java.util.UUID.randomUUID.toString()) {
for {
Expand All @@ -143,7 +142,7 @@ object Modules {
credentialService <- ZIO.service[CredentialService]

// TODO
} yield ("OfferCredential Sent")
} yield ()

case s if s == OfferCredential.`type` => // Holder
for {
Expand All @@ -160,7 +159,7 @@ object Modules {
}
.catchAll { case ex: IOException => ZIO.fail(ex) }

} yield ("Offer received")
} yield ()

case s if s == RequestCredential.`type` => // Issuer
for {
Expand All @@ -178,7 +177,7 @@ object Modules {
.catchAll { case ex: IOException => ZIO.fail(ex) }

// TODO todoTestOption if none
} yield ("RequestCredential received")
} yield ()

case s if s == IssueCredential.`type` => // Holder
for {
Expand All @@ -194,8 +193,7 @@ object Modules {
ZIO.fail(cause)
}
.catchAll { case ex: IOException => ZIO.fail(ex) }

} yield ("IssueCredential Received")
} yield ()

case _ => ZIO.succeed("Unknown Message Type")
}
Expand All @@ -204,6 +202,12 @@ object Modules {
}
}

val publishCredentialsToDltJob: RIO[DidComm, Unit] = {
val effect = BackgroundJobs.publishCredentialsToDlt
.provideLayer(AppModule.credentialServiceLayer)
(effect repeat Schedule.spaced(1.seconds)).unit
}

}

object SystemModule {
Expand Down Expand Up @@ -242,7 +246,7 @@ object AppModule {
val manageDIDServiceLayer: TaskLayer[ManagedDIDService] =
(didOpValidatorLayer ++ didServiceLayer) >>> ManagedDIDService.inMemoryStorage()

val credentialServiceLayer: TaskLayer[CredentialService] =
val credentialServiceLayer: RLayer[DidComm, CredentialService] =
(GrpcModule.layers ++ RepoModule.layers) >>> CredentialServiceImpl.layer
}

Expand Down Expand Up @@ -291,14 +295,14 @@ object HttpModule {
(apiServiceLayer ++ apiMarshallerLayer) >>> ZLayer.fromFunction(new DIDRegistrarApi(_, _))
}

val issueCredentialsApiLayer: TaskLayer[IssueCredentialsApi] = {
val issueCredentialsApiLayer: RLayer[DidComm, IssueCredentialsApi] = {
val serviceLayer = AppModule.credentialServiceLayer
val apiServiceLayer = serviceLayer >>> IssueCredentialsApiServiceImpl.layer
val apiMarshallerLayer = IssueCredentialsApiMarshallerImpl.layer
(apiServiceLayer ++ apiMarshallerLayer) >>> ZLayer.fromFunction(new IssueCredentialsApi(_, _))
}

val issueCredentialsProtocolApiLayer: TaskLayer[IssueCredentialsProtocolApi] = {
val issueCredentialsProtocolApiLayer: RLayer[DidComm, IssueCredentialsProtocolApi] = {
val serviceLayer = AppModule.credentialServiceLayer
val apiServiceLayer = serviceLayer >>> IssueCredentialsProtocolApiServiceImpl.layer
val apiMarshallerLayer = IssueCredentialsProtocolApiMarshallerImpl.layer
Expand Down Expand Up @@ -342,7 +346,8 @@ object RepoModule {
PolluxDbConfig(
username = config.username,
password = config.password,
jdbcUrl = s"jdbc:postgresql://${config.host}:${config.port}/${config.databaseName}"
jdbcUrl = s"jdbc:postgresql://${config.host}:${config.port}/${config.databaseName}",
awaitConnectionThreads = 2
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import java.util.UUID
import spray.json.JsString
import spray.json.JsValue
import spray.json.DeserializationException
import java.time.OffsetDateTime

trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {

Expand Down Expand Up @@ -61,8 +62,17 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
}
}
}
given RootJsonFormat[CreateIssueCredentialRecordRequest] = jsonFormat4(CreateIssueCredentialRecordRequest.apply)
given RootJsonFormat[IssueCredentialRecord] = jsonFormat6(IssueCredentialRecord.apply)
implicit object OffsetDateTimeFormat extends JsonFormat[OffsetDateTime] {
def write(dt: OffsetDateTime) = JsString(dt.toString)
def read(value: JsValue) = {
value match {
case JsString(dt) => OffsetDateTime.parse(dt)
case _ => throw new DeserializationException("Expected hexadecimal OffsetDateTime string")
}
}
}
given RootJsonFormat[CreateIssueCredentialRecordRequest] = jsonFormat6(CreateIssueCredentialRecordRequest.apply)
given RootJsonFormat[IssueCredentialRecord] = jsonFormat13(IssueCredentialRecord.apply)
given RootJsonFormat[IssueCredentialRecordCollection] = jsonFormat4(IssueCredentialRecordCollection.apply)
//

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import java.net.URI
import scala.util.Try
import io.iohk.atala.agent.openapi.model.IssueCredentialRecord
import io.iohk.atala.agent.openapi.model.IssueCredentialRecordCollection
import java.time.OffsetDateTime
import java.time.ZoneOffset
import io.iohk.atala.mercury.model.AttachmentDescriptor
import io.iohk.atala.mercury.model.Base64

trait OASDomainModelHelper {

Expand Down Expand Up @@ -139,11 +143,24 @@ trait OASDomainModelHelper {
extension (domain: polluxdomain.IssueCredentialRecord) {
def toOAS: IssueCredentialRecord = IssueCredentialRecord(
recordId = domain.id,
createdAt = domain.createdAt.atOffset(ZoneOffset.UTC),
updatedAt = domain.updatedAt.map(_.atOffset(ZoneOffset.UTC)),
role = domain.role.toString,
subjectId = domain.subjectId,
claims = domain.claims,
claims = domain.offerCredentialData
.map(offer => offer.body.credential_preview.attributes.map(attr => (attr.name -> attr.value)).toMap)
.getOrElse(Map.empty),
schemaId = domain.schemaId,
validityPeriod = domain.validityPeriod,
state = domain.protocolState.toString()
automaticIssuance = domain.automaticIssuance,
awaitConfirmation = domain.awaitConfirmation,
protocolState = domain.protocolState.toString(),
publicationState = domain.publicationState.map(_.toString),
jwtCredential = domain.issueCredentialData.flatMap(issueCredential => {
issueCredential.attachments.collectFirst { case AttachmentDescriptor(_, _, Base64(jwt), _, _, _, _) =>
jwt
}
})
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ class IssueCredentialsProtocolApiServiceImpl(credentialService: CredentialServic
): Route = {
val result = for {
outcome <- credentialService
.createCredentialOffer(
.createIssueCredentialRecord(
thid = UUID.randomUUID(),
request.subjectId,
request.schemaId,
request.claims,
request.validityPeriod
request.validityPeriod,
request.automaticIssuance.orElse(Some(true)),
request.awaitConfirmation.orElse(Some(false))
)
.mapError(HttpServiceError.DomainError[IssueCredentialError].apply)
} yield outcome
Expand All @@ -49,7 +51,7 @@ class IssueCredentialsProtocolApiServiceImpl(credentialService: CredentialServic
): Route = {
val result = for {
outcome <- credentialService
.getCredentialRecords()
.getIssueCredentialRecords()
.mapError(HttpServiceError.DomainError[IssueCredentialError].apply)
} yield outcome

Expand Down Expand Up @@ -81,7 +83,7 @@ class IssueCredentialsProtocolApiServiceImpl(credentialService: CredentialServic
val result = for {
uuid <- recordId.toUUID
outcome <- credentialService
.getCredentialRecord(uuid)
.getIssueCredentialRecord(uuid)
.mapError(HttpServiceError.DomainError[IssueCredentialError].apply)
} yield outcome

Expand Down Expand Up @@ -117,7 +119,7 @@ class IssueCredentialsProtocolApiServiceImpl(credentialService: CredentialServic
val result = for {
uuid <- recordId.toUUID
outcome <- credentialService
.issueCredential(uuid)
.acceptCredentialRequest(uuid)
.mapError(HttpServiceError.DomainError[IssueCredentialError].apply)
} yield outcome

Expand Down
Loading

0 comments on commit f3cb60e

Please sign in to comment.