diff --git a/build.sbt b/build.sbt index 8c3c7c797b..515717e031 100644 --- a/build.sbt +++ b/build.sbt @@ -602,7 +602,7 @@ lazy val protocolPresentProof = project .settings(libraryDependencies += D.zio) .settings(libraryDependencies ++= Seq(D.circeCore, D.circeGeneric, D.circeParser)) .settings(libraryDependencies += D.munitZio) - .dependsOn(models) + .dependsOn(models, protocolInvitation) lazy val vc = project .in(file("mercury/vc")) diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/ControllerHelper.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/ControllerHelper.scala index 8a5aa712ec..f79863d9b4 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/ControllerHelper.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/ControllerHelper.scala @@ -14,8 +14,9 @@ import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError.{ import org.hyperledger.identus.connect.core.model.ConnectionRecord import org.hyperledger.identus.connect.core.model.ConnectionRecord.{ProtocolState, Role} import org.hyperledger.identus.connect.core.service.ConnectionService -import org.hyperledger.identus.mercury.model.DidId +import org.hyperledger.identus.mercury.model.* import org.hyperledger.identus.shared.models.WalletAccessContext +import zio.* import zio.{IO, ZIO} import java.util.UUID diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala index 22e4e9d568..5d4ff494ea 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/DIDStateSyncBackgroundJobs.scala @@ -1,11 +1,13 @@ package org.hyperledger.identus.agent.server.jobs +import org.hyperledger.identus.agent.walletapi.model.error.GetManagedDIDError import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService +import org.hyperledger.identus.shared.models.WalletAccessContext import zio.* object DIDStateSyncBackgroundJobs { - val syncDIDPublicationStateFromDlt = + val syncDIDPublicationStateFromDlt: ZIO[WalletAccessContext with ManagedDIDService, GetManagedDIDError, Unit] = for { managedDidService <- ZIO.service[ManagedDIDService] _ <- managedDidService.syncManagedDIDState diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala index c42f4899b5..a07ac1f04a 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/PresentBackgroundJobs.scala @@ -96,13 +96,17 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { for { _ <- ZIO.logDebug(s"Running action with records => $record") _ <- record match { - case PresentationRecord(id, _, _, _, _, _, _, _, ProposalPending, _, _, _, _, _, _, _, _, _, _, _, _) => + case PresentationRecord(_, _, _, _, _, _, _, _, InvitationGenerated, _, _, _, _, _, _, _, _, _, _, _, _, _) => + ZIO.unit + case PresentationRecord(_, _, _, _, _, _, _, _, InvitationExpired, _, _, _, _, _, _, _, _, _, _, _, _, _) => + ZIO.unit + case PresentationRecord(id, _, _, _, _, _, _, _, ProposalPending, _, _, _, _, _, _, _, _, _, _, _, _, _) => ZIO.fail(NotImplemented) - case PresentationRecord(id, _, _, _, _, _, _, _, ProposalSent, _, _, _, _, _, _, _, _, _, _, _, _) => + case PresentationRecord(id, _, _, _, _, _, _, _, ProposalSent, _, _, _, _, _, _, _, _, _, _, _, _, _) => ZIO.fail(NotImplemented) - case PresentationRecord(id, _, _, _, _, _, _, _, ProposalReceived, _, _, _, _, _, _, _, _, _, _, _, _) => + case PresentationRecord(id, _, _, _, _, _, _, _, ProposalReceived, _, _, _, _, _, _, _, _, _, _, _, _, _) => ZIO.fail(NotImplemented) - case PresentationRecord(id, _, _, _, _, _, _, _, ProposalRejected, _, _, _, _, _, _, _, _, _, _, _, _) => + case PresentationRecord(id, _, _, _, _, _, _, _, ProposalRejected, _, _, _, _, _, _, _, _, _, _, _, _, _) => ZIO.fail(NotImplemented) case PresentationRecord( id, @@ -115,6 +119,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _, RequestPending, _, + _, None, _, _, @@ -139,6 +144,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _, RequestPending, _, + _, Some(requestPresentation), _, _, @@ -152,7 +158,30 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _ ) => // Verifier Verifier.handleRequestPending(id, requestPresentation) - case PresentationRecord(id, _, _, _, _, _, _, _, RequestSent, _, _, _, _, _, _, _, _, _, _, _, _) => // Verifier + case PresentationRecord( + id, + _, + _, + _, + _, + _, + _, + _, + RequestSent, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _ + ) => // Verifier ZIO.logDebug("PresentationRecord: RequestSent") *> ZIO.unit case PresentationRecord( id, @@ -175,6 +204,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, _ ) => // Prover ZIO.logDebug("PresentationRecord: RequestReceived") *> ZIO.unit @@ -199,14 +229,38 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, _ ) => // Prover ZIO.logDebug("PresentationRecord: RequestRejected") *> ZIO.unit - case PresentationRecord(id, _, _, _, _, _, _, _, ProblemReportPending, _, _, _, _, _, _, _, _, _, _, _, _) => + case PresentationRecord(id, _, _, _, _, _, _, _, ProblemReportPending, _, _, _, _, _, _, _, _, _, _, _, _, _) => ZIO.fail(NotImplemented) - case PresentationRecord(id, _, _, _, _, _, _, _, ProblemReportSent, _, _, _, _, _, _, _, _, _, _, _, _) => + case PresentationRecord(id, _, _, _, _, _, _, _, ProblemReportSent, _, _, _, _, _, _, _, _, _, _, _, _, _) => ZIO.fail(NotImplemented) - case PresentationRecord(id, _, _, _, _, _, _, _, ProblemReportReceived, _, _, _, _, _, _, _, _, _, _, _, _) => + case PresentationRecord( + id, + _, + _, + _, + _, + _, + _, + _, + ProblemReportReceived, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _ + ) => ZIO.fail(NotImplemented) case PresentationRecord( id, @@ -219,6 +273,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _, PresentationPending, _, + _, None, _, _, @@ -244,6 +299,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _, PresentationPending, credentialFormat, + _, Some(requestPresentation), _, _, @@ -277,6 +333,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, None, _, _, @@ -302,6 +359,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, Some(presentation), _, _, @@ -315,7 +373,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { ZIO.logDebug("PresentationRecord: PresentationGenerated") *> ZIO.unit Prover.handlePresentationGenerated(id, presentation) - case PresentationRecord(id, _, _, _, _, _, _, _, PresentationSent, _, _, _, _, _, _, _, _, _, _, _, _) => + case PresentationRecord(id, _, _, _, _, _, _, _, PresentationSent, _, _, _, _, _, _, _, _, _, _, _, _, _) => ZIO.logDebug("PresentationRecord: PresentationSent") *> ZIO.unit case PresentationRecord( id, @@ -330,6 +388,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, None, _, _, @@ -341,7 +400,30 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _ ) => // Verifier ZIO.fail(InvalidState("PresentationRecord in 'PresentationReceived' with no Presentation")) - case PresentationRecord(_, _, _, _, _, _, _, _, PresentationReceived, _, None, _, _, _, _, _, _, _, _, _, _) => + case PresentationRecord( + _, + _, + _, + _, + _, + _, + _, + _, + PresentationReceived, + _, + _, + None, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _ + ) => ZIO.fail(InvalidState("PresentationRecord in 'PresentationReceived' with no Presentation Request")) case PresentationRecord( id, @@ -354,6 +436,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _, PresentationReceived, credentialFormat, + _, Some(requestPresentation), _, Some(presentation), @@ -390,15 +473,18 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, _ ) => ZIO.logDebug("PresentationRecord: PresentationVerificationFailed") *> ZIO.unit - case PresentationRecord(id, _, _, _, _, _, _, _, PresentationAccepted, _, _, _, _, _, _, _, _, _, _, _, _) => + case PresentationRecord(id, _, _, _, _, _, _, _, PresentationAccepted, _, _, _, _, _, _, _, _, _, _, _, _, _) => ZIO.logDebug("PresentationRecord: PresentationVerifiedAccepted") *> ZIO.unit - case PresentationRecord(id, _, _, _, _, _, _, _, PresentationVerified, _, _, _, _, _, _, _, _, _, _, _, _) => + case PresentationRecord(id, _, _, _, _, _, _, _, PresentationVerified, _, _, _, _, _, _, _, _, _, _, _, _, _) => ZIO.logDebug("PresentationRecord: PresentationVerified") *> ZIO.unit - case PresentationRecord(id, _, _, _, _, _, _, _, PresentationRejected, _, _, _, _, _, _, _, _, _, _, _, _) => + case PresentationRecord(id, _, _, _, _, _, _, _, PresentationRejected, _, _, _, _, _, _, _, _, _, _, _, _, _) => ZIO.logDebug("PresentationRecord: PresentationRejected") *> ZIO.unit + case _ => + ZIO.logWarning(s"Unhandled PresentationRecord state: ${record.protocolState}") } } yield () } @@ -453,7 +539,9 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { Unit ] = { val proverPresentationPendingToGeneratedFlow = for { - walletAccessContext <- buildWalletAccessContextLayer(requestPresentation.to) + walletAccessContext <- buildWalletAccessContextLayer( + requestPresentation.to.getOrElse(throw new RuntimeException("to is None is not possible")) + ) _ <- for { presentationService <- ZIO.service[PresentationService] prover <- createPrismDIDIssuerFromPresentationCredentials(id, credentialsToUse.getOrElse(Nil)) @@ -486,8 +574,8 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { ) ), thid = requestPresentation.thid.orElse(Some(requestPresentation.id)), - from = requestPresentation.to, - to = requestPresentation.from + from = requestPresentation.to.getOrElse(throw new RuntimeException("to is None is not possible")), + to = requestPresentation.from.getOrElse(throw new RuntimeException("from is None is not possible")) ) ) } yield presentation @@ -509,7 +597,9 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { ERROR, Unit ] = for { - walletAccessContext <- buildWalletAccessContextLayer(requestPresentation.to) + walletAccessContext <- buildWalletAccessContextLayer( + requestPresentation.to.getOrElse(throw new RuntimeException("to is None is not possible")) + ) result <- for { presentationService <- ZIO.service[PresentationService] @@ -539,7 +629,9 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { maybeCredentialsToUseJson match { case Some(credentialsToUseJson) => val proverPresentationPendingToGeneratedFlow = for { - walletAccessContext <- buildWalletAccessContextLayer(requestPresentation.to) + walletAccessContext <- buildWalletAccessContextLayer( + requestPresentation.to.getOrElse(throw new RuntimeException("to is None is not possible")) + ) result <- for { presentationService <- ZIO.service[PresentationService] anoncredCredentialProofs <- @@ -741,10 +833,14 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { val verifierReqPendingToSentFlow = for { _ <- ZIO.log(s"PresentationRecord: RequestPending (Send Message)") - walletAccessContext <- buildWalletAccessContextLayer(record.from) + walletAccessContext <- buildWalletAccessContextLayer( + record.from.getOrElse(throw new RuntimeException("from is None is not possible")) + ) result <- for { didOps <- ZIO.service[DidOps] - didCommAgent <- buildDIDCommAgent(record.from).provideSomeLayer(ZLayer.succeed(walletAccessContext)) + didCommAgent <- buildDIDCommAgent( + record.from.getOrElse(throw new RuntimeException("from is None is not possible")) + ).provideSomeLayer(ZLayer.succeed(walletAccessContext)) resp <- MessagingService .send(record.makeMessage) @@ -1037,11 +1133,11 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { } } - val syncDIDPublicationStateFromDlt: ZIO[WalletAccessContext & ManagedDIDService, GetManagedDIDError, Unit] = - for { - managedDidService <- ZIO.service[ManagedDIDService] - _ <- managedDidService.syncManagedDIDState - _ <- managedDidService.syncUnconfirmedUpdateOperations - } yield () +// val syncDIDPublicationStateFromDlt: ZIO[WalletAccessContext & ManagedDIDService, GetManagedDIDError, Unit] = +// for { +// managedDidService <- ZIO.service[ManagedDIDService] +// _ <- managedDidService.syncManagedDIDState +// _ <- managedDidService.syncUnconfirmedUpdateOperations +// } yield () } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofController.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofController.scala index 027dc6ce7b..fe4f5edd44 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofController.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofController.scala @@ -31,6 +31,17 @@ trait PresentProofController { rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, PresentationStatus] + def createOOBRequestPresentationInvitation( + request: OOBRequestPresentationInput + )(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, PresentationStatus] + + def acceptRequestPresentationInvitation( + request: AcceptRequestPresentationInvitation + )(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, PresentationStatus] } object PresentProofController { diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofControllerImpl.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofControllerImpl.scala index c743c83a79..044ae2d40b 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofControllerImpl.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofControllerImpl.scala @@ -1,6 +1,8 @@ package org.hyperledger.identus.presentproof.controller +import org.hyperledger.identus.agent.server.config.AppConfig import org.hyperledger.identus.agent.server.ControllerHelper +import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext} import org.hyperledger.identus.api.http.model.PaginationInput import org.hyperledger.identus.connect.core.model.error.ConnectionServiceError @@ -10,6 +12,7 @@ import org.hyperledger.identus.mercury.protocol.presentproof.ProofType import org.hyperledger.identus.pollux.core.model.{CredentialFormat, DidCommID, PresentationRecord} import org.hyperledger.identus.pollux.core.model.error.PresentationError import org.hyperledger.identus.pollux.core.model.presentation.Options +import org.hyperledger.identus.pollux.core.service.serdes.AnoncredPresentationRequestV1 import org.hyperledger.identus.pollux.core.service.PresentationService import org.hyperledger.identus.presentproof.controller.http.* import org.hyperledger.identus.presentproof.controller.PresentProofController.toDidCommID @@ -23,7 +26,9 @@ import scala.language.implicitConversions class PresentProofControllerImpl( presentationService: PresentationService, - connectionService: ConnectionService + connectionService: ConnectionService, + managedDIDService: ManagedDIDService, + appConfig: AppConfig ) extends PresentProofController with ControllerHelper { override def requestPresentation(request: RequestPresentationInput)(implicit @@ -31,74 +36,145 @@ class PresentProofControllerImpl( ): ZIO[WalletAccessContext, ErrorResponse, PresentationStatus] = { 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 <- - 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.SDJWT => - request.claims match { - case Some(claims) => - for { - s <- presentationService.createSDJWTPresentationRecord( - 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(_))) - ) - }, - claimsToDisclose = claims, - options = request.options.map(o => Options(o.challenge, o.domain)) - ) - } yield s + record <- createRequestPresentation( + verifierDID = didIdPair.myDID, + proverDID = Some(didIdPair.theirDid), + connectionId = Some(request.connectionId.toString), + request = request + ) + } yield PresentationStatus.fromDomain(record) + result + } - case None => - ZIO.fail( - PresentationError.MissingAnoncredPresentationRequest( - "presentation request is missing claims to be disclosed" - ) - ) - } - 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") - ) - } - } + override def createOOBRequestPresentationInvitation(request: OOBRequestPresentationInput)(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, PresentationStatus] = { + val result: ZIO[WalletAccessContext, ConnectionServiceError | PresentationError, PresentationStatus] = for { + peerDid <- managedDIDService.createAndStorePeerDID(appConfig.agent.didCommEndpoint.publicEndpointUrl) + record <- createRequestPresentation( + verifierDID = peerDid.did, + proverDID = None, + connectionId = None, + request = request + ) } yield PresentationStatus.fromDomain(record) result } + private def createRequestPresentation( + verifierDID: DidId, + proverDID: Option[DidId], + connectionId: Option[String], + request: RequestPresentationInput | OOBRequestPresentationInput + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { + request match { + case req: RequestPresentationInput => + createPresentationRecord( + verifierDID, + proverDID, + connectionId, + req.credentialFormat, + req.proofs, + req.options.map(o => Options(o.challenge, o.domain)), + req.claims, + req.anoncredPresentationRequest, + None, + None + ) + case req: OOBRequestPresentationInput => + createPresentationRecord( + verifierDID, + proverDID, + connectionId, + req.credentialFormat, + req.proofs, + req.options.map(o => Options(o.challenge, o.domain)), + req.claims, + req.anoncredPresentationRequest, + req.goalCode, + req.goal + ) + } + } + + private def createPresentationRecord( + verifierDID: DidId, + proverDID: Option[DidId], + connectionId: Option[String], + credentialFormat: Option[String], + proofs: Seq[ProofRequestAux], + options: Option[Options], + claims: Option[zio.json.ast.Json.Obj], + anoncredPresentationRequest: Option[AnoncredPresentationRequestV1], + goalCode: Option[String], + goal: Option[String] + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { + val format = credentialFormat.map(CredentialFormat.valueOf).getOrElse(CredentialFormat.JWT) + format match { + case CredentialFormat.JWT => + presentationService.createJwtPresentationRecord( + pairwiseVerifierDID = verifierDID, + pairwiseProverDID = proverDID, + thid = DidCommID(), + connectionId = connectionId, + proofTypes = proofs.map { e => + ProofType( + schema = e.schemaId, + requiredFields = None, + trustIssuers = Some(e.trustIssuers.map(DidId(_))) + ) + }, + options = options, + goalCode = goalCode, + goal = goal + ) + case CredentialFormat.SDJWT => + claims match { + case Some(claimsToDisclose) => + presentationService.createSDJWTPresentationRecord( + pairwiseVerifierDID = verifierDID, + pairwiseProverDID = proverDID, + thid = DidCommID(), + connectionId = connectionId, + proofTypes = proofs.map { e => + ProofType( + schema = e.schemaId, + requiredFields = None, + trustIssuers = Some(e.trustIssuers.map(DidId(_))) + ) + }, + claimsToDisclose = claimsToDisclose, + options = options, + goalCode = goalCode, + goal = goal + ) + case None => + ZIO.fail( + PresentationError.MissingSDJWTPresentationRequest( + "presentation request is missing claims to be disclosed" + ) + ) + } + case CredentialFormat.AnonCreds => + anoncredPresentationRequest match { + case Some(presentationRequest) => + presentationService.createAnoncredPresentationRecord( + pairwiseVerifierDID = verifierDID, + pairwiseProverDID = proverDID, + thid = DidCommID(), + connectionId = connectionId, + presentationRequest = presentationRequest, + goalCode = goalCode, + goal = goal + ) + case None => + ZIO.fail( + PresentationError.MissingAnoncredPresentationRequest("Anoncred presentation request is missing") + ) + } + } + } + override def getPresentations(paginationInput: PaginationInput, thid: Option[String])(implicit rc: RequestContext ): ZIO[WalletAccessContext, ErrorResponse, PresentationStatusPage] = { @@ -168,9 +244,27 @@ class PresentProofControllerImpl( result } + + def acceptRequestPresentationInvitation( + request: AcceptRequestPresentationInvitation + )(implicit + rc: RequestContext + ): ZIO[WalletAccessContext, ErrorResponse, PresentationStatus] = { + for { + pairwiseDid <- managedDIDService.createAndStorePeerDID(appConfig.agent.didCommEndpoint.publicEndpointUrl) + requestPresentation <- presentationService.getRequestPresentationFromInvitation( + pairwiseDid.did, + request.invitation + ) + record <- presentationService.receiveRequestPresentation( + None, // connectionless hence none + requestPresentation + ) // TODO should we store the invitation in prover db ??? + } yield PresentationStatus.fromDomain(record) + } } object PresentProofControllerImpl { - val layer: URLayer[PresentationService & ConnectionService, PresentProofController] = - ZLayer.fromFunction(PresentProofControllerImpl(_, _)) + val layer: URLayer[PresentationService & ConnectionService & ManagedDIDService & AppConfig, PresentProofController] = + ZLayer.fromFunction(PresentProofControllerImpl(_, _, _, _)) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofEndpoints.scala index 2534746a3d..e24318ee5d 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofEndpoints.scala @@ -118,4 +118,66 @@ object PresentProofEndpoints { .out(jsonBody[PresentationStatus]) .errorOut(basicFailureAndNotFoundAndForbidden) + val createOOBRequestPresentationInvitation: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + (RequestContext, OOBRequestPresentationInput), + ErrorResponse, + PresentationStatus, + Any + ] = + endpoint.post + .tag("Present Proof") + .name("createOOBRequestPresentationInvitation") + .summary( + "As a Verifier, create a new OOB Invitation as proof presentation request that can be delivered out-of-band to a invitee/prover." + ) + .description(""" + |Create a new presentation request invitation that can be delivered out-of-band to a peer Agent, regardless of whether it resides in Cloud Agent or edge environment. + |The generated invitation adheres to the DIDComm Messaging v2.0 - [Out of Band Messages](https://identity.foundation/didcomm-messaging/spec/v2.0/#out-of-band-messages) specification [section 9.5.4](https://identity.foundation/didcomm-messaging/spec/v2.0/#invitation). + |The from field of the out-of-band invitation message contains a freshly generated Peer DID that complies with the [did:peer:2](https://identity.foundation/peer-did-method-spec/#generating-a-didpeer2) specification. + |This Peer DID includes the 'uri' location of the DIDComm messaging service, essential for the prover's subsequent execution of the connection flow. + |In the Agent database, the created presentation record has an initial state set to `InvitationGenerated`. + |The invitation is in the form of a presentation request (as described https://github.com/decentralized-identity/waci-didcomm/blob/main/present_proof/present-proof-v3.md), which is included as an attachment in the OOB DIDComm message sent to the invitee/prover. + |""".stripMargin) + .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) + .in("present-proof" / "presentations" / "invitation") + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in(jsonBody[OOBRequestPresentationInput].description("The present proof creation request.")) + .out( + statusCode(StatusCode.Created).description( + "The proof presentation request invitation was created successfully and that can be delivered as out-of-band to a peer Agent.." + ) + ) + .out(jsonBody[PresentationStatus]) + .errorOut(basicFailureAndNotFoundAndForbidden) + + val acceptRequestPresentationInvitation: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + (RequestContext, AcceptRequestPresentationInvitation), + ErrorResponse, + PresentationStatus, + Any + ] = + endpoint.post + .tag("Present Proof") + .name("acceptRequestPresentationInvitation") + .summary( + "Decode the invitation extract Request Presentation and Create the proof presentation record with RequestReceived state." + ) + .description("Accept Invitation for request presentation") + .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in( + "present-proof" / "presentations" / "accept-invitation" + ) + .in( + jsonBody[AcceptRequestPresentationInvitation].description( + "The action to perform on the proof presentation request invitation." + ) + ) + .out(statusCode(StatusCode.Ok).description("The proof presentation record was successfully updated.")) + .out(jsonBody[PresentationStatus]) + .errorOut(basicFailureAndNotFoundAndForbidden) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofServerEndpoints.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofServerEndpoints.scala index 798902f51c..e7f30f9864 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofServerEndpoints.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/PresentProofServerEndpoints.scala @@ -4,8 +4,15 @@ import org.hyperledger.identus.agent.walletapi.model.BaseEntity import org.hyperledger.identus.api.http.model.PaginationInput import org.hyperledger.identus.api.http.RequestContext import org.hyperledger.identus.iam.authentication.{Authenticator, Authorizer, DefaultAuthenticator, SecurityLogic} -import org.hyperledger.identus.presentproof.controller.http.{RequestPresentationAction, RequestPresentationInput} +import org.hyperledger.identus.presentproof.controller.http.{ + AcceptRequestPresentationInvitation, + OOBRequestPresentationInput, + RequestPresentationAction, + RequestPresentationInput +} import org.hyperledger.identus.presentproof.controller.PresentProofEndpoints.{ + acceptRequestPresentationInvitation, + createOOBRequestPresentationInvitation, getAllPresentations, getPresentation, requestPresentation, @@ -71,11 +78,37 @@ class PresentProofServerEndpoints( } } + private val createOOBRequestPresentationInvitationEndpoint: ZServerEndpoint[Any, Any] = + createOOBRequestPresentationInvitation + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (ctx: RequestContext, action: OOBRequestPresentationInput) => + presentProofController + .createOOBRequestPresentationInvitation(action)(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } + } + + private val acceptRequestPresentationInvitationEndpoint: ZServerEndpoint[Any, Any] = + acceptRequestPresentationInvitation + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (ctx: RequestContext, action: AcceptRequestPresentationInvitation) => + presentProofController + .acceptRequestPresentationInvitation(action)(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } + } + val all: List[ZServerEndpoint[Any, Any]] = List( requestPresentationEndpoint, getAllPresentationsEndpoint, getPresentationEndpoint, - updatePresentationEndpoint + updatePresentationEndpoint, + createOOBRequestPresentationInvitationEndpoint, + acceptRequestPresentationInvitationEndpoint ) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/AcceptRequestPresentationInvitation.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/AcceptRequestPresentationInvitation.scala new file mode 100644 index 0000000000..b7e4ecdf2f --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/AcceptRequestPresentationInvitation.scala @@ -0,0 +1,34 @@ +package org.hyperledger.identus.presentproof.controller.http + +import org.hyperledger.identus.api.http.Annotation +import sttp.tapir.Schema +import sttp.tapir.Schema.annotations.{description, encodedExample} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} +import AcceptRequestPresentationInvitation.annotations + +case class AcceptRequestPresentationInvitation( + @description(annotations.invitation.description) + @encodedExample(annotations.invitation.example) + invitation: String +) + +object AcceptRequestPresentationInvitation { + + object annotations { + object invitation + extends Annotation[String]( + description = "The base64-encoded raw invitation.", + example = + "eyJAaWQiOiIzZmE4NWY2NC01NzE3LTQ1NjItYjNmYy0yYzk2M2Y2NmFmYTYiLCJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvbXktZmFtaWx5LzEuMC9teS1tZXNzYWdlLXR5cGUiLCJkaWQiOiJXZ1d4cXp0ck5vb0c5MlJYdnhTVFd2IiwiaW1hZ2VVcmwiOiJodHRwOi8vMTkyLjE2OC41Ni4xMDEvaW1nL2xvZ28uanBnIiwibGFiZWwiOiJCb2IiLCJyZWNpcGllbnRLZXlzIjpbIkgzQzJBVnZMTXY2Z21NTmFtM3VWQWpacGZrY0pDd0R3blpuNnozd1htcVBWIl0sInJvdXRpbmdLZXlzIjpbIkgzQzJBVnZMTXY2Z21NTmFtM3VWQWpacGZrY0pDd0R3blpuNnozd1htcVBWIl0sInNlcnZpY2VFbmRwb2ludCI6Imh0dHA6Ly8xOTIuMTY4LjU2LjEwMTo4MDIwIn0=" + ) + } + + given encoder: JsonEncoder[AcceptRequestPresentationInvitation] = + DeriveJsonEncoder.gen[AcceptRequestPresentationInvitation] + + given decoder: JsonDecoder[AcceptRequestPresentationInvitation] = + DeriveJsonDecoder.gen[AcceptRequestPresentationInvitation] + + given schema: Schema[AcceptRequestPresentationInvitation] = Schema.derived + +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/OOBPresentationInvitation.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/OOBPresentationInvitation.scala new file mode 100644 index 0000000000..2061a3a4db --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/OOBPresentationInvitation.scala @@ -0,0 +1,79 @@ +package org.hyperledger.identus.presentproof.controller.http + +import org.hyperledger.identus.api.http.Annotation +import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation +import sttp.tapir.Schema +import sttp.tapir.Schema.annotations.{description, encodedExample} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} +import OOBPresentationInvitation.annotations + +import java.util.UUID + +case class OOBPresentationInvitation( + @description(annotations.id.description) + @encodedExample(annotations.id.example) + id: UUID, + @description(annotations.`type`.description) + @encodedExample(annotations.`type`.example) + `type`: String, + @description(annotations.from.description) + @encodedExample(annotations.from.example) + from: String, + @description(annotations.invitationUrl.description) + @encodedExample(annotations.invitationUrl.example) + invitationUrl: String +) + +object OOBPresentationInvitation { + + def fromDomain(invitation: Invitation) = OOBPresentationInvitation( + id = UUID.fromString(invitation.id), + `type` = invitation.`type`, + from = invitation.from.value, + invitationUrl = s"https://my.domain.com/path?_oob=${invitation.toBase64}" + ) + + object annotations { + object id + extends Annotation[UUID]( + description = + "The unique identifier of the invitation. It should be used as parent thread ID (pthid) for the Connection Request message that follows.", + example = UUID.fromString("0527aea1-d131-3948-a34d-03af39aba8b4") + ) + + object `type` + extends Annotation[String]( + description = "The DIDComm Message Type URI (MTURI) the invitation message complies with.", + example = "https://didcomm.org/out-of-band/2.0/invitation" + ) + + object from + extends Annotation[String]( + description = "The DID representing the sender to be used by recipients for future interactions.", + example = "did:peer:1234457" + ) + + object invitationUrl + extends Annotation[String]( + description = + "The invitation message encoded as a URL. This URL follows the Out of [Band 2.0 protocol](https://identity.foundation/didcomm-messaging/spec/v2.0/#out-of-band-messages) and can be used to generate a QR code for example.", + example = + "https://my.domain.com/path?_oob=eyJAaWQiOiIzZmE4NWY2NC01NzE3LTQ1NjItYjNmYy0yYzk2M2Y2NmFmYTYiLCJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvbXktZmFtaWx5LzEuMC9teS1tZXNzYWdlLXR5cGUiLCJkaWQiOiJXZ1d4cXp0ck5vb0c5MlJYdnhTVFd2IiwiaW1hZ2VVcmwiOiJodHRwOi8vMTkyLjE2OC41Ni4xMDEvaW1nL2xvZ28uanBnIiwibGFiZWwiOiJCb2IiLCJyZWNpcGllbnRLZXlzIjpbIkgzQzJBVnZMTXY2Z21NTmFtM3VWQWpacGZrY0pDd0R3blpuNnozd1htcVBWIl0sInJvdXRpbmdLZXlzIjpbIkgzQzJBVnZMTXY2Z21NTmFtM3VWQWpacGZrY0pDd0R3blpuNnozd1htcVBWIl0sInNlcnZpY2VFbmRwb2ludCI6Imh0dHA6Ly8xOTIuMTY4LjU2LjEwMTo4MDIwIn0=" + ) + } + + val Example = OOBPresentationInvitation( + id = annotations.id.example, + `type` = annotations.`type`.example, + from = annotations.from.example, + invitationUrl = annotations.invitationUrl.example + ) + + given encoder: JsonEncoder[OOBPresentationInvitation] = + DeriveJsonEncoder.gen[OOBPresentationInvitation] + + given decoder: JsonDecoder[OOBPresentationInvitation] = + DeriveJsonDecoder.gen[OOBPresentationInvitation] + + given schema: Schema[OOBPresentationInvitation] = Schema.derived +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/OOBRequestPresentationInput.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/OOBRequestPresentationInput.scala new file mode 100644 index 0000000000..eb2d57251b --- /dev/null +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/OOBRequestPresentationInput.scala @@ -0,0 +1,160 @@ +package org.hyperledger.identus.presentproof.controller.http + +import org.hyperledger.identus.api.http.Annotation +import org.hyperledger.identus.pollux.core.service.serdes.{ + AnoncredNonRevokedIntervalV1, + AnoncredPresentationRequestV1, + AnoncredRequestedAttributeV1, + AnoncredRequestedPredicateV1 +} +import sttp.tapir.{Schema, Validator} +import sttp.tapir.json.zio.* +import sttp.tapir.Schema.annotations.{description, encodedExample} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} +import OOBRequestPresentationInput.annotations + +//TODO Should I just use RequestPresentationInput and add the optional fields will that cause any confusion +final case class OOBRequestPresentationInput( + @description(annotations.goalcode.description) + @encodedExample(annotations.goalcode.example) + goalCode: Option[String] = None, + @description(annotations.goal.description) + @encodedExample(annotations.goal.example) + goal: Option[String] = None, + @description(annotations.options.description) + @encodedExample(annotations.options.example) + options: Option[Options] = None, + @description(annotations.proofs.description) + @encodedExample(annotations.proofs.example) + proofs: Seq[ProofRequestAux], + @description(annotations.anoncredPresentationRequest.description) + @encodedExample(annotations.anoncredPresentationRequest.example) + anoncredPresentationRequest: Option[AnoncredPresentationRequestV1], + @description(annotations.claims.description) + @encodedExample(annotations.claims.example) + claims: Option[zio.json.ast.Json.Obj], + @description(annotations.credentialFormat.description) + @encodedExample(annotations.credentialFormat.example) + credentialFormat: Option[String], +) + +object OOBRequestPresentationInput { + object annotations { + object goalcode + extends Annotation[String]( + description = + "A self-attested code the receiver may want to display to the user or use in automatically deciding what to do with the out-of-band message.", + example = "present-vp" + ) + + object goal + extends Annotation[String]( + description = + "A self-attested string that the receiver may want to display to the user about the context-specific goal of the out-of-band message.", + example = "Request proof of vaccine" + ) + + object options + extends Annotation[Option[Options]]( + description = "The options to use when creating the proof presentation request (e.g., domain, challenge).", + example = None + ) + object proofs + extends Annotation[Seq[ProofRequestAux]]( + description = + "The type of proofs requested in the context of this proof presentation request (e.g., VC schema, trusted issuers, etc.)", + example = Seq.empty + ) + + object anoncredPresentationRequest + extends Annotation[Option[AnoncredPresentationRequestV1]]( + description = "Anoncred Presentation Request", + example = Some( + AnoncredPresentationRequestV1( + requested_attributes = Map( + "attribute1" -> AnoncredRequestedAttributeV1( + "Attribute 1", + List( + Map( + "cred_def_id" -> "credential_definition_id_of_attribute1" + ) + ), + Some( + AnoncredNonRevokedIntervalV1( + Some(1635734400), + Some(1735734400) + ) + ) + ) + ), + requested_predicates = Map( + "predicate1" -> + AnoncredRequestedPredicateV1( + "Predicate 1", + ">=", + 18, + List( + Map( + "schema_id" -> "schema_id_of_predicate1" + ) + ), + Some( + AnoncredNonRevokedIntervalV1( + Some(1635734400), + None + ) + ) + ) + ), + name = "Example Presentation Request", + nonce = "1234567890", + version = "1.0", + non_revoked = None + ) + ) + ) + object claims + extends Annotation[Option[zio.json.ast.Json.Obj]]( + description = """ + |The set of claims to be disclosed from the issued credential. + |The JSON object should comply with the schema applicable for this offer (i.e. 'schemaId' or 'credentialDefinitionId'). + |""".stripMargin, + example = Some( + zio.json.ast.Json.Obj( + "firstname" -> zio.json.ast.Json.Str("Alice"), + "lastname" -> zio.json.ast.Json.Str("Wonderland"), + ) + ) + ) + object credentialFormat + extends Annotation[Option[String]]( + description = "The credential format (default to 'JWT')", + example = Some("JWT"), + validator = Validator.enumeration( + List( + Some("JWT"), + Some("SDJWT"), + Some("AnonCreds") + ) + ) + ) + } + + given encoder: JsonEncoder[OOBRequestPresentationInput] = + DeriveJsonEncoder.gen[OOBRequestPresentationInput] + + given decoder: JsonDecoder[OOBRequestPresentationInput] = + DeriveJsonDecoder.gen[OOBRequestPresentationInput] + + 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: Schema[OOBRequestPresentationInput] = Schema.derived +} diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/PresentationStatus.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/PresentationStatus.scala index f9eeeac563..7d4ba13f47 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/PresentationStatus.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/PresentationStatus.scala @@ -33,6 +33,17 @@ final case class PresentationStatus( @description(annotations.connectionId.description) @encodedExample(annotations.connectionId.example) connectionId: Option[String] = None, + @description(annotations.goalcode.description) + @encodedExample(annotations.goalcode.example) + goalCode: Option[String] = None, + @description(annotations.goal.description) + @encodedExample(annotations.goal.example) + goal: Option[String] = None, + @description(annotations.myDid.description) + @encodedExample(annotations.myDid.example) + myDid: Option[String] = None, + @description(annotations.invitation.description) + invitation: Option[OOBPresentationInvitation] = None, @description(annotations.metaRetries.description) @encodedExample(annotations.metaRetries.example) metaRetries: Int, @@ -60,6 +71,10 @@ object PresentationStatus { proofs = Seq.empty, data = data, connectionId = domain.connectionId, + invitation = domain.invitation.map(invitation => OOBPresentationInvitation.fromDomain(invitation)), + goalCode = domain.invitation.flatMap(_.body.goal_code), + goal = domain.invitation.flatMap(_.body.goal), + myDid = domain.invitation.map(_.from.value), metaRetries = domain.metaRetries, metaLastFailure = domain.metaLastFailure.map(failure => ErrorResponse.failureToErrorResponseConversion(failure)), ) @@ -111,7 +126,9 @@ object PresentationStatus { "PresentationRejected", "ProblemReportPending", "ProblemReportSent", - "ProblemReportReceived" + "ProblemReportReceived", + "InvitationGenerated", + "InvitationReceived" ) ) ) @@ -144,6 +161,32 @@ object PresentationStatus { example = ErrorResponse.failureToErrorResponseConversion(FailureInfo("Error", StatusCode.NotFound, "Not Found")) ) + + object goalcode + extends Annotation[String]( + description = + "A self-attested code the receiver may want to display to the user or use in automatically deciding what to do with the out-of-band message.", + example = "present-vp" + ) + + object goal + extends Annotation[String]( + description = + "A self-attested string that the receiver may want to display to the user about the context-specific goal of the out-of-band message.", + example = "To verify a Peter College Graduate credential" + ) + + object myDid + extends Annotation[String]( + description = "The DID representing me as the inviter or invitee in this specific connection.", + example = "did:peer:12345" + ) + + object invitation + extends Annotation[OOBPresentationInvitation]( + description = "The invitation for this Request Presentation", + example = OOBPresentationInvitation.Example + ) } given encoder: JsonEncoder[PresentationStatus] = diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/PresentationStatusPage.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/PresentationStatusPage.scala index df72dc1107..e7c34c1689 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/PresentationStatusPage.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/PresentationStatusPage.scala @@ -6,6 +6,8 @@ import sttp.tapir.Schema import sttp.tapir.Schema.annotations.{description, encodedExample} import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} +import java.util.UUID + final case class PresentationStatusPage( @description(annotations.contents.description) @encodedExample( // This is a hammer - to be improved in the future @@ -70,6 +72,7 @@ object PresentationStatusPage { proofs = Seq.empty, data = Seq.empty, connectionId = Some("e0d81be9-47ca-4e0b-b8a7-325e8c3abc2f"), + invitation = None, metaRetries = 5 ), PresentationStatus( @@ -102,6 +105,26 @@ object PresentationStatusPage { connectionId = Some("e0d81be9-47ca-4e0b-b8a7-325e8c3abc2f"), metaRetries = 5 ), + PresentationStatus( + presentationId = "938bfc23-f78d-4734-9bf3-6dccf300856f", + thid = "04112f4d-e894-4bff-a706-85b3e7190a2c", + role = "Verifier", + status = "InvitationGenerated", + proofs = Seq.empty, + data = Seq.empty, + connectionId = None, + myDid = Some("did:peer:veriferPeerDID1234567890"), + invitation = Some( + OOBPresentationInvitation( + id = UUID.fromString("04112f4d-e894-4bff-a706-85b3e7190a2c"), + `type` = "didcomm/aip2;rfc0048/invitation", + from = "did:peer:veriferPeerDID1234567890", + invitationUrl = + "http://localhost:8000/present-proof/invitation?_oob=eyJpZCI6ImU2M2JkNzQ1LWZjYzYtNGQ0My05NjgzLTY4MjUyOTNlYTgxNiIsInR5cGUiOiJodHRwczovL2RpZGNvbW0ub3JnL291dC1vZi1iYW5kLzIuMC9pbnZpdGF0aW9uIiwiZnJvbSI6ImRpZDpwZWVyOjIuRXo2TFNoOWFSQmRFQlV6WkFRSzN5VnFBRnRYS0pVMVZ1cUZlMVd1U1ZRcnRvRGROZi5WejZNa3NCWmZkc3U4UmFxWjNmdjlBdkJ0elVGd1VyaW5td0xRODFNVjVoc29td2JZLlNleUowSWpvaVpHMGlMQ0p6SWpwN0luVnlhU0k2SW1oMGRIQTZMeTh4T1RJdU1UWTRMakV1TVRrNU9qZ3dOekF2Wkdsa1kyOXRiU0lzSW5JaU9sdGRMQ0poSWpwYkltUnBaR052YlcwdmRqSWlYWDE5IiwiYm9keSI6eyJnb2FsX2NvZGUiOiJwcmVzZW50LXZwIiwiZ29hbCI6IlJlcXVlc3QgcHJvb2Ygb2YgdmFjY2luYXRpb24gaW5mb3JtYXRpb24iLCJhY2NlcHQiOltdfSwiYXR0YWNobWVudHMiOlt7ImlkIjoiZTE5ZjNkNmMtY2U2Ni00Y2EwLWI1ZWUtZDBiY2ZhOGI1MTc3IiwibWVkaWFfdHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJkYXRhIjp7Impzb24iOnsiaWQiOiIxYjMwYzRjZi05MmVjLTQwOTMtYWFlOC1hZDk3NmIzODljY2MiLCJ0eXBlIjoiaHR0cHM6Ly9kaWRjb21tLmF0YWxhcHJpc20uaW8vcHJlc2VudC1wcm9vZi8zLjAvcmVxdWVzdC1wcmVzZW50YXRpb24iLCJib2R5Ijp7ImdvYWxfY29kZSI6IlJlcXVlc3QgUHJvb2YgUHJlc2VudGF0aW9uIiwid2lsbF9jb25maXJtIjpmYWxzZSwicHJvb2ZfdHlwZXMiOltdfSwiYXR0YWNobWVudHMiOlt7ImlkIjoiNDBiZjcyNzUtMDNkNS00MjI1LWFlYjAtMzhhZDYyODhhMThkIiwibWVkaWFfdHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJkYXRhIjp7Impzb24iOnsib3B0aW9ucyI6eyJjaGFsbGVuZ2UiOiIxMWM5MTQ5My0wMWIzLTRjNGQtYWMzNi1iMzM2YmFiNWJkZGYiLCJkb21haW4iOiJodHRwczovL3ByaXNtLXZlcmlmaWVyLmNvbSJ9LCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IjkyODkyMjJmLWY3ZmItNDk4Yi1iMmE0LTNlODdiNzdiMzk5ZiIsImlucHV0X2Rlc2NyaXB0b3JzIjpbXX19fSwiZm9ybWF0IjoicHJpc20vand0In1dLCJ0aGlkIjoiZTYzYmQ3NDUtZmNjNi00ZDQzLTk2ODMtNjgyNTI5M2VhODE2IiwiZnJvbSI6ImRpZDpwZWVyOjIuRXo2TFNoOWFSQmRFQlV6WkFRSzN5VnFBRnRYS0pVMVZ1cUZlMVd1U1ZRcnRvRGROZi5WejZNa3NCWmZkc3U4UmFxWjNmdjlBdkJ0elVGd1VyaW5td0xRODFNVjVoc29td2JZLlNleUowSWpvaVpHMGlMQ0p6SWpwN0luVnlhU0k2SW1oMGRIQTZMeTh4T1RJdU1UWTRMakV1TVRrNU9qZ3dOekF2Wkdsa1kyOXRiU0lzSW5JaU9sdGRMQ0poSWpwYkltUnBaR052YlcwdmRqSWlYWDE5In19fV19" + ) + ), + metaRetries = 5 + ), ) ) } diff --git a/mercury/protocol-invitation/src/test/scala/org/hyperledger/identus/mercury/protocol/invitation/v2/OutOfBandSpec.scala b/mercury/protocol-invitation/src/test/scala/org/hyperledger/identus/mercury/protocol/invitation/v2/OutOfBandSpec.scala index 182dbacb43..60d0cec95c 100644 --- a/mercury/protocol-invitation/src/test/scala/org/hyperledger/identus/mercury/protocol/invitation/v2/OutOfBandSpec.scala +++ b/mercury/protocol-invitation/src/test/scala/org/hyperledger/identus/mercury/protocol/invitation/v2/OutOfBandSpec.scala @@ -23,7 +23,6 @@ class OutOfBandSpec extends FunSuite { ), Body(Some("request-mediate"), Some("RequestMediate"), Seq("didcomm/v2", "didcomm/aip2;env=rfc587")), ) - assertEquals(ret, Right(expected)) } diff --git a/mercury/protocol-present-proof/src/main/scala/org/hyperledger/identus/mercury/protocol/presentproof/PresentProofInvitation.scala b/mercury/protocol-present-proof/src/main/scala/org/hyperledger/identus/mercury/protocol/presentproof/PresentProofInvitation.scala new file mode 100644 index 0000000000..1418eb3410 --- /dev/null +++ b/mercury/protocol-present-proof/src/main/scala/org/hyperledger/identus/mercury/protocol/presentproof/PresentProofInvitation.scala @@ -0,0 +1,27 @@ +package org.hyperledger.identus.mercury.protocol.presentproof + +import org.hyperledger.identus.mercury.model.AttachmentDescriptor +import org.hyperledger.identus.mercury.model.DidId +import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation +object PresentProofInvitation { + def makeInvitation( + from: DidId, + goalCode: Option[String], + goal: Option[String], + invitationId: String, + requestPresentation: RequestPresentation + ): Invitation = { + val attachmentDescriptor = AttachmentDescriptor.buildJsonAttachment(payload = requestPresentation) + Invitation( + id = invitationId, + from = from, + body = Invitation.Body( + goal_code = goalCode, + goal = goal, + Nil + ), + attachments = Some(Seq(attachmentDescriptor)) + ) + } + +} diff --git a/mercury/protocol-present-proof/src/main/scala/org/hyperledger/identus/mercury/protocol/presentproof/RequestPresentation.scala b/mercury/protocol-present-proof/src/main/scala/org/hyperledger/identus/mercury/protocol/presentproof/RequestPresentation.scala index bc522377b5..1548d08800 100644 --- a/mercury/protocol-present-proof/src/main/scala/org/hyperledger/identus/mercury/protocol/presentproof/RequestPresentation.scala +++ b/mercury/protocol-present-proof/src/main/scala/org/hyperledger/identus/mercury/protocol/presentproof/RequestPresentation.scala @@ -12,15 +12,15 @@ final case class RequestPresentation( attachments: Seq[AttachmentDescriptor], // extra thid: Option[String] = None, - from: DidId, - to: DidId, + from: Option[DidId], + to: Option[DidId], ) { def makeMessage: Message = Message( id = this.id, `type` = this.`type`, - from = Some(this.from), - to = Seq(this.to), + from = this.from, + to = this.to.toSeq, thid = this.thid, body = this.body.asJson.asObject.get, // TODO get attachments = Some(this.attachments), @@ -64,9 +64,9 @@ object RequestPresentation { thid = Some(msg.id), from = { assert(msg.to.length == 1, "The recipient is ambiguous. Need to have only 1 recipient") // TODO return error - msg.to.head + msg.to.headOption }, - to = msg.from.get, // TODO get + to = msg.from, ) } @@ -79,10 +79,10 @@ object RequestPresentation { body = body, attachments = message.attachments.getOrElse(Seq.empty), thid = message.thid, - from = message.from.get, // TODO get + from = message.from, to = { assert(message.to.length == 1, "The recipient is ambiguous. Need to have only 1 recipient") // TODO return error - message.to.head + message.to.headOption }, ) diff --git a/mercury/protocol-present-proof/src/test/scala/org/hyperledger/identus/mercury/protocol/presentproof/RequestPresentationSpec.scala b/mercury/protocol-present-proof/src/test/scala/org/hyperledger/identus/mercury/protocol/presentproof/RequestPresentationSpec.scala index 7f8458fe99..a647cd96c4 100644 --- a/mercury/protocol-present-proof/src/test/scala/org/hyperledger/identus/mercury/protocol/presentproof/RequestPresentationSpec.scala +++ b/mercury/protocol-present-proof/src/test/scala/org/hyperledger/identus/mercury/protocol/presentproof/RequestPresentationSpec.scala @@ -34,8 +34,8 @@ class RequestCredentialSpec extends ZSuite { id = "061bf917-2cbe-460b-8d12-b1a9609505c2", body = body, attachments = Seq(attachmentDescriptor), - to = DidId("did:prism:test123"), - from = DidId("did:prism:test123"), + to = Some(DidId("did:prism:test123")), + from = Some(DidId("did:prism:test123")), ) val result = requestPresentation.asJson.deepDropNullValues diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/PresentationRecord.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/PresentationRecord.scala index 6847451cfd..d6fbaa972e 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/PresentationRecord.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/PresentationRecord.scala @@ -1,6 +1,7 @@ package org.hyperledger.identus.pollux.core.model import org.hyperledger.identus.mercury.model.DidId +import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation import org.hyperledger.identus.mercury.protocol.presentproof.{Presentation, ProposePresentation, RequestPresentation} import org.hyperledger.identus.shared.models.Failure @@ -18,9 +19,10 @@ final case class PresentationRecord( schemaId: Option[String], connectionId: Option[String], role: PresentationRecord.Role, - subjectId: DidId, + subjectId: DidId, // TODO Remove protocolState: PresentationRecord.ProtocolState, credentialFormat: CredentialFormat, + invitation: Option[Invitation], requestPresentationData: Option[RequestPresentation], proposePresentationData: Option[ProposePresentation], presentationData: Option[Presentation], @@ -90,4 +92,9 @@ object PresentationRecord { // Verifier has rejected the presentation (proof) (Verifier DB) case PresentationRejected extends ProtocolState // TODO send problem report + // Verifier has created a OOB Presentation request (in Verifier DB) + case InvitationGenerated extends ProtocolState + // Verifier receives a presentation from an expired OOB Presentation request (update Verifier DB) //TODO send problem report + case InvitationExpired extends ProtocolState + } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala index 7150fa8605..93acaf4ef4 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/PresentationError.scala @@ -121,6 +121,11 @@ object PresentationError { StatusCode.InternalServerError, error ) + final case class MissingSDJWTPresentationRequest(error: String) + extends PresentationError( + StatusCode.InternalServerError, + error + ) final case class NotMatchingPresentationCredentialFormat(cause: Throwable) extends PresentationError( @@ -199,4 +204,29 @@ object PresentationError { StatusCode.InternalServerError, msg ) + + final case class RequestPresentationDecodingError(msg: String) + extends PresentationError( + StatusCode.InternalServerError, + msg + ) + + final case class InvitationParsingError(cause: String) + extends PresentationError( + StatusCode.BadRequest, + cause + ) + + final case class InvitationAlreadyReceived(msg: String) + extends PresentationError( + StatusCode.BadRequest, + msg + ) + + final case class MissingInvitationAttachment(msg: String) + extends PresentationError( + StatusCode.BadRequest, + msg + ) + } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala index 95f7a91aa0..0a781ae851 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationService.scala @@ -6,6 +6,7 @@ import org.hyperledger.identus.pollux.anoncreds.AnoncredPresentation import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.core.model.error.PresentationError import org.hyperledger.identus.pollux.core.model.presentation.* +import org.hyperledger.identus.pollux.core.model.presentation.Options import org.hyperledger.identus.pollux.core.service.serdes.{AnoncredCredentialProofsV1, AnoncredPresentationRequestV1} import org.hyperledger.identus.pollux.sdjwt.{HolderPrivateKey, PresentationCompact} import org.hyperledger.identus.pollux.vc.jwt.* @@ -22,29 +23,35 @@ trait PresentationService { def createJwtPresentationRecord( pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, + pairwiseProverDID: Option[DidId], thid: DidCommID, connectionId: Option[String], proofTypes: Seq[ProofType], options: Option[org.hyperledger.identus.pollux.core.model.presentation.Options], + goalCode: Option[String], + goal: Option[String], ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] def createSDJWTPresentationRecord( pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, + pairwiseProverDID: Option[DidId], thid: DidCommID, connectionId: Option[String], proofTypes: Seq[ProofType], claimsToDisclose: ast.Json.Obj, options: Option[org.hyperledger.identus.pollux.core.model.presentation.Options], + goalCode: Option[String], + goal: Option[String], ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] def createAnoncredPresentationRecord( pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, + pairwiseProverDID: Option[DidId], thid: DidCommID, connectionId: Option[String], - presentationRequest: AnoncredPresentationRequestV1 + presentationRequest: AnoncredPresentationRequestV1, + goalCode: Option[String], + goal: Option[String], ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] def getPresentationRecords( @@ -171,4 +178,8 @@ trait PresentationService { failReason: Option[Failure] ): UIO[Unit] + def getRequestPresentationFromInvitation( + pairwiseProverDID: DidId, + invitation: String + ): ZIO[WalletAccessContext, PresentationError, RequestPresentation] } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala index bf23847f03..9178b2f688 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceImpl.scala @@ -6,6 +6,7 @@ import io.circe.* import io.circe.parser.* import io.circe.syntax.* import org.hyperledger.identus.mercury.model.* +import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation import org.hyperledger.identus.mercury.protocol.issuecredential.IssueCredentialIssuedFormat import org.hyperledger.identus.mercury.protocol.presentproof.* import org.hyperledger.identus.pollux.anoncreds.* @@ -20,6 +21,7 @@ import org.hyperledger.identus.pollux.sdjwt.{CredentialCompact, HolderPrivateKey import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.models.* import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect +import org.hyperledger.identus.shared.utils.Base64Utils import zio.* import zio.json.* @@ -195,8 +197,8 @@ private class PresentationServiceImpl( ) ), thid = requestPresentation.thid.orElse(Some(requestPresentation.id)), - from = requestPresentation.to, - to = requestPresentation.from + from = requestPresentation.to.getOrElse(throw RuntimeException(s"RequestPresentation to field is missing")), + to = requestPresentation.from.getOrElse(throw RuntimeException(s"RequestPresentation from field is missing")) ) ) } yield presentation @@ -266,8 +268,8 @@ private class PresentationServiceImpl( ) ), thid = requestPresentation.thid.orElse(Some(requestPresentation.id)), - from = requestPresentation.to, - to = requestPresentation.from + from = requestPresentation.to.getOrElse(throw RuntimeException(s"RequestPresentation to field is missing")), + to = requestPresentation.from.getOrElse(throw RuntimeException(s"RequestPresentation from field is missing")) ) ) } yield presentation @@ -303,11 +305,13 @@ private class PresentationServiceImpl( override def createJwtPresentationRecord( pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, + pairwiseProverDID: Option[DidId], thid: DidCommID, connectionId: Option[String], proofTypes: Seq[ProofType], - options: Option[org.hyperledger.identus.pollux.core.model.presentation.Options] + options: Option[org.hyperledger.identus.pollux.core.model.presentation.Options], + goalCode: Option[String] = None, + goal: Option[String] = None, ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { createPresentationRecord( pairwiseVerifierDID, @@ -316,18 +320,22 @@ private class PresentationServiceImpl( connectionId, CredentialFormat.JWT, proofTypes, - options.map(o => Seq(toJWTAttachment(o))).getOrElse(Seq.empty) + options.map(o => Seq(toJWTAttachment(o))).getOrElse(Seq.empty), + goalCode, + goal ) } override def createSDJWTPresentationRecord( pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, + pairwiseProverDID: Option[DidId], thid: DidCommID, connectionId: Option[String], proofTypes: Seq[ProofType], claimsToDisclose: ast.Json.Obj, options: Option[org.hyperledger.identus.pollux.core.model.presentation.Options], + goalCode: Option[String] = None, + goal: Option[String] = None, ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { createPresentationRecord( pairwiseVerifierDID, @@ -336,16 +344,20 @@ private class PresentationServiceImpl( connectionId, CredentialFormat.SDJWT, proofTypes, - attachments = Seq(toSDJWTAttachment(options, claimsToDisclose)) + attachments = Seq(toSDJWTAttachment(options, claimsToDisclose)), + goalCode, + goal ) } override def createAnoncredPresentationRecord( pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, + pairwiseProverDID: Option[DidId], thid: DidCommID, connectionId: Option[String], - presentationRequest: AnoncredPresentationRequestV1 + presentationRequest: AnoncredPresentationRequestV1, + goalCode: Option[String] = None, + goal: Option[String] = None, ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { createPresentationRecord( pairwiseVerifierDID, @@ -354,29 +366,45 @@ private class PresentationServiceImpl( connectionId, CredentialFormat.AnonCreds, Seq.empty, - Seq(toAnoncredAttachment(presentationRequest)) + Seq(toAnoncredAttachment(presentationRequest)), + goalCode, + goal ) } private def createPresentationRecord( pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, + pairwiseProverDID: Option[DidId], thid: DidCommID, connectionId: Option[String], format: CredentialFormat, proofTypes: Seq[ProofType], - attachments: Seq[AttachmentDescriptor] + attachments: Seq[AttachmentDescriptor], + goalCode: Option[String] = None, + goal: Option[String] = None, ) = { for { request <- ZIO.succeed( createDidCommRequestPresentation( proofTypes, thid, - pairwiseVerifierDID, + Some(pairwiseVerifierDID), pairwiseProverDID, attachments ) ) + invitation = connectionId.fold( + Some( + PresentProofInvitation.makeInvitation( + pairwiseVerifierDID, + goalCode, + goal, + thid.value, + request + ) + ) + )(_ => None) + record <- ZIO.succeed( PresentationRecord( id = DidCommID(), @@ -386,9 +414,12 @@ private class PresentationServiceImpl( connectionId = connectionId, schemaId = None, // TODO REMOVE from DB role = PresentationRecord.Role.Verifier, - subjectId = pairwiseProverDID, - protocolState = PresentationRecord.ProtocolState.RequestPending, + subjectId = pairwiseProverDID.getOrElse(DidId("TODO REMOVE subject did")), + protocolState = invitation.fold(PresentationRecord.ProtocolState.RequestPending)(_ => + PresentationRecord.ProtocolState.InvitationGenerated + ), credentialFormat = format, + invitation = invitation, requestPresentationData = Some(request), proposePresentationData = None, presentationData = None, @@ -463,9 +494,11 @@ private class PresentationServiceImpl( connectionId = connectionId, schemaId = None, role = Role.Prover, - subjectId = request.to, + subjectId = + request.to.getOrElse(throw RuntimeException(s"RequestPresentation from field is missing")), // TODO REMOVE protocolState = PresentationRecord.ProtocolState.RequestReceived, credentialFormat = format, + invitation = None, requestPresentationData = Some(request), proposePresentationData = None, presentationData = None, @@ -1147,8 +1180,8 @@ private class PresentationServiceImpl( private def createDidCommRequestPresentation( proofTypes: Seq[ProofType], thid: DidCommID, - pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, + pairwiseVerifierDID: Option[DidId], + pairwiseProverDID: Option[DidId], attachments: Seq[AttachmentDescriptor] ): RequestPresentation = { RequestPresentation( @@ -1172,8 +1205,8 @@ private class PresentationServiceImpl( RequestPresentation( body = body, attachments = proposePresentation.attachments, - from = proposePresentation.to, - to = proposePresentation.from, + from = Some(proposePresentation.to), + to = Some(proposePresentation.from), thid = proposePresentation.thid ) } @@ -1189,6 +1222,47 @@ private class PresentationServiceImpl( record <- getRecord(id) } yield record + override def getRequestPresentationFromInvitation( + pairwiseProverDID: DidId, + invitation: String + ): ZIO[WalletAccessContext, PresentationError, RequestPresentation] = { + for { + invitation <- ZIO + .fromEither(io.circe.parser.decode[Invitation](Base64Utils.decodeUrlToString(invitation))) + .mapError(err => InvitationParsingError(err.getMessage)) + _ <- presentationRepository + .findPresentationRecordByThreadId(DidCommID(invitation.id)) + .flatMap { + case None => ZIO.unit + case Some(_) => ZIO.fail(InvitationAlreadyReceived(invitation.id)) + } + requestPresentation <- ZIO.fromEither { + invitation.attachments + .flatMap( + _.headOption.map(attachment => + decode[org.hyperledger.identus.mercury.model.JsonData]( + attachment.data.asJson.noSpaces + ) // TODO Move mercury to use ZIO JSON + .flatMap { data => + RequestPresentation.given_Decoder_RequestPresentation + .decodeJson(data.json.asJson) + .map(r => r.copy(to = Some(pairwiseProverDID))) + .leftMap(err => + PresentationDecodingError( + s"RequestPresentation As Attachment decoding error: ${err.getMessage}" + ) + ) + } + .leftMap(err => PresentationDecodingError(s"Invitation Attachment JsonData decoding error: $err")) + ) + ) + .getOrElse( + Left(MissingInvitationAttachment("Missing Invitation Attachment for RequestPresentation")) + ) + } + } yield requestPresentation + + } } object PresentationServiceImpl { diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala index cc622522e9..955149f1df 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifier.scala @@ -31,11 +31,13 @@ class PresentationServiceNotifier( override def createJwtPresentationRecord( pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, + pairwiseProverDID: Option[DidId], thid: DidCommID, connectionId: Option[String], proofTypes: Seq[ProofType], options: Option[Options], + goalCode: Option[String], + goal: Option[String], ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = notifyOnSuccess( svc.createJwtPresentationRecord( @@ -44,18 +46,22 @@ class PresentationServiceNotifier( thid, connectionId, proofTypes, - options + options, + goalCode, + goal ) ) override def createSDJWTPresentationRecord( pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, + pairwiseProverDID: Option[DidId], thid: DidCommID, connectionId: Option[String], proofTypes: Seq[ProofType], claimsToDisclose: ast.Json.Obj, - options: Option[org.hyperledger.identus.pollux.core.model.presentation.Options] + options: Option[org.hyperledger.identus.pollux.core.model.presentation.Options], + goalCode: Option[String], + goal: Option[String], ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = notifyOnSuccess( svc.createSDJWTPresentationRecord( @@ -66,15 +72,19 @@ class PresentationServiceNotifier( proofTypes, claimsToDisclose, options, + goalCode, + goal ) ) def createAnoncredPresentationRecord( pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, + pairwiseProverDID: Option[DidId], thid: DidCommID, connectionId: Option[String], - presentationRequest: AnoncredPresentationRequestV1 + presentationRequest: AnoncredPresentationRequestV1, + goalCode: Option[String], + goal: Option[String] ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = notifyOnSuccess( svc.createAnoncredPresentationRecord( @@ -82,7 +92,9 @@ class PresentationServiceNotifier( pairwiseProverDID, thid, connectionId, - presentationRequest + presentationRequest, + goalCode, + goal ) ) @@ -282,6 +294,12 @@ class PresentationServiceNotifier( recordId: DidCommID, failReason: Option[Failure] ): UIO[Unit] = svc.reportProcessingFailure(recordId, failReason) + + override def getRequestPresentationFromInvitation( + pairwiseProverDID: DidId, + invitation: String + ): ZIO[WalletAccessContext, PresentationError, RequestPresentation] = + svc.getRequestPresentationFromInvitation(pairwiseProverDID, invitation) } object PresentationServiceNotifier { diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepositorySpecSuite.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepositorySpecSuite.scala index 13c270c149..0d1aab356c 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepositorySpecSuite.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepositorySpecSuite.scala @@ -27,6 +27,7 @@ object PresentationRepositorySpecSuite { subjectId = DidId("did:prism:aaa"), protocolState = PresentationRecord.ProtocolState.RequestPending, credentialFormat = CredentialFormat.JWT, + invitation = None, requestPresentationData = None, proposePresentationData = None, presentationData = None, @@ -41,8 +42,8 @@ object PresentationRepositorySpecSuite { ).withTruncatedTimestamp() private def requestPresentation = RequestPresentation( - from = DidId("did:prism:aaa"), - to = DidId("did:prism:bbb"), + from = Some(DidId("did:prism:aaa")), + to = Some(DidId("did:prism:bbb")), thid = Some(UUID.randomUUID.toString), body = RequestPresentation.Body(goal_code = Some("request Presentation")), attachments = Nil diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala index fd47d0d48e..bcfb0d95d7 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala @@ -26,20 +26,47 @@ object MockPresentationService extends Mock[PresentationService] { object CreateJwtPresentationRecord extends Effect[ - (DidId, DidId, DidCommID, Option[String], Seq[ProofType], Option[Options]), + ( + DidId, + Option[DidId], + DidCommID, + Option[String], + Seq[ProofType], + Option[Options], + Option[String], + Option[String] + ), PresentationError, PresentationRecord ] object CreateSDJWTPresentationRecord extends Effect[ - (DidId, DidId, DidCommID, Option[String], Seq[ProofType], ast.Json.Obj, Option[Options]), + ( + DidId, + Option[DidId], + DidCommID, + Option[String], + Seq[ProofType], + ast.Json.Obj, + Option[Options], + Option[String], + Option[String] + ), PresentationError, PresentationRecord ] object CreateAnoncredPresentationRecord extends Effect[ - (DidId, DidId, DidCommID, Option[String], AnoncredPresentationRequestV1), + ( + DidId, + Option[DidId], + DidCommID, + Option[String], + AnoncredPresentationRequestV1, + Option[String], + Option[String] + ), PresentationError, PresentationRecord ] @@ -60,6 +87,8 @@ object MockPresentationService extends Mock[PresentationService] { object AcceptRequestPresentation extends Effect[(DidCommID, Seq[String]), PresentationError, PresentationRecord] + object AcceptRequestPresentationInvitation extends Effect[(DidId, String), PresentationError, RequestPresentation] + object AcceptSDJWTRequestPresentation extends Effect[(DidCommID, Seq[String], Option[ast.Json.Obj]), PresentationError, PresentationRecord] @@ -90,41 +119,57 @@ object MockPresentationService extends Mock[PresentationService] { override def createJwtPresentationRecord( pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, + pairwiseProverDID: Option[DidId], thid: DidCommID, connectionId: Option[String], proofTypes: Seq[ProofType], - options: Option[Options] + options: Option[Options], + goalCode: Option[String], + goal: Option[String] ): IO[PresentationError, PresentationRecord] = proxy( CreateJwtPresentationRecord, - (pairwiseVerifierDID, pairwiseProverDID, thid, connectionId, proofTypes, options) + (pairwiseVerifierDID, pairwiseProverDID, thid, connectionId, proofTypes, options, goalCode, goal) ) override def createSDJWTPresentationRecord( pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, + pairwiseProverDID: Option[DidId], thid: DidCommID, connectionId: Option[String], proofTypes: Seq[ProofType], claimsToDisclose: ast.Json.Obj, options: Option[org.hyperledger.identus.pollux.core.model.presentation.Options], + goalCode: Option[String], + goal: Option[String] ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = proxy( CreateSDJWTPresentationRecord, - (pairwiseVerifierDID, pairwiseProverDID, thid, connectionId, proofTypes, claimsToDisclose, options) + ( + pairwiseVerifierDID, + pairwiseProverDID, + thid, + connectionId, + proofTypes, + claimsToDisclose, + options, + goalCode, + goal + ) ) override def createAnoncredPresentationRecord( pairwiseVerifierDID: DidId, - pairwiseProverDID: DidId, + pairwiseProverDID: Option[DidId], thid: DidCommID, connectionId: Option[String], - presentationRequest: AnoncredPresentationRequestV1 + presentationRequest: AnoncredPresentationRequestV1, + goalCode: Option[String], + goal: Option[String] ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { proxy( CreateAnoncredPresentationRecord, - (pairwiseVerifierDID, pairwiseProverDID, thid, connectionId, presentationRequest) + (pairwiseVerifierDID, pairwiseProverDID, thid, connectionId, presentationRequest, goalCode, goal) ) } @@ -264,6 +309,11 @@ object MockPresentationService extends Mock[PresentationService] { failReason: Option[Failure] ): UIO[Unit] = ??? + override def getRequestPresentationFromInvitation( + pairwiseProverDID: DidId, + invitation: String + ): IO[PresentationError, RequestPresentation] = + proxy(AcceptRequestPresentationInvitation, (pairwiseProverDID, invitation)) } } diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifierSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifierSpec.scala index 608e92305b..9eef4b8372 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifierSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceNotifierSpec.scala @@ -37,6 +37,7 @@ object PresentationServiceNotifierSpec extends ZIOSpecDefault with PresentationS None, None, None, + None, 5, None, None @@ -109,10 +110,12 @@ object PresentationServiceNotifierSpec extends ZIOSpecDefault with PresentationS record <- svc.createJwtPresentationRecord( DidId(""), - DidId(""), + Some(DidId("")), DidCommID(""), None, Seq.empty, + None, + None, None ) _ <- svc.markRequestPresentationSent(record.id) diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpec.scala index 7acb44110e..b135946e1d 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpec.scala @@ -60,7 +60,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp check( Gen.uuid.map(e => DidCommID(e.toString)), - Gen.option(Gen.string), + Gen.string, Gen.listOfBounded(1, 5)(proofTypeGen), Gen.option(optionsGen) ) { (thid, connectionId, proofTypes, options) => @@ -70,20 +70,22 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp pairwiseProverDid = DidId("did:peer:Prover") record <- svc.createJwtPresentationRecord( pairwiseVerifierDid, - pairwiseProverDid, + Some(pairwiseProverDid), thid, - connectionId, + Some(connectionId), proofTypes, - options + options, + None, + None ) } yield { assertTrue(record.thid == thid) && assertTrue(record.updatedAt.isEmpty) && - assertTrue(record.connectionId == connectionId) && + assertTrue(record.connectionId.contains(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.to.contains(pairwiseProverDid)) && assertTrue(record.requestPresentationData.get.thid.contains(thid.toString)) && assertTrue(record.requestPresentationData.get.body.goal_code.contains("Request Proof Presentation")) && assertTrue(record.requestPresentationData.get.body.proof_types == proofTypes) && @@ -116,7 +118,7 @@ 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, Gen.string @@ -136,19 +138,21 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp record <- svc.createAnoncredPresentationRecord( pairwiseVerifierDid, - pairwiseProverDid, + Some(pairwiseProverDid), thid, - connectionId, - anoncredPresentationRequestV1 + Some(connectionId), + anoncredPresentationRequestV1, + None, + None ) } yield { assertTrue(record.thid == thid) && assertTrue(record.updatedAt.isEmpty) && - assertTrue(record.connectionId == connectionId) && + assertTrue(record.connectionId.contains(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.to.contains(pairwiseProverDid)) && assertTrue(record.requestPresentationData.get.thid.contains(thid.toString)) && assertTrue(record.requestPresentationData.get.body.goal_code.contains("Request Proof Presentation")) && assertTrue( @@ -322,8 +326,8 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp "domain": "us.gov/DriverLicense", "credential_manifest": {} }""" - prover = DidId("did:peer:Prover") - verifier = DidId("did:peer:Verifier") + prover = Some(DidId("did:peer:Prover")) + verifier = Some(DidId("did:peer:Verifier")) attachmentDescriptor = AttachmentDescriptor.buildJsonAttachment( payload = presentationAttachmentAsJson, @@ -350,8 +354,8 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp "domain": "us.gov/DriverLicense", "credential_manifest": {} }""" - prover = DidId("did:peer:Prover") - verifier = DidId("did:peer:Verifier") + prover = Some(DidId("did:peer:Prover")) + verifier = Some(DidId("did:peer:Verifier")) attachmentDescriptor = AttachmentDescriptor.buildJsonAttachment( payload = presentationAttachmentAsJson, @@ -379,8 +383,8 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp "domain": "us.gov/DriverLicense", "credential_manifest": {} }""" - prover = DidId("did:peer:Prover") - verifier = DidId("did:peer:Verifier") + prover = Some(DidId("did:peer:Prover")) + verifier = Some(DidId("did:peer:Verifier")) attachmentDescriptor = AttachmentDescriptor.buildJsonAttachment( payload = presentationAttachmentAsJson, @@ -536,8 +540,8 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp requestPresentation = RequestPresentation( body = RequestPresentation.Body(goal_code = Some("Presentation Request")), attachments = Seq(attachmentDescriptor), - to = DidId("did:peer:Prover"), - from = DidId("did:peer:Verifier"), + to = Some(DidId("did:peer:Prover")), + from = Some(DidId("did:peer:Verifier")), ) aRecord <- svc.receiveRequestPresentation(connectionId, requestPresentation) credentialsToUse = @@ -598,8 +602,8 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp requestPresentation = RequestPresentation( body = RequestPresentation.Body(goal_code = Some("Presentation Request")), attachments = Seq(attachmentDescriptor), - to = DidId("did:peer:Prover"), - from = DidId("did:peer:Verifier"), + to = Some(DidId("did:peer:Prover")), + from = Some(DidId("did:peer:Verifier")), ) aRecord <- svc.receiveRequestPresentation(connectionId, requestPresentation) credentialsToUse = Seq(aIssueCredentialRecord.id.value) @@ -772,8 +776,8 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp svc <- ZIO.service[PresentationService] connectionId = Some("connectionId") body = RequestPresentation.Body(goal_code = Some("Presentation Request")) - prover = DidId("did:peer:Prover") - verifier = DidId("did:peer:Verifier") + prover = Some(DidId("did:peer:Prover")) + verifier = Some(DidId("did:peer:Verifier")) attachmentDescriptor = attachment requestPresentation = RequestPresentation( body = body, diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala index 0c356aeba6..701852d856 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/PresentationServiceSpecHelper.scala @@ -72,8 +72,8 @@ trait PresentationServiceSpecHelper { "domain": "us.gov/DriverLicense", "credential_manifest": {} }""" - val prover = DidId("did:peer:Prover") - val verifier = DidId("did:peer:Verifier") + val prover = Some(DidId("did:peer:Prover")) + val verifier = Some(DidId("did:peer:Verifier")) val attachmentDescriptor = AttachmentDescriptor.buildJsonAttachment( payload = presentationAttachmentAsJson, @@ -165,10 +165,12 @@ trait PresentationServiceSpecHelper { svc.createJwtPresentationRecord( thid = thid, pairwiseVerifierDID = pairwiseVerifierDID, - pairwiseProverDID = pairwiseProverDID, + pairwiseProverDID = Some(pairwiseProverDID), connectionId = Some("connectionId"), proofTypes = Seq(proofType), - options = options + options = options, + goalCode = None, + goal = None ) } @@ -208,9 +210,11 @@ trait PresentationServiceSpecHelper { svc.createAnoncredPresentationRecord( thid = thid, pairwiseVerifierDID = pairwiseVerifierDID, - pairwiseProverDID = pairwiseProverDID, + pairwiseProverDID = Some(pairwiseProverDID), connectionId = Some("connectionId"), - anoncredPresentationRequestV1 + anoncredPresentationRequestV1, + goalCode = None, + goal = None ) } } diff --git a/pollux/sql-doobie/src/main/resources/sql/pollux/V24__add_invitation_column_presentation_record.sql b/pollux/sql-doobie/src/main/resources/sql/pollux/V24__add_invitation_column_presentation_record.sql new file mode 100644 index 0000000000..f5b7600651 --- /dev/null +++ b/pollux/sql-doobie/src/main/resources/sql/pollux/V24__add_invitation_column_presentation_record.sql @@ -0,0 +1,4 @@ +-- presentation_records +-- Introduce new field invitation for connection-less presentation +ALTER TABLE public.presentation_records + ADD COLUMN "invitation" TEXT; \ No newline at end of file diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala index 57a22d2ee8..6df0a78f6d 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala @@ -11,6 +11,7 @@ import io.circe import io.circe.* import io.circe.parser.* import io.circe.syntax.* +import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation import org.hyperledger.identus.mercury.protocol.presentproof.* import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.core.repository.PresentationRepository @@ -164,6 +165,9 @@ class JdbcPresentationRepository( given failureGet: Get[Failure] = Get[String].temap(_.fromJson[FailureInfo]) given failurePut: Put[Failure] = Put[String].contramap(_.asFailureInfo.toJson) + given invitationGet: Get[Invitation] = Get[String].map(decode[Invitation](_).getOrElse(???)) + given invitationPut: Put[Invitation] = Put[String].contramap(_.asJson.toString) + override def createPresentationRecord(record: PresentationRecord): URIO[WalletAccessContext, Unit] = { val cxnIO = sql""" | INSERT INTO public.presentation_records( @@ -177,6 +181,7 @@ class JdbcPresentationRepository( | subject_id, | protocol_state, | credential_format, + | invitation, | request_presentation_data, | credentials_to_use, | anoncred_credentials_to_use_json_schema_id, @@ -198,6 +203,7 @@ class JdbcPresentationRepository( | ${record.subjectId}, | ${record.protocolState}, | ${record.credentialFormat}, + | ${record.invitation}, | ${record.requestPresentationData}, | ${record.credentialsToUse.map(_.toList)}, | ${record.anoncredCredentialsToUseJsonSchemaId}, @@ -235,6 +241,7 @@ class JdbcPresentationRepository( | subject_id, | protocol_state, | credential_format, + | invitation, | request_presentation_data, | propose_presentation_data, | presentation_data, @@ -286,6 +293,7 @@ class JdbcPresentationRepository( | subject_id, | protocol_state, | credential_format, + | invitation, | request_presentation_data, | propose_presentation_data, | presentation_data, @@ -334,6 +342,7 @@ class JdbcPresentationRepository( | subject_id, | protocol_state, | credential_format, + | invitation, | request_presentation_data, | propose_presentation_data, | presentation_data, @@ -371,6 +380,7 @@ class JdbcPresentationRepository( | subject_id, | protocol_state, | credential_format, + | invitation, | request_presentation_data, | propose_presentation_data, | presentation_data,