From 4edcb0a89d59a49f6ce36d51d99a7a9ce1f3203c Mon Sep 17 00:00:00 2001 From: Bassam Date: Tue, 31 Oct 2023 08:30:03 -0400 Subject: [PATCH] feat: Create/Store presentation proof request (#774) Signed-off-by: Bassam Riman --- .../core/model/error/PresentationError.scala | 2 + .../model/schema/validator/SchemaSerDes.scala | 8 +- .../service/MockPresentationService.scala | 33 +++- .../core/service/PresentationService.scala | 12 +- .../service/PresentationServiceImpl.scala | 78 +++++++-- .../service/PresentationServiceNotifier.scala | 29 +++- .../AnoncredPresentationRequestV1.scala | 158 ++++++++++++++++++ .../PresentationServiceNotifierSpec.scala | 7 +- .../service/PresentationServiceSpec.scala | 118 +++++++++---- .../PresentationServiceSpecHelper.scala | 17 +- .../controller/PresentProofController.scala | 2 + .../PresentProofControllerImpl.scala | 51 ++++-- .../http/RequestPresentationInput.scala | 20 ++- 13 files changed, 444 insertions(+), 91 deletions(-) create mode 100644 pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredPresentationRequestV1.scala diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/error/PresentationError.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/error/PresentationError.scala index 48896a7409..7ecf89525c 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/error/PresentationError.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/error/PresentationError.scala @@ -17,4 +17,6 @@ object PresentationError { object MissingCredential extends PresentationError object MissingCredentialFormat extends PresentationError final case class UnsupportedCredentialFormat(vcFormat: String) extends PresentationError + + final case class MissingAnoncredPresentationRequest(error: String) extends PresentationError } diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/validator/SchemaSerDes.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/validator/SchemaSerDes.scala index bec8fc2e06..82fba4989f 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/validator/SchemaSerDes.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/validator/SchemaSerDes.scala @@ -2,18 +2,20 @@ package io.iohk.atala.pollux.core.model.schema.validator import com.networknt.schema.JsonSchema import io.iohk.atala.pollux.core.model.schema.validator.JsonSchemaError.* -import zio.IO -import zio.ZIO import zio.json.* -import zio.json.JsonDecoder import zio.json.ast.Json import zio.json.ast.Json.* +import zio.{IO, ZIO} class SchemaSerDes[S](jsonSchemaSchemaStr: String) { def initialiseJsonSchema: IO[JsonSchemaError, JsonSchema] = JsonSchemaUtils.jsonSchema(jsonSchemaSchemaStr) + def serialize(instance: S)(using encoder: JsonEncoder[S]): String = { + instance.toJson + } + def deserialize( schema: zio.json.ast.Json )(using decoder: JsonDecoder[S]): IO[JsonSchemaError, S] = { diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockPresentationService.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockPresentationService.scala index 8376f80d90..639bcc4a4e 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockPresentationService.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockPresentationService.scala @@ -5,23 +5,31 @@ import io.iohk.atala.mercury.protocol.presentproof.{Presentation, ProofType, Pro import io.iohk.atala.pollux.core.model.error.PresentationError import io.iohk.atala.pollux.core.model.presentation.Options import io.iohk.atala.pollux.core.model.{DidCommID, PresentationRecord} +import io.iohk.atala.pollux.core.service.serdes.AnoncredPresentationRequestV1 import io.iohk.atala.pollux.vc.jwt.{Issuer, PresentationPayload, W3cCredentialPayload} +import io.iohk.atala.shared.models.WalletAccessContext import zio.mock.{Mock, Proxy} import zio.{IO, URLayer, ZIO, ZLayer, mock} import java.time.Instant import java.util.UUID -import io.iohk.atala.pollux.core.model.CredentialFormat object MockPresentationService extends Mock[PresentationService] { - object CreatePresentationRecord + object CreateJwtPresentationRecord extends Effect[ (DidId, DidId, DidCommID, Option[String], Seq[ProofType], Option[Options]), PresentationError, PresentationRecord ] + object CreateAnoncredPresentationRecord + extends Effect[ + (DidId, DidId, DidCommID, Option[String], AnoncredPresentationRequestV1), + PresentationError, + PresentationRecord + ] + object MarkRequestPresentationSent extends Effect[DidCommID, PresentationError, PresentationRecord] object ReceivePresentation extends Effect[Presentation, PresentationError, PresentationRecord] @@ -54,20 +62,32 @@ object MockPresentationService extends Mock[PresentationService] { proxy <- ZIO.service[Proxy] } yield new PresentationService { - override def createPresentationRecord( + override def createJwtPresentationRecord( pairwiseVerifierDID: DidId, pairwiseProverDID: DidId, thid: DidCommID, connectionId: Option[String], proofTypes: Seq[ProofType], - options: Option[Options], - format: CredentialFormat, + options: Option[Options] ): IO[PresentationError, PresentationRecord] = proxy( - CreatePresentationRecord, + CreateJwtPresentationRecord, (pairwiseVerifierDID, pairwiseProverDID, thid, connectionId, proofTypes, options) ) + override def createAnoncredPresentationRecord( + pairwiseVerifierDID: DidId, + pairwiseProverDID: DidId, + thid: DidCommID, + connectionId: Option[String], + presentationRequest: AnoncredPresentationRequestV1 + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { + proxy( + CreateAnoncredPresentationRecord, + (pairwiseVerifierDID, pairwiseProverDID, thid, connectionId, presentationRequest) + ) + } + override def acceptRequestPresentation( recordId: DidCommID, credentialsToUse: Seq[String] @@ -158,6 +178,7 @@ object MockPresentationService extends Mock[PresentationService] { recordId: DidCommID, failReason: Option[String] ): IO[PresentationError, Unit] = ??? + } } diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationService.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationService.scala index 5b314426bd..911697351a 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationService.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationService.scala @@ -5,6 +5,7 @@ import io.iohk.atala.mercury.protocol.presentproof.* import io.iohk.atala.pollux.core.model.* import io.iohk.atala.pollux.core.model.error.PresentationError import io.iohk.atala.pollux.core.model.presentation.* +import io.iohk.atala.pollux.core.service.serdes.AnoncredPresentationRequestV1 import io.iohk.atala.pollux.vc.jwt.* import io.iohk.atala.shared.models.WalletAccessContext import zio.* @@ -17,14 +18,21 @@ trait PresentationService { def extractIdFromCredential(credential: W3cCredentialPayload): Option[UUID] - def createPresentationRecord( + def createJwtPresentationRecord( pairwiseVerifierDID: DidId, pairwiseProverDID: DidId, thid: DidCommID, connectionId: Option[String], proofTypes: Seq[ProofType], options: Option[io.iohk.atala.pollux.core.model.presentation.Options], - format: CredentialFormat, + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] + + def createAnoncredPresentationRecord( + pairwiseVerifierDID: DidId, + pairwiseProverDID: DidId, + thid: DidCommID, + connectionId: Option[String], + presentationRequest: AnoncredPresentationRequestV1 ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] def getPresentationRecords( diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceImpl.scala index 8022b26ea9..12678eb92d 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceImpl.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceImpl.scala @@ -12,6 +12,7 @@ import io.iohk.atala.pollux.core.model.error.PresentationError import io.iohk.atala.pollux.core.model.error.PresentationError.* import io.iohk.atala.pollux.core.model.presentation.* import io.iohk.atala.pollux.core.repository.{CredentialRepository, PresentationRepository} +import io.iohk.atala.pollux.core.service.serdes.AnoncredPresentationRequestV1 import io.iohk.atala.pollux.vc.jwt.* import io.iohk.atala.shared.models.WalletAccessContext import io.iohk.atala.shared.utils.aspects.CustomMetricsAspect @@ -141,14 +142,13 @@ private class PresentationServiceImpl( markPresentationRejected(recordId) } - override def createPresentationRecord( + override def createJwtPresentationRecord( pairwiseVerifierDID: DidId, pairwiseProverDID: DidId, thid: DidCommID, connectionId: Option[String], proofTypes: Seq[ProofType], - maybeOptions: Option[io.iohk.atala.pollux.core.model.presentation.Options], - format: CredentialFormat, + maybeOptions: Option[io.iohk.atala.pollux.core.model.presentation.Options] ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { for { request <- ZIO.succeed( @@ -157,13 +157,7 @@ private class PresentationServiceImpl( thid, pairwiseVerifierDID, pairwiseProverDID, - format match { - case CredentialFormat.JWT => maybeOptions.map(options => Seq(toJWTAttachment(options))).getOrElse(Seq.empty) - case CredentialFormat.AnonCreds => - maybeOptions - .map(options => Seq(toAnoncredAttachment(options))) - .getOrElse(Seq.empty) // TODO ATL-5945 Create Actual Anoncred Request - } + maybeOptions.map(options => Seq(toJWTAttachment(options))).getOrElse(Seq.empty) ) ) record <- ZIO.succeed( @@ -177,7 +171,57 @@ private class PresentationServiceImpl( role = PresentationRecord.Role.Verifier, subjectId = pairwiseProverDID, protocolState = PresentationRecord.ProtocolState.RequestPending, - credentialFormat = format, + credentialFormat = CredentialFormat.JWT, + requestPresentationData = Some(request), + proposePresentationData = None, + presentationData = None, + credentialsToUse = None, + metaRetries = maxRetries, + metaNextRetry = Some(Instant.now()), + metaLastFailure = None, + ) + ) + count <- presentationRepository + .createPresentationRecord(record) + .flatMap { + case 1 => ZIO.succeed(()) + case n => ZIO.fail(UnexpectedException(s"Invalid row count result: $n")) + } + .mapError(RepositoryError.apply) @@ CustomMetricsAspect.startRecordingTime( + s"${record.id}_present_proof_flow_verifier_req_pending_to_sent_ms_gauge" + ) + } yield record + } + + override def createAnoncredPresentationRecord( + pairwiseVerifierDID: DidId, + pairwiseProverDID: DidId, + thid: DidCommID, + connectionId: Option[String], + presentationRequest: AnoncredPresentationRequestV1 + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { + for { + request <- ZIO.succeed( + createDidCommRequestPresentation( + Seq.empty, + thid, + pairwiseVerifierDID, + pairwiseProverDID, + Seq(toAnoncredAttachment(presentationRequest)) + ) + ) + record <- ZIO.succeed( + PresentationRecord( + id = DidCommID(), + createdAt = Instant.now, + updatedAt = None, + thid = thid, + connectionId = connectionId, + schemaId = None, // TODO REMOVE from DB + role = PresentationRecord.Role.Verifier, + subjectId = pairwiseProverDID, + protocolState = PresentationRecord.ProtocolState.RequestPending, + credentialFormat = CredentialFormat.AnonCreds, requestPresentationData = Some(request), proposePresentationData = None, presentationData = None, @@ -621,11 +665,13 @@ private class PresentationServiceImpl( ) } - // TODO ATL-5945 Create Actual Anoncred Request - private[this] def toAnoncredAttachment(options: Options): AttachmentDescriptor = { - AttachmentDescriptor.buildJsonAttachment( - payload = PresentationAttachment.build(Some(options)), - format = Some(PresentCredentialRequestFormat.Anoncred.name) + private[this] def toAnoncredAttachment( + presentationRequest: AnoncredPresentationRequestV1 + ): AttachmentDescriptor = { + AttachmentDescriptor.buildBase64Attachment( + mediaType = Some("application/json"), + format = Some(PresentCredentialRequestFormat.Anoncred.name), + payload = AnoncredPresentationRequestV1.schemaSerDes.serialize(presentationRequest).getBytes() ) } diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala index 287b1b2ae5..8b11864fa4 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifier.scala @@ -6,13 +6,13 @@ import io.iohk.atala.mercury.protocol.presentproof.{Presentation, ProofType, Pro import io.iohk.atala.pollux.core.model.error.PresentationError import io.iohk.atala.pollux.core.model.presentation.Options import io.iohk.atala.pollux.core.model.{DidCommID, PresentationRecord} +import io.iohk.atala.pollux.core.service.serdes.AnoncredPresentationRequestV1 import io.iohk.atala.pollux.vc.jwt.{Issuer, PresentationPayload, W3cCredentialPayload} import io.iohk.atala.shared.models.WalletAccessContext -import zio.{URLayer, ZIO, ZLayer, IO} +import zio.{IO, URLayer, ZIO, ZLayer} import java.time.Instant import java.util.UUID -import io.iohk.atala.pollux.core.model.CredentialFormat class PresentationServiceNotifier( svc: PresentationService, @@ -21,24 +21,39 @@ class PresentationServiceNotifier( private val presentationUpdatedEvent = "PresentationUpdated" - override def createPresentationRecord( + override def createJwtPresentationRecord( pairwiseVerifierDID: DidId, pairwiseProverDID: DidId, thid: DidCommID, connectionId: Option[String], proofTypes: Seq[ProofType], options: Option[Options], - format: CredentialFormat, ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = notifyOnSuccess( - svc.createPresentationRecord( + svc.createJwtPresentationRecord( pairwiseVerifierDID, pairwiseProverDID, thid, connectionId, proofTypes, - options, - format: CredentialFormat + options + ) + ) + + def createAnoncredPresentationRecord( + pairwiseVerifierDID: DidId, + pairwiseProverDID: DidId, + thid: DidCommID, + connectionId: Option[String], + presentationRequest: AnoncredPresentationRequestV1 + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = + notifyOnSuccess( + svc.createAnoncredPresentationRecord( + pairwiseVerifierDID, + pairwiseProverDID, + thid, + connectionId, + presentationRequest ) ) diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredPresentationRequestV1.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredPresentationRequestV1.scala new file mode 100644 index 0000000000..0cc8ed1e86 --- /dev/null +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredPresentationRequestV1.scala @@ -0,0 +1,158 @@ +package io.iohk.atala.pollux.core.service.serdes + +import io.iohk.atala.pollux.core.model.schema.validator.SchemaSerDes +import zio.* +import zio.json.* + +case class AnoncredPresentationRequestV1( + requested_attributes: Map[String, AnoncredRequestedAttributeV1], + requested_predicates: Map[String, AnoncredRequestedPredicateV1], + name: String, + nonce: String, + version: String, + non_revoked: Option[AnoncredNonRevokedIntervalV1] +) + +case class AnoncredRequestedAttributeV1(name: String, restrictions: List[AnoncredAttributeRestrictionV1]) + +case class AnoncredRequestedPredicateV1( + name: String, + p_type: String, + p_value: Int, + restrictions: List[AnoncredPredicateRestrictionV1] +) + +case class AnoncredAttributeRestrictionV1( + schema_id: Option[String], + cred_def_id: Option[String], + non_revoked: Option[AnoncredNonRevokedIntervalV1] +) + +case class AnoncredPredicateRestrictionV1( + schema_id: Option[String], + cred_def_id: Option[String], + non_revoked: Option[AnoncredNonRevokedIntervalV1] +) + +case class AnoncredNonRevokedIntervalV1(from: Option[Int], to: Option[Int]) + +object AnoncredPresentationRequestV1 { + val version: String = "PresentationRequestV1" + + private val schema: String = + """ + |{ + | "$schema": "http://json-schema.org/draft-07/schema#", + | "type": "object", + | "properties": { + | "requested_attributes": { + | "type": "object", + | "additionalProperties": { + | "type": "object", + | "properties": { + | "name": { "type": "string" }, + | "restrictions": { + | "type": "array", + | "items": { + | "type": "object", + | "properties": { + | "schema_id": { "type": "string" }, + | "cred_def_id": { "type": "string" }, + | "non_revoked": { + | "type": "object", + | "properties": { + | "from": { "type": "integer" }, + | "to": { "type": "integer" } + | } + | } + | } + | } + | } + | }, + | "required": ["name", "restrictions"] + | } + | }, + | "requested_predicates": { + | "type": "object", + | "additionalProperties": { + | "type": "object", + | "properties": { + | "name": { "type": "string" }, + | "p_type": { "type": "string" }, + | "p_value": { "type": "integer" }, + | "restrictions": { + | "type": "array", + | "items": { + | "type": "object", + | "properties": { + | "schema_id": { "type": "string" }, + | "cred_def_id": { "type": "string" }, + | "non_revoked": { + | "type": "object", + | "properties": { + | "from": { "type": "integer" }, + | "to": { "type": "integer" } + | } + | } + | } + | } + | } + | }, + | "required": ["name", "p_type", "p_value", "restrictions"] + | } + | }, + | "name": { "type": "string" }, + | "nonce": { "type": "string" }, + | "version": { "type": "string" }, + | "non_revoked": { + | "type": "object", + | "properties": { + | "from": { "type": "integer" }, + | "to": { "type": "integer" } + | } + | } + | }, + | "required": ["requested_attributes", "requested_predicates", "name", "nonce", "version" ] + |} + | + |""".stripMargin + + val schemaSerDes: SchemaSerDes[AnoncredPresentationRequestV1] = SchemaSerDes(schema) + + given JsonDecoder[AnoncredRequestedAttributeV1] = + DeriveJsonDecoder.gen[AnoncredRequestedAttributeV1] + + given JsonEncoder[AnoncredRequestedAttributeV1] = + DeriveJsonEncoder.gen[AnoncredRequestedAttributeV1] + + given JsonDecoder[AnoncredRequestedPredicateV1] = + DeriveJsonDecoder.gen[AnoncredRequestedPredicateV1] + + given JsonEncoder[AnoncredRequestedPredicateV1] = + DeriveJsonEncoder.gen[AnoncredRequestedPredicateV1] + + given JsonDecoder[AnoncredAttributeRestrictionV1] = + DeriveJsonDecoder.gen[AnoncredAttributeRestrictionV1] + + given JsonEncoder[AnoncredNonRevokedIntervalV1] = + DeriveJsonEncoder.gen[AnoncredNonRevokedIntervalV1] + + given JsonDecoder[AnoncredNonRevokedIntervalV1] = + DeriveJsonDecoder.gen[AnoncredNonRevokedIntervalV1] + + given JsonEncoder[AnoncredAttributeRestrictionV1] = + DeriveJsonEncoder.gen[AnoncredAttributeRestrictionV1] + + given JsonDecoder[AnoncredPredicateRestrictionV1] = + DeriveJsonDecoder.gen[AnoncredPredicateRestrictionV1] + + given JsonEncoder[AnoncredPredicateRestrictionV1] = + DeriveJsonEncoder.gen[AnoncredPredicateRestrictionV1] + + given JsonDecoder[AnoncredPresentationRequestV1] = + DeriveJsonDecoder.gen[AnoncredPresentationRequestV1] + + given JsonEncoder[AnoncredPresentationRequestV1] = + DeriveJsonEncoder.gen[AnoncredPresentationRequestV1] + +} diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifierSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifierSpec.scala index 06f5751e11..c65a1d512c 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifierSpec.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceNotifierSpec.scala @@ -36,7 +36,7 @@ object PresentationServiceNotifierSpec extends ZIOSpecDefault with PresentationS ) private val verifierHappyFlowExpectations = - MockPresentationService.CreatePresentationRecord( + MockPresentationService.CreateJwtPresentationRecord( assertion = Assertion.anything, result = Expectation.value(record) ) ++ @@ -100,14 +100,13 @@ object PresentationServiceNotifierSpec extends ZIOSpecDefault with PresentationS svc <- ZIO.service[PresentationService] ens <- ZIO.service[EventNotificationService] - record <- svc.createPresentationRecord( + record <- svc.createJwtPresentationRecord( DidId(""), DidId(""), DidCommID(""), None, Seq.empty, - None, - format = CredentialFormat.JWT, + None ) _ <- svc.markRequestPresentationSent(record.id) _ <- svc.receivePresentation(presentation(record.thid.value)) diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala index 94135bc62d..96d2f1ea1d 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpec.scala @@ -2,7 +2,7 @@ package io.iohk.atala.pollux.core.service import io.circe.parser.decode import io.circe.syntax.* -import io.iohk.atala.mercury.model.{AttachmentDescriptor, DidId} +import io.iohk.atala.mercury.model.{AttachmentDescriptor, Base64, DidId} import io.iohk.atala.mercury.protocol.issuecredential.IssueCredential import io.iohk.atala.mercury.protocol.presentproof.* import io.iohk.atala.pollux.core.model.* @@ -12,14 +12,15 @@ import io.iohk.atala.pollux.core.model.error.PresentationError import io.iohk.atala.pollux.core.model.error.PresentationError.* import io.iohk.atala.pollux.core.model.presentation.Options import io.iohk.atala.pollux.core.repository.{CredentialRepository, PresentationRepository} +import io.iohk.atala.pollux.core.service.serdes.AnoncredPresentationRequestV1 import io.iohk.atala.pollux.vc.jwt.* +import io.iohk.atala.shared.models.{WalletAccessContext, WalletId} import zio.* import zio.test.* import zio.test.Assertion.* import java.time.Instant -import io.iohk.atala.shared.models.WalletId -import io.iohk.atala.shared.models.WalletAccessContext +import java.util.Base64 as JBase64 object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSpecHelper { @@ -28,7 +29,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp private val singleWalletSpec = suite("singleWalletSpec")( - test("createPresentationRecord creates a valid PresentationRecord") { + test("createPresentationRecord creates a valid JWT PresentationRecord") { val didGen = for { suffix <- Gen.stringN(10)(Gen.alphaNumericChar) } yield DidId("did:peer:" + suffix) @@ -54,14 +55,13 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp svc <- ZIO.service[PresentationService] pairwiseVerifierDid = DidId("did:peer:Verifier") pairwiseProverDid = DidId("did:peer:Prover") - record <- svc.createPresentationRecord( + record <- svc.createJwtPresentationRecord( pairwiseVerifierDid, pairwiseProverDid, thid, connectionId, proofTypes, - options, - format = CredentialFormat.JWT, + options ) } yield { assertTrue(record.thid == thid) && @@ -100,12 +100,74 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp } } }, + test("createPresentationRecord creates a valid Anoncred PresentationRecord") { + check( + Gen.uuid.map(e => DidCommID(e.toString())), + Gen.option(Gen.string), + Gen.string, + Gen.string, + Gen.string + ) { (thid, connectionId, name, nonce, version) => + for { + svc <- ZIO.service[PresentationService] + pairwiseVerifierDid = DidId("did:peer:Verifier") + pairwiseProverDid = DidId("did:peer:Prover") + anoncredPresentationRequestV1 = AnoncredPresentationRequestV1( + Map.empty, + Map.empty, + name, + nonce, + version, + None + ) + record <- + svc.createAnoncredPresentationRecord( + pairwiseVerifierDid, + pairwiseProverDid, + thid, + connectionId, + anoncredPresentationRequestV1 + ) + } yield { + assertTrue(record.thid == thid) && + assertTrue(record.updatedAt.isEmpty) && + assertTrue(record.connectionId == connectionId) && + assertTrue(record.role == PresentationRecord.Role.Verifier) && + assertTrue(record.protocolState == PresentationRecord.ProtocolState.RequestPending) && + assertTrue(record.requestPresentationData.isDefined) && + assertTrue(record.requestPresentationData.get.to == pairwiseProverDid) && + assertTrue(record.requestPresentationData.get.thid.contains(thid.toString)) && + assertTrue(record.requestPresentationData.get.body.goal_code.contains("Request Proof Presentation")) && + assertTrue( + record.requestPresentationData.get.attachments.map(_.media_type) == Seq(Some("application/json")) + ) && + assertTrue( + record.requestPresentationData.get.attachments.map(_.format) == Seq( + Some(PresentCredentialRequestFormat.Anoncred.name) + ) + ) && + assertTrue( + record.requestPresentationData.get.attachments.map(_.data) == + Seq( + Base64( + JBase64.getUrlEncoder.encodeToString( + AnoncredPresentationRequestV1.schemaSerDes.serialize(anoncredPresentationRequestV1).getBytes() + ) + ) + ) + ) && + assertTrue(record.proposePresentationData.isEmpty) && + assertTrue(record.presentationData.isEmpty) && + assertTrue(record.credentialsToUse.isEmpty) + } + } + }, test("getPresentationRecords returns created PresentationRecord") { for { svc <- ZIO.service[PresentationService] pairwiseProverDid = DidId("did:peer:Prover") - record1 <- svc.createRecord() - record2 <- svc.createRecord() + record1 <- svc.createJwtRecord() + record2 <- svc.createJwtRecord() records <- svc.getPresentationRecords(false) } yield { assertTrue(records.size == 2) @@ -114,7 +176,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp test("getPresentationRecordsByStates returns the correct records") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() records <- svc.getPresentationRecordsByStates( ignoreWithZeroRetries = true, limit = 10, @@ -132,16 +194,16 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp test("getPresentationRecord returns the correct record") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() - bRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() + bRecord <- svc.createJwtRecord() record <- svc.getPresentationRecord(bRecord.id) } yield assertTrue(record.contains(bRecord)) }, test("getPresentationRecord returns nothing for an unknown 'recordId'") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() - bRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() + bRecord <- svc.createJwtRecord() record <- svc.getPresentationRecord(DidCommID()) } yield assertTrue(record.isEmpty) }, @@ -159,7 +221,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp IssueCredentialRecord.ProtocolState.CredentialReceived ) svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() repo <- ZIO.service[PresentationRepository] _ <- repo.updatePresentationWithCredentialsToUse( aRecord.id, @@ -176,7 +238,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp for { svc <- ZIO.service[PresentationService] pairwiseProverDid = DidId("did:peer:Prover") - record <- svc.createRecord() + record <- svc.createJwtRecord() record <- svc.markRequestPresentationSent(record.id) } yield { @@ -187,7 +249,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp for { svc <- ZIO.service[PresentationService] pairwiseProverDid = DidId("did:peer:Prover") - record <- svc.createRecord() + record <- svc.createJwtRecord() repo <- ZIO.service[PresentationRepository] _ <- repo.updatePresentationRecordProtocolState( record.id, @@ -331,7 +393,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp for { svc <- ZIO.service[PresentationService] pairwiseProverDid = DidId("did:peer:Prover") - record <- svc.createRecord() + record <- svc.createJwtRecord() repo <- ZIO.service[PresentationRepository] _ <- repo.updatePresentationRecordProtocolState( record.id, @@ -347,7 +409,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp test("receivePresentation updates the PresentatinRecord") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() p = presentation(aRecord.thid.value) aRecordReceived <- svc.receivePresentation(p) @@ -359,7 +421,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp test("acceptPresentation updates the PresentatinRecord") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() p = presentation(aRecord.thid.value) aRecordReceived <- svc.receivePresentation(p) repo <- ZIO.service[PresentationRepository] @@ -377,7 +439,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp test("markPresentationRejected updates the PresentatinRecord") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() p = presentation(aRecord.thid.value) _ <- svc.receivePresentation(p) repo <- ZIO.service[PresentationRepository] @@ -396,7 +458,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp test("rejectPresentation updates the PresentatinRecord") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() p = presentation(aRecord.thid.value) aRecordReceived <- svc.receivePresentation(p) repo <- ZIO.service[PresentationRepository] @@ -416,7 +478,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp for { svc <- ZIO.service[PresentationService] pairwiseProverDid = DidId("did:peer:Prover") - record <- svc.createRecord() + record <- svc.createJwtRecord() p = presentation(record.thid.value) repo <- ZIO.service[PresentationRepository] _ <- repo.updatePresentationRecordProtocolState( @@ -433,7 +495,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp for { svc <- ZIO.service[PresentationService] pairwiseProverDid = DidId("did:peer:Prover") - record <- svc.createRecord() + record <- svc.createJwtRecord() repo <- ZIO.service[PresentationRepository] _ <- repo.updatePresentationRecordProtocolState( record.id, @@ -448,7 +510,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp test("receiveProposePresentation updates the PresentatinRecord") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() p = proposePresentation(aRecord.thid.value) aRecordReceived <- svc.receiveProposePresentation(p) } yield { @@ -459,7 +521,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp test("acceptProposePresentation updates the PresentatinRecord") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() p = proposePresentation(aRecord.thid.value) aRecordReceived <- svc.receiveProposePresentation(p) repo <- ZIO.service[PresentationRepository] @@ -485,8 +547,8 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp val wallet2 = ZLayer.succeed(WalletAccessContext(walletId2)) for { svc <- ZIO.service[PresentationService] - record1 <- svc.createRecord().provide(wallet1) - record2 <- svc.createRecord().provide(wallet2) + record1 <- svc.createJwtRecord().provide(wallet1) + record2 <- svc.createJwtRecord().provide(wallet2) ownRecord1 <- svc.getPresentationRecord(record1.id).provide(wallet1) ownRecord2 <- svc.getPresentationRecord(record2.id).provide(wallet2) crossRecord1 <- svc.getPresentationRecord(record1.id).provide(wallet2) diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpecHelper.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpecHelper.scala index 83b8da38ad..a6ce5b0ba7 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpecHelper.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/PresentationServiceSpecHelper.scala @@ -5,13 +5,15 @@ import io.iohk.atala.mercury.model.{AttachmentDescriptor, DidId} import io.iohk.atala.mercury.protocol.presentproof.* import io.iohk.atala.mercury.{AgentPeerService, PeerDID} import io.iohk.atala.pollux.core.model.* -import io.iohk.atala.pollux.core.repository.PresentationRepository +import io.iohk.atala.pollux.core.model.error.PresentationError import io.iohk.atala.pollux.core.repository.{ CredentialRepository, CredentialRepositoryInMemory, + PresentationRepository, PresentationRepositoryInMemory } import io.iohk.atala.pollux.vc.jwt.* +import io.iohk.atala.shared.models.WalletAccessContext import zio.* import java.security.* @@ -139,22 +141,21 @@ trait PresentationServiceSpecHelper { ) extension (svc: PresentationService) - def createRecord( + def createJwtRecord( pairwiseVerifierDID: DidId = DidId("did:prism:issuer"), pairwiseProverDID: DidId = DidId("did:prism:prover-pairwise"), thid: DidCommID = DidCommID(), - schemaId: String = "schemaId", - connectionId: Option[String] = None, - ) = { + schemaId: _root_.java.lang.String = "schemaId", + options: Option[io.iohk.atala.pollux.core.model.presentation.Options] = None + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { val proofType = ProofType(schemaId, None, None) - svc.createPresentationRecord( + svc.createJwtPresentationRecord( thid = thid, pairwiseVerifierDID = pairwiseVerifierDID, pairwiseProverDID = pairwiseProverDID, connectionId = Some("connectionId"), proofTypes = Seq(proofType), - options = None, - format = CredentialFormat.JWT, + options = options ) } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofController.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofController.scala index 40af6c73d5..02dc1223d2 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofController.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofController.scala @@ -45,6 +45,8 @@ object PresentProofController { ErrorResponse.notFound(detail = Some(s"Thread Id not found: $thid")) case PresentationError.InvalidFlowStateError(msg) => ErrorResponse.badRequest(title = "InvalidFlowState", detail = Some(msg)) + case PresentationError.MissingAnoncredPresentationRequest(msg) => + ErrorResponse.badRequest(title = "Missing Anoncred Presentation Request", detail = Some(msg)) case PresentationError.UnexpectedError(msg) => ErrorResponse.internalServerError(detail = Some(msg)) case PresentationError.IssuedCredentialNotFoundError(_) => diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofControllerImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofControllerImpl.scala index 77d478c648..581593677b 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofControllerImpl.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/PresentProofControllerImpl.scala @@ -30,22 +30,41 @@ class PresentProofControllerImpl( val result: ZIO[WalletAccessContext, ConnectionServiceError | PresentationError, PresentationStatus] = for { didIdPair <- getPairwiseDIDs(request.connectionId).provideSomeLayer(ZLayer.succeed(connectionService)) credentialFormat = request.credentialFormat.map(CredentialFormat.valueOf).getOrElse(CredentialFormat.JWT) - record <- presentationService - .createPresentationRecord( - pairwiseVerifierDID = didIdPair.myDID, - pairwiseProverDID = didIdPair.theirDid, - thid = DidCommID(), - connectionId = Some(request.connectionId.toString), - proofTypes = request.proofs.map { e => - ProofType( - schema = e.schemaId, // TODO rename field to schemaId - requiredFields = None, - trustIssuers = Some(e.trustIssuers.map(DidId(_))) - ) - }, - options = request.options.map(x => Options(x.challenge, x.domain)), - format = credentialFormat, - ) + record <- + credentialFormat match { + case CredentialFormat.JWT => + presentationService + .createJwtPresentationRecord( + pairwiseVerifierDID = didIdPair.myDID, + pairwiseProverDID = didIdPair.theirDid, + thid = DidCommID(), + connectionId = Some(request.connectionId.toString), + proofTypes = request.proofs.map { e => + ProofType( + schema = e.schemaId, + requiredFields = None, + trustIssuers = Some(e.trustIssuers.map(DidId(_))) + ) + }, + options = request.options.map(x => Options(x.challenge, x.domain)) + ) + case CredentialFormat.AnonCreds => + request.anoncredPresentationRequest match { + case Some(presentationRequest) => + presentationService + .createAnoncredPresentationRecord( + pairwiseVerifierDID = didIdPair.myDID, + pairwiseProverDID = didIdPair.theirDid, + thid = DidCommID(), + connectionId = Some(request.connectionId.toString), + presentationRequest = presentationRequest + ) + case None => + ZIO.fail( + PresentationError.MissingAnoncredPresentationRequest("Anoncred presentation request is missing") + ) + } + } } yield PresentationStatus.fromDomain(record) result.mapError { diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/http/RequestPresentationInput.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/http/RequestPresentationInput.scala index fd146ac010..4f50e50f30 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/http/RequestPresentationInput.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/http/RequestPresentationInput.scala @@ -1,9 +1,10 @@ package io.iohk.atala.presentproof.controller.http import io.iohk.atala.api.http.Annotation +import io.iohk.atala.pollux.core.service.serdes.* import io.iohk.atala.presentproof.controller.http.RequestPresentationInput.annotations -import sttp.tapir.{Schema, Validator} import sttp.tapir.Schema.annotations.{description, encodedExample} +import sttp.tapir.{Schema, Validator} import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} import java.util.UUID @@ -18,6 +19,9 @@ final case class RequestPresentationInput( @description(annotations.proofs.description) @encodedExample(annotations.proofs.example) proofs: Seq[ProofRequestAux], + @description(annotations.proofs.description) // TODO + @encodedExample(annotations.proofs.example) // TODO + anoncredPresentationRequest: Option[AnoncredPresentationRequestV1], @description(annotations.credentialFormat.description) @encodedExample(annotations.credentialFormat.example) credentialFormat: Option[String], @@ -61,5 +65,19 @@ object RequestPresentationInput { given decoder: JsonDecoder[RequestPresentationInput] = DeriveJsonDecoder.gen[RequestPresentationInput] + import AnoncredPresentationRequestV1.given + + given Schema[AnoncredPresentationRequestV1] = Schema.derived + + given Schema[AnoncredRequestedAttributeV1] = Schema.derived + + given Schema[AnoncredRequestedPredicateV1] = Schema.derived + + given Schema[AnoncredNonRevokedIntervalV1] = Schema.derived + + given Schema[AnoncredAttributeRestrictionV1] = Schema.derived + + given Schema[AnoncredPredicateRestrictionV1] = Schema.derived + given schema: Schema[RequestPresentationInput] = Schema.derived }