diff --git a/build.sbt b/build.sbt index 1e369601c1..e95c354731 100644 --- a/build.sbt +++ b/build.sbt @@ -142,6 +142,7 @@ lazy val D = new { "com.github.dasniko" % "testcontainers-keycloak" % V.testContainersJavaKeycloak % Test val doobiePostgres: ModuleID = "org.tpolecat" %% "doobie-postgres" % V.doobie + val doobiePostgresCirce: ModuleID = "org.tpolecat" %% "doobie-postgres-circe" % V.doobie val doobieHikari: ModuleID = "org.tpolecat" %% "doobie-hikari" % V.doobie val flyway: ModuleID = "org.flywaydb" % "flyway-core" % V.flyway @@ -160,7 +161,7 @@ lazy val D = new { // LIST of Dependencies val doobieDependencies: Seq[ModuleID] = - Seq(doobiePostgres, doobieHikari, flyway) + Seq(doobiePostgres, doobiePostgresCirce, doobieHikari, flyway) } lazy val D_Shared = new { diff --git a/docs/docusaurus/credentials/present-proof.md b/docs/docusaurus/credentials/present-proof.md index 4509eac59b..9469c57af1 100644 --- a/docs/docusaurus/credentials/present-proof.md +++ b/docs/docusaurus/credentials/present-proof.md @@ -1,3 +1,6 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + # Present proof The [Present Proof Protocol](/docs/concepts/glossary#present-proof-protocol) allows: @@ -57,6 +60,9 @@ To do this, he makes a `POST` request to the [`/present-proof/presentations`](/a 1. `connectionId`: This field represents the unique identifier of an existing connection between the verifier and the Holder/prover. It is for exchanging messages related to the protocol flow execution. 2. `challenge` and `domain`: The Verifier provides the random seed challenge and operational domain, and the Holder/Prover must sign the generated proof to protect from replay attacks. + + + ```bash curl -X 'POST' 'http://localhost:8070/prism-agent/present-proof/presentations' \ -H 'accept: application/json' \ @@ -72,6 +78,56 @@ curl -X 'POST' 'http://localhost:8070/prism-agent/present-proof/presentations' \ }' ``` + + + +```bash +curl -X 'POST' 'http://localhost:8070/prism-agent/present-proof/presentations' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -H "apikey: $API_KEY" \ + -d '{ + "connectionId": "872ddfa9-4115-46c2-8a1b-22c24c7431d7", + "anoncredPresentationRequest": { + "requested_attributes": { + "attribute1": { + "name": "Attribute 1", + "restrictions": [ + { + "cred_def_id": "credential_definition_id_of_attribute1" + } + ], + "non_revoked": { + "from": 1635734400, + "to": 1735734400 + } + } + }, + "requested_predicates": { + "predicate1": { + "name": "age", + "p_type": ">=", + "p_value": 18, + "restrictions": [ + { + "schema_id": "schema_id_of_predicate1" + } + ], + "non_revoked": { + "from": 1635734400 + } + } + }, + "name": "Example Presentation Request", + "nonce": "1234567890", + "version": "1.0" + }, + "credentialFormat": "AnonCreds" + }' +``` + + + Upon execution, a new presentation request record gets created with an initial state of `RequestPending`. The Verifier PRISM Agent will send the presentation request message to the PRISM Agent of the Holder/Prover through the specified DIDComm connection. The record state then is updated to `RequestSent`. The Verifier can retrieve the list of presentation records by making a `GET` request to the [`/present-proof/presentations`](/agent-api/#tag/Present-Proof/operation/getAllPresentation) endpoint: @@ -121,6 +177,9 @@ curl -X 'GET' 'http://localhost:8090/prism-agent/present-proof/presentations' \ The Holder/Prover can then accept a specific request, generate the proof, and send it to the Verifier PRISM Agent by making a `PATCH` request to the [`/present-proof/presentations/{id}`](/agent-api/#tag/Present-Proof/operation/updatePresentation) endpoint: + + + ```bash curl -X 'PATCH' 'http://localhost:8090/prism-agent/present-proof/presentations/{PRESENTATION_ID}' \ -H 'Content-Type: application/json' \ @@ -133,8 +192,39 @@ curl -X 'PATCH' 'http://localhost:8090/prism-agent/present-proof/presentations/{ The Holder/Prover will have to provide the following information: 1. `presentationId`: The unique identifier of the presentation record to accept. -2. `proofId`: The unique identifier of the verifiable credential record to use as proof. +2. `proofId`: The unique identifier of the verifiable credential record to use as proof. + + + + +```bash +curl -X 'PATCH' 'http://localhost:8090/prism-agent/present-proof/presentations/{PRESENTATION_ID}' \ + -H 'Content-Type: application/json' \ + -H "apikey: $API_KEY" \ + -d '{ + "action": "request-accept", + "anoncredPresentationRequest":{ + "credentialProofs":[ + { + "credential":"3e849b98-f0fd-4cb4-ae96-9ea527a76267", + "requestedAttribute":[ + "age" + ], + "requestedPredicate":[ + "age" + ] + } + ] + } + }' +``` + + +The Holder/Prover will have to provide the following information: +1. `presentationId`: The unique identifier of the presentation record to accept. +2. `anoncredPresentationRequest`: A list of credential unique identifier with the attribute and predicate the credential is answering for. + The record state is updated to `PresentationPending` and processed by the Holder/Prover PRISM Agent. The agent will automatically generate the proof presentation, change the state to `PresentationGenerated`, and will eventually send it to the Verifier Agent, and change the state to `PresentationSent`. ```mermaid @@ -152,4 +242,20 @@ stateDiagram-v2 The following diagram shows the end-to-end flow for a verifier to request and verify a proof presentation from a Holder/prover. -![](present-proof-flow.png) \ No newline at end of file +### JWT Present Proof Flow Diagram +![](present-proof-flow.png) +### Anoncreds Present Proof Flow Diagram +![](anoncreds-present-proof-flow.png) + + + + +![](present-proof-flow.png) + + + + +![](anoncreds-present-proof-flow.png) + + + \ No newline at end of file diff --git a/pollux/lib/anoncreds/src/main/scala/io/iohk/atala/pollux/anoncreds/AnoncredLib.scala b/pollux/lib/anoncreds/src/main/scala/io/iohk/atala/pollux/anoncreds/AnoncredLib.scala index 9bd681714c..5d6b3c7cf4 100644 --- a/pollux/lib/anoncreds/src/main/scala/io/iohk/atala/pollux/anoncreds/AnoncredLib.scala +++ b/pollux/lib/anoncreds/src/main/scala/io/iohk/atala/pollux/anoncreds/AnoncredLib.scala @@ -18,16 +18,16 @@ object AnoncredLib { version: String, // SCHEMA_Version attr_names: AttributeNames, issuer_id: IssuerId, // ISSUER_DID - ): SchemaDef = uniffi.anoncreds_wrapper.Schema.apply(name, version, attr_names.toSeq.asJava, issuer_id) + ): AnoncredSchemaDef = uniffi.anoncreds_wrapper.Schema.apply(name, version, attr_names.toSeq.asJava, issuer_id) // issuer def createCredDefinition( issuer_id: String, - schema: SchemaDef, + schema: AnoncredSchemaDef, tag: String, supportRevocation: Boolean, signature_type: uniffi.anoncreds_wrapper.SignatureType.CL.type = uniffi.anoncreds_wrapper.SignatureType.CL - ) = { + ): AnoncredCreateCredentialDefinition = { val credentialDefinition: uniffi.anoncreds_wrapper.IssuerCreateCredentialDefinitionReturn = uniffi.anoncreds_wrapper .Issuer() @@ -40,7 +40,7 @@ object AnoncredLib { uniffi.anoncreds_wrapper.CredentialDefinitionConfig(supportRevocation) ) - CreateCredentialDefinition( + AnoncredCreateCredentialDefinition( credentialDefinition.getCredentialDefinition(), credentialDefinition.getCredentialDefinitionPrivate(), credentialDefinition.getCredentialKeyCorrectnessProof() @@ -49,9 +49,9 @@ object AnoncredLib { // issuer def createOffer( - credentialDefinition: CreateCredentialDefinition, + credentialDefinition: AnoncredCreateCredentialDefinition, credentialDefinitionId: String - ): CredentialOffer = + ): AnoncredCredentialOffer = uniffi.anoncreds_wrapper .Issuer() .createCredentialOffer( @@ -62,15 +62,15 @@ object AnoncredLib { // holder def createCredentialRequest( - linkSecret: LinkSecretWithId, - credentialDefinition: CredentialDefinition, - credentialOffer: CredentialOffer, + linkSecret: AnoncredLinkSecretWithId, + credentialDefinition: AnoncredCredentialDefinition, + credentialOffer: AnoncredCredentialOffer, entropy: String = { val tmp = scala.util.Random() tmp.setSeed(java.security.SecureRandom.getInstanceStrong().nextLong()) tmp.nextString(80) } - ): CreateCrendentialRequest = { + ): AnoncredCreateCrendentialRequest = { val credentialRequest = uniffi.anoncreds_wrapper .Prover() @@ -83,16 +83,16 @@ object AnoncredLib { credentialOffer, // CredentialOffer credential_offer ) - CreateCrendentialRequest(credentialRequest.getRequest(), credentialRequest.getMetadata()) + AnoncredCreateCrendentialRequest(credentialRequest.getRequest(), credentialRequest.getMetadata()) } // holder def processCredential( - credential: Credential, - metadata: CredentialRequestMetadata, - linkSecret: LinkSecretWithId, - credentialDefinition: CredentialDefinition, - ): Credential = { + credential: AnoncredCredential, + metadata: AnoncredCredentialRequestMetadata, + linkSecret: AnoncredLinkSecretWithId, + credentialDefinition: AnoncredCredentialDefinition, + ): AnoncredCredential = { uniffi.anoncreds_wrapper .Prover() .processCredential( @@ -106,16 +106,16 @@ object AnoncredLib { // issuer def createCredential( - credentialDefinition: CredentialDefinition, - credentialDefinitionPrivate: CredentialDefinitionPrivate, - credentialOffer: CredentialOffer, - credentialRequest: CredentialRequest, + credentialDefinition: AnoncredCredentialDefinition, + credentialDefinitionPrivate: AnoncredCredentialDefinitionPrivate, + credentialOffer: AnoncredCredentialOffer, + credentialRequest: AnoncredCredentialRequest, attributeValues: Seq[(String, String)] // java.util.List[AttributeValues] : java.util.List[AttributeValues] // revocationRegistryId : String // revocationStatusList : RevocationStatusList // credentialRevocationConfig : CredentialRevocationConfig - ): Credential = { + ): AnoncredCredential = { uniffi.anoncreds_wrapper .Issuer() .createCredential( @@ -140,13 +140,13 @@ object AnoncredLib { // [info] Caused by: Predicate is not satisfied def createPresentation( - presentationRequest: PresentationRequest, - credentialRequests: Seq[CredentialAndRequestedAttributesPredicates], + presentationRequest: AnoncredPresentationRequest, + credentialRequests: Seq[AnoncredCredentialRequests], selfAttested: Map[String, String], - linkSecret: LinkSecret, - schemas: Map[SchemaId, SchemaDef], - credentialDefinitions: Map[CredentialDefinitionId, CredentialDefinition], - ): Either[uniffi.anoncreds_wrapper.AnoncredsException.CreatePresentationException, Presentation] = { + linkSecret: AnoncredLinkSecret, + schemas: Map[SchemaId, AnoncredSchemaDef], + credentialDefinitions: Map[CredentialDefinitionId, AnoncredCredentialDefinition], + ): Either[uniffi.anoncreds_wrapper.AnoncredsException.CreatePresentationException, AnoncredPresentation] = { try { Right( uniffi.anoncreds_wrapper @@ -181,10 +181,10 @@ object AnoncredLib { // FIXME its always return false .... def verifyPresentation( - presentation: Presentation, - presentationRequest: PresentationRequest, - schemas: Map[SchemaId, SchemaDef], - credentialDefinitions: Map[CredentialDefinitionId, CredentialDefinition], + presentation: AnoncredPresentation, + presentationRequest: AnoncredPresentationRequest, + schemas: Map[SchemaId, AnoncredSchemaDef], + credentialDefinitions: Map[CredentialDefinitionId, AnoncredCredentialDefinition], ): Boolean = { uniffi.anoncreds_wrapper .Verifier() diff --git a/pollux/lib/anoncreds/src/main/scala/io/iohk/atala/pollux/anoncreds/Models.scala b/pollux/lib/anoncreds/src/main/scala/io/iohk/atala/pollux/anoncreds/Models.scala index 0324c89601..38f9d4014e 100644 --- a/pollux/lib/anoncreds/src/main/scala/io/iohk/atala/pollux/anoncreds/Models.scala +++ b/pollux/lib/anoncreds/src/main/scala/io/iohk/atala/pollux/anoncreds/Models.scala @@ -3,17 +3,17 @@ package io.iohk.atala.pollux.anoncreds import uniffi.anoncreds_wrapper.{ Nonce, Credential as UniffiCredential, - CredentialRequests as UniffiCredentialRequests, CredentialDefinition as UniffiCredentialDefinition, CredentialDefinitionPrivate as UniffiCredentialDefinitionPrivate, CredentialKeyCorrectnessProof as UniffiCredentialKeyCorrectnessProof, CredentialOffer as UniffiCredentialOffer, CredentialRequest as UniffiCredentialRequest, CredentialRequestMetadata as UniffiCredentialRequestMetadata, + CredentialRequests as UniffiCredentialRequests, LinkSecret as UniffiLinkSecret, - Schema as UniffiSchema, Presentation as UniffiPresentation, PresentationRequest as UniffiPresentationRequest, + Schema as UniffiSchema } import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} @@ -21,41 +21,42 @@ import scala.jdk.CollectionConverters.* type AttributeNames = Set[String] type IssuerId = String -case class LinkSecretWithId(id: String, secret: LinkSecret) { def data = secret.data } -object LinkSecretWithId { - def apply(id: String): LinkSecretWithId = LinkSecretWithId(id, LinkSecret()) +case class AnoncredLinkSecretWithId(id: String, secret: AnoncredLinkSecret) { def data = secret.data } +object AnoncredLinkSecretWithId { + def apply(id: String): AnoncredLinkSecretWithId = AnoncredLinkSecretWithId(id, AnoncredLinkSecret()) } -case class LinkSecret(data: String) -object LinkSecret { +case class AnoncredLinkSecret(data: String) +object AnoncredLinkSecret { - def apply(): LinkSecret = LinkSecret.given_Conversion_UniffiLinkSecret_LinkSecret(UniffiLinkSecret()) + def apply(): AnoncredLinkSecret = + AnoncredLinkSecret.given_Conversion_UniffiLinkSecret_AnoncredLinkSecret(UniffiLinkSecret()) - given Conversion[LinkSecret, UniffiLinkSecret] with { - def apply(linkSecret: LinkSecret): UniffiLinkSecret = + given Conversion[AnoncredLinkSecret, UniffiLinkSecret] with { + def apply(linkSecret: AnoncredLinkSecret): UniffiLinkSecret = UniffiLinkSecret.Companion.newFromValue(linkSecret.data) } - given Conversion[UniffiLinkSecret, LinkSecret] with { - def apply(uniffiLinkSecret: UniffiLinkSecret): LinkSecret = - LinkSecret.apply(uniffiLinkSecret.getValue()) + given Conversion[UniffiLinkSecret, AnoncredLinkSecret] with { + def apply(uniffiLinkSecret: UniffiLinkSecret): AnoncredLinkSecret = + AnoncredLinkSecret.apply(uniffiLinkSecret.getValue()) } } //FIXME use same names as in https://hyperledger.github.io/anoncreds-spec/#term:schemas -case class SchemaDef( +case class AnoncredSchemaDef( name: String, // SCHEMA_ID version: String, // SCHEMA_Version attributes: AttributeNames, issuer_id: IssuerId, // ISSUER_DID ) -object SchemaDef { +object AnoncredSchemaDef { - given Conversion[SchemaDef, UniffiSchema] with { - def apply(schemaDef: SchemaDef): UniffiSchema = + given Conversion[AnoncredSchemaDef, UniffiSchema] with { + def apply(schemaDef: AnoncredSchemaDef): UniffiSchema = UniffiSchema.apply( schemaDef.name, schemaDef.version, @@ -65,9 +66,9 @@ object SchemaDef { } - given Conversion[UniffiSchema, SchemaDef] with { - def apply(schema: UniffiSchema): SchemaDef = - SchemaDef.apply( + given Conversion[UniffiSchema, AnoncredSchemaDef] with { + def apply(schema: UniffiSchema): AnoncredSchemaDef = + AnoncredSchemaDef.apply( name = schema.getName(), version = schema.getVersion(), attributes = schema.getAttrNames().asScala.toSet, @@ -106,20 +107,20 @@ object SchemaDef { // value: String, // issuerId: String, // ) -case class CredentialDefinition(data: String) { // TODO - def schemaId = CredentialDefinition - .given_Conversion_CredentialDefinition_UniffiCredentialDefinition(this) +case class AnoncredCredentialDefinition(data: String) { // TODO + def schemaId = AnoncredCredentialDefinition + .given_Conversion_AnoncredCredentialDefinition_UniffiCredentialDefinition(this) .getSchemaId() } -object CredentialDefinition { - given Conversion[CredentialDefinition, UniffiCredentialDefinition] with { - def apply(credentialDefinition: CredentialDefinition): UniffiCredentialDefinition = +object AnoncredCredentialDefinition { + given Conversion[AnoncredCredentialDefinition, UniffiCredentialDefinition] with { + def apply(credentialDefinition: AnoncredCredentialDefinition): UniffiCredentialDefinition = UniffiCredentialDefinition(credentialDefinition.data) } - given Conversion[UniffiCredentialDefinition, CredentialDefinition] with { - def apply(credentialDefinition: UniffiCredentialDefinition): CredentialDefinition = - CredentialDefinition(credentialDefinition.getJson()) + given Conversion[UniffiCredentialDefinition, AnoncredCredentialDefinition] with { + def apply(credentialDefinition: UniffiCredentialDefinition): AnoncredCredentialDefinition = + AnoncredCredentialDefinition(credentialDefinition.getJson()) } } @@ -134,90 +135,94 @@ object CredentialDefinition { // "r_key": null // } // } -case class CredentialDefinitionPrivate(data: String) -object CredentialDefinitionPrivate { - given Conversion[CredentialDefinitionPrivate, UniffiCredentialDefinitionPrivate] with { - def apply(credentialDefinitionPrivate: CredentialDefinitionPrivate): UniffiCredentialDefinitionPrivate = +case class AnoncredCredentialDefinitionPrivate(data: String) +object AnoncredCredentialDefinitionPrivate { + given Conversion[AnoncredCredentialDefinitionPrivate, UniffiCredentialDefinitionPrivate] with { + def apply(credentialDefinitionPrivate: AnoncredCredentialDefinitionPrivate): UniffiCredentialDefinitionPrivate = UniffiCredentialDefinitionPrivate(credentialDefinitionPrivate.data) } - given Conversion[UniffiCredentialDefinitionPrivate, CredentialDefinitionPrivate] with { - def apply(credentialDefinitionPrivate: UniffiCredentialDefinitionPrivate): CredentialDefinitionPrivate = - CredentialDefinitionPrivate(credentialDefinitionPrivate.getJson()) + given Conversion[UniffiCredentialDefinitionPrivate, AnoncredCredentialDefinitionPrivate] with { + def apply(credentialDefinitionPrivate: UniffiCredentialDefinitionPrivate): AnoncredCredentialDefinitionPrivate = + AnoncredCredentialDefinitionPrivate(credentialDefinitionPrivate.getJson()) } } // **************************************************************************** -case class CredentialKeyCorrectnessProof(data: String) -object CredentialKeyCorrectnessProof { - given Conversion[CredentialKeyCorrectnessProof, UniffiCredentialKeyCorrectnessProof] with { - def apply(credentialKeyCorrectnessProof: CredentialKeyCorrectnessProof): UniffiCredentialKeyCorrectnessProof = +case class AnoncredCredentialKeyCorrectnessProof(data: String) +object AnoncredCredentialKeyCorrectnessProof { + given Conversion[AnoncredCredentialKeyCorrectnessProof, UniffiCredentialKeyCorrectnessProof] with { + def apply( + credentialKeyCorrectnessProof: AnoncredCredentialKeyCorrectnessProof + ): UniffiCredentialKeyCorrectnessProof = UniffiCredentialKeyCorrectnessProof(credentialKeyCorrectnessProof.data) } - given Conversion[UniffiCredentialKeyCorrectnessProof, CredentialKeyCorrectnessProof] with { - def apply(credentialKeyCorrectnessProof: UniffiCredentialKeyCorrectnessProof): CredentialKeyCorrectnessProof = - CredentialKeyCorrectnessProof(credentialKeyCorrectnessProof.getJson()) + given Conversion[UniffiCredentialKeyCorrectnessProof, AnoncredCredentialKeyCorrectnessProof] with { + def apply( + credentialKeyCorrectnessProof: UniffiCredentialKeyCorrectnessProof + ): AnoncredCredentialKeyCorrectnessProof = + AnoncredCredentialKeyCorrectnessProof(credentialKeyCorrectnessProof.getJson()) } } -case class CreateCredentialDefinition( - cd: CredentialDefinition, - cdPrivate: CredentialDefinitionPrivate, - proofKey: CredentialKeyCorrectnessProof, +case class AnoncredCreateCredentialDefinition( + cd: AnoncredCredentialDefinition, + cdPrivate: AnoncredCredentialDefinitionPrivate, + proofKey: AnoncredCredentialKeyCorrectnessProof, ) // **************************************************************************** -case class CredentialOffer(data: String) { - lazy val schemaId = CredentialOffer - .given_Conversion_CredentialOffer_UniffiCredentialOffer(this) +case class AnoncredCredentialOffer(data: String) { + lazy val schemaId = AnoncredCredentialOffer + .given_Conversion_AnoncredCredentialOffer_UniffiCredentialOffer(this) .getSchemaId() - lazy val credDefId = CredentialOffer - .given_Conversion_CredentialOffer_UniffiCredentialOffer(this) + lazy val credDefId = AnoncredCredentialOffer + .given_Conversion_AnoncredCredentialOffer_UniffiCredentialOffer(this) .getCredDefId() } -object CredentialOffer { - given Conversion[CredentialOffer, UniffiCredentialOffer] with { - def apply(credentialOffer: CredentialOffer): UniffiCredentialOffer = +object AnoncredCredentialOffer { + given Conversion[AnoncredCredentialOffer, UniffiCredentialOffer] with { + def apply(credentialOffer: AnoncredCredentialOffer): UniffiCredentialOffer = UniffiCredentialOffer(credentialOffer.data) } - given Conversion[UniffiCredentialOffer, CredentialOffer] with { - def apply(credentialOffer: UniffiCredentialOffer): CredentialOffer = - CredentialOffer(credentialOffer.getJson()) + given Conversion[UniffiCredentialOffer, AnoncredCredentialOffer] with { + def apply(credentialOffer: UniffiCredentialOffer): AnoncredCredentialOffer = + AnoncredCredentialOffer(credentialOffer.getJson()) } } // **************************************************************************** -case class CreateCrendentialRequest( - request: CredentialRequest, - metadata: CredentialRequestMetadata +case class AnoncredCreateCrendentialRequest( + request: AnoncredCredentialRequest, + metadata: AnoncredCredentialRequestMetadata ) -case class CredentialRequest(data: String) -object CredentialRequest { +case class AnoncredCredentialRequest(data: String) +object AnoncredCredentialRequest { - given Conversion[CredentialRequest, UniffiCredentialRequest] with { - def apply(credentialRequest: CredentialRequest): UniffiCredentialRequest = + given Conversion[AnoncredCredentialRequest, UniffiCredentialRequest] with { + def apply(credentialRequest: AnoncredCredentialRequest): UniffiCredentialRequest = UniffiCredentialRequest(credentialRequest.data) } - given Conversion[UniffiCredentialRequest, CredentialRequest] with { - def apply(credentialRequest: UniffiCredentialRequest): CredentialRequest = - CredentialRequest(credentialRequest.getJson()) + given Conversion[UniffiCredentialRequest, AnoncredCredentialRequest] with { + def apply(credentialRequest: UniffiCredentialRequest): AnoncredCredentialRequest = + AnoncredCredentialRequest(credentialRequest.getJson()) } } -case class CredentialRequestMetadata( +case class AnoncredCredentialRequestMetadata( linkSecretBlinding: String, nonce: String, linkSecretName: String, ) -object CredentialRequestMetadata { - given Conversion[CredentialRequestMetadata, UniffiCredentialRequestMetadata] with { - def apply(credentialRequestMetadata: CredentialRequestMetadata): UniffiCredentialRequestMetadata = +object AnoncredCredentialRequestMetadata { + given Conversion[AnoncredCredentialRequestMetadata, UniffiCredentialRequestMetadata] with { + def apply(credentialRequestMetadata: AnoncredCredentialRequestMetadata): UniffiCredentialRequestMetadata = UniffiCredentialRequestMetadata( /*link_secret_blinding_data*/ credentialRequestMetadata.linkSecretBlinding, /*nonce*/ Nonce.Companion.newFromValue(credentialRequestMetadata.nonce), @@ -225,62 +230,63 @@ object CredentialRequestMetadata { ) } - given Conversion[UniffiCredentialRequestMetadata, CredentialRequestMetadata] with { - def apply(credentialRequestMetadata: UniffiCredentialRequestMetadata): CredentialRequestMetadata = - CredentialRequestMetadata( + given Conversion[UniffiCredentialRequestMetadata, AnoncredCredentialRequestMetadata] with { + def apply(credentialRequestMetadata: UniffiCredentialRequestMetadata): AnoncredCredentialRequestMetadata = + AnoncredCredentialRequestMetadata( linkSecretBlinding = credentialRequestMetadata.getLinkSecretBlindingData(), nonce = credentialRequestMetadata.getNonce().getValue(), linkSecretName = credentialRequestMetadata.getLinkSecretName(), ) } - given JsonDecoder[CredentialRequestMetadata] = DeriveJsonDecoder.gen[CredentialRequestMetadata] - given JsonEncoder[CredentialRequestMetadata] = DeriveJsonEncoder.gen[CredentialRequestMetadata] + given JsonDecoder[AnoncredCredentialRequestMetadata] = DeriveJsonDecoder.gen[AnoncredCredentialRequestMetadata] + given JsonEncoder[AnoncredCredentialRequestMetadata] = DeriveJsonEncoder.gen[AnoncredCredentialRequestMetadata] } // **************************************************************************** //Credential -case class Credential(data: String) { - lazy val credDefId: String = Credential - .given_Conversion_Credential_UniffiCredential(this) +case class AnoncredCredential(data: String) { + lazy val credDefId: String = AnoncredCredential + .given_Conversion_AnoncredCredential_UniffiCredential(this) .getCredDefId } -object Credential { - given Conversion[Credential, UniffiCredential] with { - def apply(credential: Credential): UniffiCredential = +object AnoncredCredential { + given Conversion[AnoncredCredential, UniffiCredential] with { + def apply(credential: AnoncredCredential): UniffiCredential = UniffiCredential(credential.data) } - given Conversion[UniffiCredential, Credential] with { - def apply(credential: UniffiCredential): Credential = - Credential(credential.getJson()) + given Conversion[UniffiCredential, AnoncredCredential] with { + def apply(credential: UniffiCredential): AnoncredCredential = + AnoncredCredential(credential.getJson()) } } // **************************************************************************** -case class CredentialAndRequestedAttributesPredicates( - credential: Credential, +case class AnoncredCredentialRequests( + credential: AnoncredCredential, requestedAttribute: Seq[String], requestedPredicate: Seq[String], ) -object CredentialAndRequestedAttributesPredicates { - given Conversion[CredentialAndRequestedAttributesPredicates, UniffiCredentialRequests] with { +object AnoncredCredentialRequests { + given Conversion[AnoncredCredentialRequests, UniffiCredentialRequests] with { import uniffi.anoncreds_wrapper.RequestedAttribute import uniffi.anoncreds_wrapper.RequestedPredicate - def apply(credentialRequests: CredentialAndRequestedAttributesPredicates): UniffiCredentialRequests = { - val credential = Credential.given_Conversion_Credential_UniffiCredential(credentialRequests.credential) + def apply(credentialRequests: AnoncredCredentialRequests): UniffiCredentialRequests = { + val credential = + AnoncredCredential.given_Conversion_AnoncredCredential_UniffiCredential(credentialRequests.credential) val requestedAttributes = credentialRequests.requestedAttribute.map(a => RequestedAttribute(a, true)) val requestedPredicates = credentialRequests.requestedPredicate.map(p => RequestedPredicate(p)) UniffiCredentialRequests(credential, requestedAttributes.asJava, requestedPredicates.asJava) } } - given Conversion[UniffiCredentialRequests, CredentialAndRequestedAttributesPredicates] with { - def apply(credentialRequests: UniffiCredentialRequests): CredentialAndRequestedAttributesPredicates = { - CredentialAndRequestedAttributesPredicates( - Credential.given_Conversion_UniffiCredential_Credential(credentialRequests.getCredential()), + given Conversion[UniffiCredentialRequests, AnoncredCredentialRequests] with { + def apply(credentialRequests: UniffiCredentialRequests): AnoncredCredentialRequests = { + AnoncredCredentialRequests( + AnoncredCredential.given_Conversion_UniffiCredential_AnoncredCredential(credentialRequests.getCredential()), credentialRequests .getRequestedAttribute() .asScala @@ -301,32 +307,32 @@ object CredentialAndRequestedAttributesPredicates { // **************************************************************************** -case class PresentationRequest(data: String) -object PresentationRequest { - given Conversion[PresentationRequest, UniffiPresentationRequest] with { - def apply(presentationRequest: PresentationRequest): UniffiPresentationRequest = +case class AnoncredPresentationRequest(data: String) +object AnoncredPresentationRequest { + given Conversion[AnoncredPresentationRequest, UniffiPresentationRequest] with { + def apply(presentationRequest: AnoncredPresentationRequest): UniffiPresentationRequest = UniffiPresentationRequest(presentationRequest.data) } - given Conversion[UniffiPresentationRequest, PresentationRequest] with { - def apply(presentationRequest: UniffiPresentationRequest): PresentationRequest = - PresentationRequest(presentationRequest.getJson()) + given Conversion[UniffiPresentationRequest, AnoncredPresentationRequest] with { + def apply(presentationRequest: UniffiPresentationRequest): AnoncredPresentationRequest = + AnoncredPresentationRequest(presentationRequest.getJson()) } } // **************************************************************************** -case class Presentation(data: String) -object Presentation { - given Conversion[Presentation, UniffiPresentation] with { - def apply(presentation: Presentation): UniffiPresentation = { +case class AnoncredPresentation(data: String) +object AnoncredPresentation { + given Conversion[AnoncredPresentation, UniffiPresentation] with { + def apply(presentation: AnoncredPresentation): UniffiPresentation = { UniffiPresentation(presentation.data) } } - given Conversion[UniffiPresentation, Presentation] with { - def apply(presentation: UniffiPresentation): Presentation = { - Presentation(presentation.getJson()) + given Conversion[UniffiPresentation, AnoncredPresentation] with { + def apply(presentation: UniffiPresentation): AnoncredPresentation = { + AnoncredPresentation(presentation.getJson()) } } } diff --git a/pollux/lib/anoncredsTest/src/test/scala/io/iohk/atala/pollux/anoncreds/PoCNewLib.scala b/pollux/lib/anoncredsTest/src/test/scala/io/iohk/atala/pollux/anoncreds/PoCNewLib.scala index 7172e28ea7..fd75b61a4b 100644 --- a/pollux/lib/anoncredsTest/src/test/scala/io/iohk/atala/pollux/anoncreds/PoCNewLib.scala +++ b/pollux/lib/anoncredsTest/src/test/scala/io/iohk/atala/pollux/anoncreds/PoCNewLib.scala @@ -15,13 +15,13 @@ class PoCNewLib extends AnyFlatSpec { "LinkSecret" should "be able to parse back to the anoncreds lib" in { import scala.language.implicitConversions - val ls1 = LinkSecret("65965334953670062552662719679603258895632947953618378932199361160021795698890") + val ls1 = AnoncredLinkSecret("65965334953670062552662719679603258895632947953618378932199361160021795698890") val ls1p = ls1: uniffi.anoncreds_wrapper.LinkSecret assert(ls1p.getValue() == "65965334953670062552662719679603258895632947953618378932199361160021795698890") - val ls0 = LinkSecret() + val ls0 = AnoncredLinkSecret() val ls0p = ls0: uniffi.anoncreds_wrapper.LinkSecret - val ls0_ = ls0p: LinkSecret + val ls0_ = ls0p: AnoncredLinkSecret assert(ls0.data == ls0_.data) } @@ -58,8 +58,8 @@ class PoCNewLib extends AnyFlatSpec { // ############## println("*** holder " + ("*" * 100)) - val ls1 = LinkSecret("65965334953670062552662719679603258895632947953618378932199361160021795698890") - val linkSecret = LinkSecretWithId("ID_of_some_secret_1", ls1) + val ls1 = AnoncredLinkSecret("65965334953670062552662719679603258895632947953618378932199361160021795698890") + val linkSecret = AnoncredLinkSecretWithId("ID_of_some_secret_1", ls1) val credentialRequest = AnoncredLib.createCredentialRequest(linkSecret, credentialDefinition.cd, credentialOffer) println("*" * 100) @@ -89,7 +89,7 @@ class PoCNewLib extends AnyFlatSpec { // ############## // TODO READ about PresentationRequest https://hyperledger.github.io/anoncreds-spec/#create-presentation-request - val presentationRequest = PresentationRequest( + val presentationRequest = AnoncredPresentationRequest( s"""{ "nonce": "1103253414365527824079144", "name":"proof_req_1", @@ -109,7 +109,7 @@ class PoCNewLib extends AnyFlatSpec { val presentation = AnoncredLib.createPresentation( presentationRequest, // : PresentationRequest, Seq( - CredentialAndRequestedAttributesPredicates(processedCredential, Seq("sex"), Seq("age")) + AnoncredCredentialRequests(processedCredential, Seq("sex"), Seq("age")) ), // credentials: Seq[Credential], Map(), // selfAttested: Map[String, String], linkSecret.secret, // linkSecret: LinkSecret, diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/IssueCredentialRecord.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/IssueCredentialRecord.scala index a232e39848..c803f3421f 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/IssueCredentialRecord.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/IssueCredentialRecord.scala @@ -9,7 +9,7 @@ import io.iohk.atala.mercury.protocol.issuecredential.{ OfferCredential, RequestCredential } -import io.iohk.atala.pollux.anoncreds.CredentialRequestMetadata +import io.iohk.atala.pollux.anoncreds.AnoncredCredentialRequestMetadata import io.iohk.atala.pollux.core.model.IssueCredentialRecord.* import java.time.Instant @@ -21,8 +21,9 @@ final case class IssueCredentialRecord( createdAt: Instant, updatedAt: Option[Instant], thid: DidCommID, - schemaId: Option[String], + schemaUri: Option[String], credentialDefinitionId: Option[UUID], + credentialDefinitionUri: Option[String], credentialFormat: CredentialFormat, role: Role, subjectId: Option[String], @@ -31,7 +32,7 @@ final case class IssueCredentialRecord( protocolState: ProtocolState, offerCredentialData: Option[OfferCredential], requestCredentialData: Option[RequestCredential], - anonCredsRequestMetadata: Option[CredentialRequestMetadata], + anonCredsRequestMetadata: Option[AnoncredCredentialRequestMetadata], issueCredentialData: Option[IssueCredential], issuedCredentialRaw: Option[String], issuingDID: Option[CanonicalPrismDID], @@ -69,6 +70,16 @@ final case class IssueCredentialRecord( final case class ValidIssuedCredentialRecord( id: DidCommID, issuedCredentialRaw: Option[String], + credentialFormat: CredentialFormat, + subjectId: Option[String] +) + +final case class ValidFullIssuedCredentialRecord( + id: DidCommID, + issuedCredential: Option[IssueCredential], + credentialFormat: CredentialFormat, + schemaUri: Option[String], + credentialDefinitionUri: Option[String], subjectId: Option[String] ) diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/PresentationRecord.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/PresentationRecord.scala index 60f35f11ae..7d0c5f2ef9 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/PresentationRecord.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/PresentationRecord.scala @@ -1,13 +1,13 @@ package io.iohk.atala.pollux.core.model -import io.iohk.atala.mercury.protocol.presentproof.ProposePresentation -import io.iohk.atala.mercury.protocol.presentproof.RequestPresentation -import io.iohk.atala.mercury.protocol.presentproof.Presentation import io.iohk.atala.mercury.model.DidId +import io.iohk.atala.mercury.protocol.presentproof.{Presentation, ProposePresentation, RequestPresentation} import java.time.Instant import java.time.temporal.ChronoUnit +type AnoncredCredentialProofs = zio.json.ast.Json + final case class PresentationRecord( id: DidCommID, createdAt: Instant, @@ -23,6 +23,8 @@ final case class PresentationRecord( proposePresentationData: Option[ProposePresentation], presentationData: Option[Presentation], credentialsToUse: Option[List[String]], + anoncredCredentialsToUseJsonSchemaId: Option[String], + anoncredCredentialsToUse: Option[AnoncredCredentialProofs], metaRetries: Int, metaNextRetry: Option[Instant], metaLastFailure: Option[String], 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 064d6fa9d3..12176583fd 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 @@ -11,6 +11,7 @@ object PresentationError { final case class InvalidFlowStateError(msg: String) extends PresentationError final case class UnexpectedError(msg: String) extends PresentationError final case class IssuedCredentialNotFoundError(cause: Throwable) extends PresentationError + final case class NotMatchingPresentationCredentialFormat(cause: Throwable) extends PresentationError final case class PresentationDecodingError(cause: Throwable) extends PresentationError final case class PresentationNotFoundError(cause: Throwable) extends PresentationError final case class HolderBindingError(msg: String) extends PresentationError @@ -18,4 +19,9 @@ object PresentationError { object MissingCredentialFormat extends PresentationError final case class UnsupportedCredentialFormat(vcFormat: String) extends PresentationError + final case class InvalidAnoncredPresentationRequest(error: String) extends PresentationError + final case class InvalidAnoncredPresentation(error: String) extends PresentationError + final case class MissingAnoncredPresentationRequest(error: String) extends PresentationError + final case class AnoncredPresentationCreationError(cause: Throwable) extends PresentationError + final case class AnoncredPresentationVerificationError(cause: Throwable) extends PresentationError } diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/CredentialDefinition.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/CredentialDefinition.scala index 554d36aab0..98823ed3f5 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/CredentialDefinition.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/CredentialDefinition.scala @@ -8,6 +8,7 @@ import zio.json.* import java.time.OffsetDateTime import java.time.ZoneOffset import java.util.UUID +import scala.util.Try type Definition = zio.json.ast.Json type CorrectnessProof = zio.json.ast.Json @@ -58,7 +59,7 @@ case class CredentialDefinition( signatureType: String, supportRevocation: Boolean ) { - def longId = CredentialDefinition.makeLongId(author, id, version) + def longId = CredentialDefinition.makeLongId(author, guid, version) } object CredentialDefinition { @@ -69,6 +70,17 @@ object CredentialDefinition { def makeGUID(author: String, id: UUID, version: String) = UUID.nameUUIDFromBytes(makeLongId(author, id, version).getBytes) + def extractGUID(longId: String): Option[UUID] = { + longId.split("/") match { + case Array(_, idWithVersion) => + idWithVersion.split("\\?") match { + case Array(id, _) => Try(UUID.fromString(id)).toOption + case _ => None + } + case _ => None + } + } + def make( in: Input, definitionSchemaId: String, diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/CredentialSchema.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/CredentialSchema.scala index ea77c80792..6c05b941cb 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/CredentialSchema.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/CredentialSchema.scala @@ -149,10 +149,11 @@ object CredentialSchema { for { uri <- ZIO.attempt(new URI(schemaId)).mapError(t => URISyntaxError(t.getMessage)) content <- uriDereferencer.dereference(uri).mapError(err => UnexpectedError(err.toString)) - validAttrNames <- ZIO - .fromEither(content.fromJson[AnoncredSchemaSerDesV1]) - .mapError(error => CredentialSchemaParsingError(s"AnonCreds Schema parsing error: $error")) - .map(_.attrNames) + validAttrNames <- + AnoncredSchemaSerDesV1.schemaSerDes + .deserialize(content) + .mapError(error => CredentialSchemaParsingError(s"AnonCreds Schema parsing error: $error")) + .map(_.attrNames) jsonClaims <- ZIO.fromEither(claims.fromJson[Json]).mapError(err => UnexpectedError(err)) _ <- jsonClaims match case Json.Obj(fields) => 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..365f8a0251 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,24 @@ 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 serializeToJsonString(instance: S)(using encoder: JsonEncoder[S]): String = { + instance.toJson + } + + def serialize(instance: S)(using encoder: JsonEncoder[S]): Either[String, Json] = { + instance.toJsonAST + } + def deserialize( schema: zio.json.ast.Json )(using decoder: JsonDecoder[S]): IO[JsonSchemaError, S] = { @@ -42,12 +48,12 @@ class SchemaSerDes[S](jsonSchemaSchemaStr: String) { } yield json } - def validate(jsonString: String): IO[JsonSchemaError, Boolean] = { + def validate(jsonString: String): IO[JsonSchemaError, Unit] = { for { jsonSchemaSchema <- JsonSchemaUtils.jsonSchema(jsonSchemaSchemaStr) schemaValidator = JsonSchemaValidatorImpl(jsonSchemaSchema) - _ <- schemaValidator.validate(jsonString) - } yield true + result <- schemaValidator.validate(jsonString) + } yield result } } diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/CredentialRepository.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/CredentialRepository.scala index 593f2e7725..2f2b9615e5 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/CredentialRepository.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/CredentialRepository.scala @@ -1,7 +1,7 @@ package io.iohk.atala.pollux.core.repository import io.iohk.atala.mercury.protocol.issuecredential.{IssueCredential, RequestCredential} -import io.iohk.atala.pollux.anoncreds.CredentialRequestMetadata +import io.iohk.atala.pollux.anoncreds.AnoncredCredentialRequestMetadata import io.iohk.atala.pollux.core.model.* import io.iohk.atala.pollux.core.model.IssueCredentialRecord.ProtocolState import io.iohk.atala.shared.models.WalletAccessContext @@ -53,7 +53,7 @@ trait CredentialRepository { def updateWithAnonCredsRequestCredential( recordId: DidCommID, request: RequestCredential, - metadata: CredentialRequestMetadata, + metadata: AnoncredCredentialRequestMetadata, protocolState: ProtocolState ): RIO[WalletAccessContext, Int] @@ -74,6 +74,10 @@ trait CredentialRepository { def getValidIssuedCredentials(recordId: Seq[DidCommID]): RIO[WalletAccessContext, Seq[ValidIssuedCredentialRecord]] + def getValidAnoncredIssuedCredentials( + recordIds: Seq[DidCommID] + ): RIO[WalletAccessContext, Seq[ValidFullIssuedCredentialRecord]] + def updateAfterFail( recordId: DidCommID, failReason: Option[String] diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/CredentialRepositoryInMemory.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/CredentialRepositoryInMemory.scala index f69e3b94e4..2bc9bce4c5 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/CredentialRepositoryInMemory.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/CredentialRepositoryInMemory.scala @@ -1,7 +1,7 @@ package io.iohk.atala.pollux.core.repository import io.iohk.atala.mercury.protocol.issuecredential.{IssueCredential, RequestCredential} -import io.iohk.atala.pollux.anoncreds.CredentialRequestMetadata +import io.iohk.atala.pollux.anoncreds.AnoncredCredentialRequestMetadata import io.iohk.atala.pollux.core.model.* import io.iohk.atala.pollux.core.model.IssueCredentialRecord.ProtocolState import io.iohk.atala.pollux.core.model.error.CredentialRepositoryError.* @@ -133,7 +133,35 @@ class CredentialRepositoryInMemory( store <- storeRef.get } yield store.values .filter(rec => recordId.contains(rec.id) && rec.issuedCredentialRaw.isDefined) - .map(rec => ValidIssuedCredentialRecord(rec.id, rec.issuedCredentialRaw, rec.subjectId)) + .map(rec => ValidIssuedCredentialRecord(rec.id, rec.issuedCredentialRaw, rec.credentialFormat, rec.subjectId)) + .toSeq + } + + override def getValidAnoncredIssuedCredentials( + recordId: Seq[DidCommID] + ): RIO[WalletAccessContext, Seq[ValidFullIssuedCredentialRecord]] = { + for { + storeRef <- walletStoreRef + store <- storeRef.get + } yield store.values + .filter(rec => + recordId.contains( + rec.id + ) && rec.issueCredentialData.isDefined + && rec.schemaUri.isDefined + && rec.credentialDefinitionId.isDefined + && rec.credentialFormat == CredentialFormat.AnonCreds + ) + .map(rec => + ValidFullIssuedCredentialRecord( + rec.id, + rec.issueCredentialData, + rec.credentialFormat, + rec.schemaUri, + rec.credentialDefinitionUri, + rec.subjectId + ) + ) .toSeq } @@ -287,7 +315,7 @@ class CredentialRepositoryInMemory( override def updateWithAnonCredsRequestCredential( recordId: DidCommID, request: RequestCredential, - metadata: CredentialRequestMetadata, + metadata: AnoncredCredentialRequestMetadata, protocolState: ProtocolState ): RIO[WalletAccessContext, RuntimeFlags] = { for { diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/PresentationRepository.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/PresentationRepository.scala index 506a5b6e66..5f361ac2fa 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/PresentationRepository.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/PresentationRepository.scala @@ -51,6 +51,13 @@ trait PresentationRepository { protocolState: ProtocolState ): RIO[WalletAccessContext, Int] + def updateAnoncredPresentationWithCredentialsToUse( + recordId: DidCommID, + anoncredCredentialsToUseJsonSchemaId: Option[String], + anoncredCredentialsToUse: Option[AnoncredCredentialProofs], + protocolState: ProtocolState + ): RIO[WalletAccessContext, Int] + def updateAfterFail( recordId: DidCommID, failReason: Option[String] diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/PresentationRepositoryInMemory.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/PresentationRepositoryInMemory.scala index d4a663d218..76cb0f0154 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/PresentationRepositoryInMemory.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/repository/PresentationRepositoryInMemory.scala @@ -1,11 +1,10 @@ package io.iohk.atala.pollux.core.repository -import io.iohk.atala.mercury.protocol.presentproof._ +import io.iohk.atala.mercury.protocol.presentproof.* +import io.iohk.atala.pollux.core.model.* import io.iohk.atala.pollux.core.model.PresentationRecord.ProtocolState -import io.iohk.atala.pollux.core.model._ -import io.iohk.atala.pollux.core.model.error.PresentationError._ -import io.iohk.atala.shared.models.WalletAccessContext -import io.iohk.atala.shared.models.WalletId +import io.iohk.atala.pollux.core.model.error.PresentationError.* +import io.iohk.atala.shared.models.{WalletAccessContext, WalletId} import zio.* import java.time.Instant @@ -114,6 +113,37 @@ class PresentationRepositoryInMemory( } yield count } + def updateAnoncredPresentationWithCredentialsToUse( + recordId: DidCommID, + anoncredCredentialsToUseJsonSchemaId: Option[String], + anoncredCredentialsToUse: Option[AnoncredCredentialProofs], + protocolState: ProtocolState + ): RIO[WalletAccessContext, Int] = { + for { + storeRef <- walletStoreRef + maybeRecord <- getPresentationRecord(recordId) + count <- maybeRecord + .map(record => + for { + _ <- storeRef.update(r => + r.updated( + recordId, + record.copy( + updatedAt = Some(Instant.now), + anoncredCredentialsToUseJsonSchemaId = anoncredCredentialsToUseJsonSchemaId, + anoncredCredentialsToUse = anoncredCredentialsToUse, + protocolState = protocolState, + metaRetries = maxRetries, + metaLastFailure = None, + ) + ) + ) + } yield 1 + ) + .getOrElse(ZIO.succeed(0)) + } yield count + } + override def updateWithPresentation( recordId: DidCommID, presentation: Presentation, diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialDefinitionServiceImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialDefinitionServiceImpl.scala index a7ec38bb81..01ea444476 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialDefinitionServiceImpl.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialDefinitionServiceImpl.scala @@ -2,9 +2,9 @@ package io.iohk.atala.pollux.core.service import io.iohk.atala.agent.walletapi.storage import io.iohk.atala.agent.walletapi.storage.GenericSecretStorage -import io.iohk.atala.pollux.anoncreds.{AnoncredLib, SchemaDef} +import io.iohk.atala.pollux.anoncreds.{AnoncredLib, AnoncredSchemaDef} import io.iohk.atala.pollux.core.model.error.CredentialSchemaError -import io.iohk.atala.pollux.core.model.error.CredentialSchemaError.URISyntaxError +import io.iohk.atala.pollux.core.model.error.CredentialSchemaError.{SchemaError, URISyntaxError} import io.iohk.atala.pollux.core.model.schema.CredentialDefinition import io.iohk.atala.pollux.core.model.schema.CredentialDefinition.{Filter, FilteredEntries} import io.iohk.atala.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1 @@ -37,7 +37,7 @@ class CredentialDefinitionServiceImpl( content <- uriDereferencer.dereference(uri) anoncredSchema <- AnoncredSchemaSerDesV1.schemaSerDes.deserialize(content) anoncredLibSchema = - SchemaDef( + AnoncredSchemaDef( in.schemaId, anoncredSchema.version, anoncredSchema.attrNames, @@ -84,12 +84,10 @@ class CredentialDefinitionServiceImpl( ) } yield createdCredentialDefinition }.mapError { - case e: CredentialDefinitionCreationError => e - case j: JsonSchemaError => UnexpectedError(j.error) - case s: URISyntaxError => UnexpectedError(s.message) - case u: URIDereferencerError => UnexpectedError(u.error) - case e: CredentialSchemaError => CredentialDefinitionValidationError(e) + case u: URIDereferencerError => CredentialDefinitionValidationError(URISyntaxError(u.error)) + case j: JsonSchemaError => CredentialDefinitionValidationError(SchemaError(j)) case t: Throwable => RepositoryError(t) + case e: CredentialDefinitionCreationError => e } override def delete(guid: UUID): Result[CredentialDefinition] = diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialService.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialService.scala index 0af8d97b01..85e453ce08 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialService.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialService.scala @@ -32,10 +32,10 @@ trait CredentialService { pairwiseHolderDID: DidId, thid: DidCommID, credentialDefinitionGUID: UUID, + credentialDefinitionId: String, claims: io.circe.Json, validityPeriod: Option[Double] = None, - automaticIssuance: Option[Boolean], - credentialDefinitionId: String + automaticIssuance: Option[Boolean] ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] /** Return a list of records as well as a count of all filtered items */ diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceImpl.scala index a580e0bdfb..9b2034ce81 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceImpl.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceImpl.scala @@ -10,7 +10,12 @@ import io.iohk.atala.castor.core.service.DIDService import io.iohk.atala.mercury.model.* import io.iohk.atala.mercury.protocol.issuecredential.* import io.iohk.atala.pollux.* -import io.iohk.atala.pollux.anoncreds.{AnoncredLib, CreateCredentialDefinition, CredentialOffer} +import io.iohk.atala.pollux.anoncreds.{ + AnoncredCreateCredentialDefinition, + AnoncredCredential, + AnoncredCredentialOffer, + AnoncredLib +} import io.iohk.atala.pollux.core.model.* import io.iohk.atala.pollux.core.model.CredentialFormat.AnonCreds import io.iohk.atala.pollux.core.model.IssueCredentialRecord.ProtocolState.OfferReceived @@ -153,8 +158,9 @@ private class CredentialServiceImpl( createdAt = Instant.now, updatedAt = None, thid = thid, - schemaId = maybeSchemaId, + schemaUri = maybeSchemaId, credentialDefinitionId = None, + credentialDefinitionUri = None, credentialFormat = CredentialFormat.JWT, role = IssueCredentialRecord.Role.Issuer, subjectId = None, @@ -188,10 +194,10 @@ private class CredentialServiceImpl( pairwiseHolderDID: DidId, thid: DidCommID, credentialDefinitionGUID: UUID, + credentialDefinitionId: String, claims: Json, validityPeriod: Option[Double], - automaticIssuance: Option[Boolean], - credentialDefinitionId: String + automaticIssuance: Option[Boolean] ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = { for { credentialDefinition <- credentialDefinitionService @@ -204,11 +210,11 @@ private class CredentialServiceImpl( offer <- createAnonCredsDidCommOfferCredential( pairwiseIssuerDID = pairwiseIssuerDID, pairwiseHolderDID = pairwiseHolderDID, - schemaId = credentialDefinition.schemaId, + schemaUri = credentialDefinition.schemaId, credentialDefinitionGUID = credentialDefinitionGUID, + credentialDefinitionId = credentialDefinitionId, claims = attributes, thid = thid, - credentialDefinitionId ) record <- ZIO.succeed( IssueCredentialRecord( @@ -216,8 +222,9 @@ private class CredentialServiceImpl( createdAt = Instant.now, updatedAt = None, thid = thid, - schemaId = Some(credentialDefinition.schemaId), + schemaUri = Some(credentialDefinition.schemaId), credentialDefinitionId = Some(credentialDefinitionGUID), + credentialDefinitionUri = Some(credentialDefinitionId), credentialFormat = CredentialFormat.AnonCreds, role = IssueCredentialRecord.Role.Issuer, subjectId = None, @@ -292,8 +299,9 @@ private class CredentialServiceImpl( createdAt = Instant.now, updatedAt = None, thid = DidCommID(offer.thid.getOrElse(offer.id)), - schemaId = None, + schemaUri = None, credentialDefinitionId = None, + credentialDefinitionUri = None, credentialFormat = credentialFormat, role = Role.Holder, subjectId = None, @@ -345,7 +353,7 @@ private class CredentialServiceImpl( case Base64(value) => for { _ <- ZIO - .attempt(CredentialOffer(value)) + .attempt(AnoncredCredentialOffer(value)) .mapError(e => CredentialServiceError.UnexpectedError( s"Unexpected error parsing credential offer attachment: ${e.toString}" @@ -563,12 +571,12 @@ private class CredentialServiceImpl( } ) .mapError(_ => InvalidFlowStateError(s"No AnonCreds offer attachment found")) - credentialOffer = anoncreds.CredentialOffer(attachmentData) + credentialOffer = anoncreds.AnoncredCredentialOffer(attachmentData) _ <- ZIO.logInfo(s"Cred def ID => ${credentialOffer.getCredDefId}") credDefContent <- uriDereferencer .dereference(new URI(credentialOffer.getCredDefId)) .mapError(err => UnexpectedError(err.toString)) - credentialDefinition = anoncreds.CredentialDefinition(credDefContent) + credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent) linkSecret <- linkSecretService .fetchOrCreate() .mapError(e => CredentialServiceError.LinkSecretError.apply(e.cause)) @@ -627,34 +635,48 @@ private class CredentialServiceImpl( override def receiveCredentialIssue( issueCredential: IssueCredential ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = { - // TODO We can get rid of this 'raw' representation stored in DB, because it is not used. - val rawIssuedCredential = issueCredential.attachments.map(_.data.asJson.noSpaces).headOption.getOrElse("???") for { // TODO Move this type of generic/reusable code to a helper trait - attachmentFormatAndData <- ZIO.succeed { - import IssueCredentialIssuedFormat.{Anoncred, JWT} - issueCredential.attachments - .collectFirst { - case AttachmentDescriptor(_, _, Base64(v), Some(JWT.name), _, _, _, _) => (JWT, v) - case AttachmentDescriptor(_, _, Base64(v), Some(Anoncred.name), _, _, _, _) => (Anoncred, v) - } - .map { case (f, v) => (f, java.util.Base64.getUrlDecoder.decode(v)) } - } record <- getRecordFromThreadIdWithState( issueCredential.thid.map(DidCommID(_)), ignoreWithZeroRetries = true, ProtocolState.RequestPending, ProtocolState.RequestSent ) - _ <- attachmentFormatAndData match - case Some(IssueCredentialIssuedFormat.JWT, _) => ZIO.succeed(()) - case Some(IssueCredentialIssuedFormat.Anoncred, ba) => processAnonCredsCredential(record, ba) - case _ => ZIO.fail(UnexpectedError("No AnonCreds or JWT credential attachment found")) + processedAttachments <- { + import IssueCredentialIssuedFormat.Anoncred + ZIO.collectAll( + issueCredential.attachments + .map { + case AttachmentDescriptor( + id, + media_type, + Base64(v), + Some(Anoncred.name), + _, + _, + _, + _ + ) => + processAnonCredsCredential(record, java.util.Base64.getUrlDecoder.decode(v)) + .map(processedCredential => + AttachmentDescriptor.buildBase64Attachment( + id = id, + mediaType = media_type, + format = Some(IssueCredentialIssuedFormat.Anoncred.name), + payload = processedCredential + ) + ) + case attachment => ZIO.succeed(attachment) + } + ) + } + processedIssuedCredential = issueCredential.copy(attachments = processedAttachments) _ <- credentialRepository .updateWithIssuedRawCredential( record.id, - issueCredential, - rawIssuedCredential, + processedIssuedCredential, + processedIssuedCredential.attachments.map(_.data.asJson.noSpaces).headOption.getOrElse("???"), ProtocolState.CredentialReceived ) .flatMap { @@ -669,30 +691,33 @@ private class CredentialServiceImpl( } yield record } - private[this] def processAnonCredsCredential(record: IssueCredentialRecord, credentialBytes: Array[Byte]) = { + private[this] def processAnonCredsCredential( + record: IssueCredentialRecord, + credentialBytes: Array[Byte] + ): ZIO[WalletAccessContext, CredentialServiceError, Array[Byte]] = { for { - credential <- ZIO.succeed(anoncreds.Credential(new String(credentialBytes))) + credential <- ZIO.succeed(anoncreds.AnoncredCredential(new String(credentialBytes))) credDefContent <- uriDereferencer .dereference(new URI(credential.getCredDefId)) .mapError(err => UnexpectedError(err.toString)) - credentialDefinition = anoncreds.CredentialDefinition(credDefContent) + credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent) metadata <- ZIO .fromOption(record.anonCredsRequestMetadata) .mapError(_ => CredentialServiceError.UnexpectedError(s"No request metadata Id found un record: ${record.id}")) linkSecret <- linkSecretService .fetchOrCreate() .mapError(e => CredentialServiceError.LinkSecretError.apply(e.cause)) - _ <- ZIO + credential <- ZIO .attempt( AnoncredLib.processCredential( - anoncreds.Credential(new String(credentialBytes)), + anoncreds.AnoncredCredential(new String(credentialBytes)), metadata, linkSecret, credentialDefinition ) ) .mapError(error => UnexpectedError(s"AnonCreds credential processing error: ${error.getMessage}")) - } yield () + } yield credential.data.getBytes() } override def markOfferSent( @@ -848,14 +873,14 @@ private class CredentialServiceImpl( private[this] def createAnonCredsDidCommOfferCredential( pairwiseIssuerDID: DidId, pairwiseHolderDID: DidId, - schemaId: String, + schemaUri: String, credentialDefinitionGUID: UUID, + credentialDefinitionId: String, claims: Seq[Attribute], - thid: DidCommID, - credentialDefinitionId: String + thid: DidCommID ) = { for { - credentialPreview <- ZIO.succeed(CredentialPreview(schema_id = Some(schemaId), attributes = claims)) + credentialPreview <- ZIO.succeed(CredentialPreview(schema_id = Some(schemaUri), attributes = claims)) body = OfferCredential.Body( goal_code = Some("Offer Credential"), credential_preview = credentialPreview, @@ -883,16 +908,16 @@ private class CredentialServiceImpl( credentialDefinition <- credentialDefinitionService .getByGUID(credentialDefinitionGUID) .mapError(e => CredentialServiceError.UnexpectedError(e.toString)) - cd = anoncreds.CredentialDefinition(credentialDefinition.definition.toString) - kcp = anoncreds.CredentialKeyCorrectnessProof(credentialDefinition.keyCorrectnessProof.toString) + cd = anoncreds.AnoncredCredentialDefinition(credentialDefinition.definition.toString) + kcp = anoncreds.AnoncredCredentialKeyCorrectnessProof(credentialDefinition.keyCorrectnessProof.toString) maybeCredentialDefinitionSecret <- genericSecretStorage .get[UUID, CredentialDefinitionSecret](credentialDefinition.guid) .orDie credentialDefinitionSecret <- ZIO .fromOption(maybeCredentialDefinitionSecret) .mapError(_ => CredentialServiceError.CredentialDefinitionPrivatePartNotFound(credentialDefinition.guid)) - cdp = anoncreds.CredentialDefinitionPrivate(credentialDefinitionSecret.json.toString) - createCredentialDefinition = CreateCredentialDefinition(cd, cdp, kcp) + cdp = anoncreds.AnoncredCredentialDefinitionPrivate(credentialDefinitionSecret.json.toString) + createCredentialDefinition = AnoncredCreateCredentialDefinition(cd, cdp, kcp) offer = AnoncredLib.createOffer(createCredentialDefinition, credentialDefinitionId) } yield offer @@ -1139,7 +1164,7 @@ private class CredentialServiceImpl( credentialDefinition <- credentialDefinitionService .getByGUID(credentialDefinitionId) .mapError(e => CredentialServiceError.UnexpectedError(e.toString)) - cd = anoncreds.CredentialDefinition(credentialDefinition.definition.toString) + cd = anoncreds.AnoncredCredentialDefinition(credentialDefinition.definition.toString) offerCredential <- ZIO .fromOption(record.offerCredentialData) .mapError(_ => InvalidFlowStateError(s"No offer found for this record: ${record.id}")) @@ -1154,7 +1179,7 @@ private class CredentialServiceImpl( } ) .mapError(_ => InvalidFlowStateError(s"No AnonCreds offer attachment found")) - credentialOffer = anoncreds.CredentialOffer(offerCredentialAttachmentData) + credentialOffer = anoncreds.AnoncredCredentialOffer(offerCredentialAttachmentData) requestCredential <- ZIO .fromOption(record.requestCredentialData) .mapError(_ => InvalidFlowStateError(s"No request found for this record: ${record.id}")) @@ -1169,7 +1194,7 @@ private class CredentialServiceImpl( } ) .mapError(_ => InvalidFlowStateError(s"No AnonCreds request attachment found")) - credentialRequest = anoncreds.CredentialRequest(requestCredentialAttachmentData) + credentialRequest = anoncreds.AnoncredCredentialRequest(requestCredentialAttachmentData) attrValues = offerCredential.body.credential_preview.body.attributes.map { attr => (attr.name, attr.value) } @@ -1179,14 +1204,15 @@ private class CredentialServiceImpl( credentialDefinitionSecret <- ZIO .fromOption(maybeCredentialDefinitionSecret) .mapError(_ => CredentialServiceError.CredentialDefinitionPrivatePartNotFound(credentialDefinition.guid)) - cdp = anoncreds.CredentialDefinitionPrivate(credentialDefinitionSecret.json.toString) - credential = AnoncredLib.createCredential( - cd, - cdp, - credentialOffer, - credentialRequest, - attrValues - ) + cdp = anoncreds.AnoncredCredentialDefinitionPrivate(credentialDefinitionSecret.json.toString) + credential = + AnoncredLib.createCredential( + cd, + cdp, + credentialOffer, + credentialRequest, + attrValues + ) } yield credential } diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifier.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifier.scala index bdd7e64947..504b82b5d1 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifier.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifier.scala @@ -47,10 +47,10 @@ class CredentialServiceNotifier( pairwiseHolderDID: DidId, thid: DidCommID, credentialDefinitionGUID: UUID, + credentialDefinitionId: _root_.java.lang.String, claims: Json, validityPeriod: Option[Double], - automaticIssuance: Option[Boolean], - credentialDefinitionId: _root_.java.lang.String + automaticIssuance: Option[Boolean] ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = notifyOnSuccess( svc.createAnonCredsIssueCredentialRecord( @@ -58,10 +58,10 @@ class CredentialServiceNotifier( pairwiseHolderDID, thid, credentialDefinitionGUID, + credentialDefinitionId, claims, validityPeriod, - automaticIssuance, - credentialDefinitionId + automaticIssuance ) ) diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/LinkSecretService.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/LinkSecretService.scala index c87c63e6ed..f0d6aa2448 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/LinkSecretService.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/LinkSecretService.scala @@ -1,6 +1,6 @@ package io.iohk.atala.pollux.core.service -import io.iohk.atala.pollux.anoncreds.LinkSecretWithId +import io.iohk.atala.pollux.anoncreds.AnoncredLinkSecretWithId import io.iohk.atala.pollux.core.model.error.LinkSecretError import io.iohk.atala.shared.models.WalletAccessContext import zio.ZIO @@ -8,5 +8,5 @@ import zio.ZIO trait LinkSecretService { type Result[T] = ZIO[WalletAccessContext, LinkSecretError, T] - def fetchOrCreate(): Result[LinkSecretWithId] + def fetchOrCreate(): Result[AnoncredLinkSecretWithId] } diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/LinkSecretServiceImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/LinkSecretServiceImpl.scala index 4219d2bbd0..88e51199be 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/LinkSecretServiceImpl.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/LinkSecretServiceImpl.scala @@ -1,7 +1,7 @@ package io.iohk.atala.pollux.core.service import io.iohk.atala.agent.walletapi.storage.{GenericSecret, GenericSecretStorage} -import io.iohk.atala.pollux.anoncreds.{LinkSecret, LinkSecretWithId} +import io.iohk.atala.pollux.anoncreds.{AnoncredLinkSecret, AnoncredLinkSecretWithId} import io.iohk.atala.pollux.core.model.error.LinkSecretError import io.iohk.atala.shared.models.WalletAccessContext import zio.* @@ -15,18 +15,18 @@ class LinkSecretServiceImpl(genericSecretStorage: GenericSecretStorage) extends type Result[T] = ZIO[WalletAccessContext, LinkSecretError, T] - override def fetchOrCreate(): Result[LinkSecretWithId] = { + override def fetchOrCreate(): Result[AnoncredLinkSecretWithId] = { genericSecretStorage - .get[String, LinkSecret](LinkSecretServiceImpl.defaultLinkSecretId) + .get[String, AnoncredLinkSecret](LinkSecretServiceImpl.defaultLinkSecretId) .flatMap { case Some(secret) => ZIO.succeed(secret) case None => - val linkSecret = LinkSecret() + val linkSecret = AnoncredLinkSecret() genericSecretStorage - .set[String, LinkSecret](LinkSecretServiceImpl.defaultLinkSecretId, linkSecret) + .set[String, AnoncredLinkSecret](LinkSecretServiceImpl.defaultLinkSecretId, linkSecret) .as(linkSecret) } - .map(linkSecret => LinkSecretWithId(LinkSecretServiceImpl.defaultLinkSecretId, linkSecret)) + .map(linkSecret => AnoncredLinkSecretWithId(LinkSecretServiceImpl.defaultLinkSecretId, linkSecret)) .mapError(LinkSecretError.apply) } } @@ -40,13 +40,13 @@ object LinkSecretServiceImpl { ] = ZLayer.fromFunction(LinkSecretServiceImpl(_)) - given GenericSecret[String, LinkSecret] = new { + given GenericSecret[String, AnoncredLinkSecret] = new { override def keyPath(id: String): String = s"link-secret/${id.toString}" - override def encodeValue(secret: LinkSecret): Json = Json.Str(secret.data) + override def encodeValue(secret: AnoncredLinkSecret): Json = Json.Str(secret.data) - override def decodeValue(json: Json): Try[LinkSecret] = json match { - case Json.Str(data) => Try(LinkSecret(data)) + override def decodeValue(json: Json): Try[AnoncredLinkSecret] = json match { + case Json.Str(data) => Try(AnoncredLinkSecret(data)) case _ => scala.util.Failure(new Exception("Invalid JSON format for LinkSecret")) } } diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockCredentialService.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockCredentialService.scala index e73c337845..21b7d82e73 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockCredentialService.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/MockCredentialService.scala @@ -100,10 +100,10 @@ object MockCredentialService extends Mock[CredentialService] { pairwiseHolderDID: DidId, thid: DidCommID, credentialDefinitionGUID: UUID, + credentialDefinitionId: String, claims: Json, validityPeriod: Option[Double], - automaticIssuance: Option[Boolean], - credentialDefinitionId: String + automaticIssuance: Option[Boolean] ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = proxy( CreateAnonCredsIssueCredentialRecord, 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..6049790a99 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 @@ -2,26 +2,35 @@ package io.iohk.atala.pollux.core.service import io.iohk.atala.mercury.model.DidId import io.iohk.atala.mercury.protocol.presentproof.{Presentation, ProofType, ProposePresentation, RequestPresentation} +import io.iohk.atala.pollux.anoncreds.AnoncredPresentation 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.{AnoncredCredentialProofsV1, 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] @@ -34,8 +43,17 @@ object MockPresentationService extends Mock[PresentationService] { object MarkPresentationVerificationFailed extends Effect[DidCommID, PresentationError, PresentationRecord] + object VerifyAnoncredPresentation extends Effect[DidCommID, PresentationError, PresentationRecord] + object AcceptRequestPresentation extends Effect[(DidCommID, Seq[String]), PresentationError, PresentationRecord] + object AcceptAnoncredRequestPresentation + extends Effect[ + (DidCommID, AnoncredCredentialProofsV1), + PresentationError, + PresentationRecord + ] + object RejectRequestPresentation extends Effect[DidCommID, PresentationError, PresentationRecord] object MarkPresentationGenerated extends Effect[(DidCommID, Presentation), PresentationError, PresentationRecord] @@ -54,26 +72,44 @@ 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] ): IO[PresentationError, PresentationRecord] = proxy(AcceptRequestPresentation, (recordId, credentialsToUse)) + override def acceptAnoncredRequestPresentation( + recordId: DidCommID, + credentialsToUse: AnoncredCredentialProofsV1 + ): IO[PresentationError, PresentationRecord] = + proxy(AcceptAnoncredRequestPresentation, (recordId, credentialsToUse)) + override def rejectRequestPresentation(recordId: DidCommID): IO[PresentationError, PresentationRecord] = proxy(RejectRequestPresentation, recordId) @@ -104,6 +140,13 @@ object MockPresentationService extends Mock[PresentationService] { override def markPresentationVerificationFailed(recordId: DidCommID): IO[PresentationError, PresentationRecord] = proxy(MarkPresentationVerificationFailed, recordId) + override def verifyAnoncredPresentation( + presentation: Presentation, + requestPresentation: RequestPresentation, + recordId: DidCommID + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = + proxy(VerifyAnoncredPresentation, recordId) + override def receiveRequestPresentation( connectionId: Option[String], request: RequestPresentation @@ -122,12 +165,27 @@ object MockPresentationService extends Mock[PresentationService] { ignoreWithZeroRetries: Boolean ): IO[PresentationError, Seq[PresentationRecord]] = ??? - override def createPresentationPayloadFromRecord( + override def createJwtPresentationPayloadFromRecord( record: DidCommID, issuer: Issuer, issuanceDate: Instant ): IO[PresentationError, PresentationPayload] = ??? + override def createAnoncredPresentationPayloadFromRecord( + record: DidCommID, + issuer: Issuer, + anoncredCredentialProof: AnoncredCredentialProofsV1, + issuanceDate: Instant + ): IO[PresentationError, AnoncredPresentation] = ??? + + override def createAnoncredPresentation( + requestPresentation: RequestPresentation, + recordId: DidCommID, + prover: Issuer, + anoncredCredentialProof: AnoncredCredentialProofsV1, + issuanceDate: Instant + ): ZIO[WalletAccessContext, PresentationError, Presentation] = ??? + override def getPresentationRecordsByStates( ignoreWithZeroRetries: Boolean, limit: Int, @@ -158,6 +216,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..9727983870 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 @@ -2,9 +2,11 @@ package io.iohk.atala.pollux.core.service import io.iohk.atala.mercury.model.* import io.iohk.atala.mercury.protocol.presentproof.* +import io.iohk.atala.pollux.anoncreds.AnoncredPresentation 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.{AnoncredCredentialProofsV1, AnoncredPresentationRequestV1} import io.iohk.atala.pollux.vc.jwt.* import io.iohk.atala.shared.models.WalletAccessContext import zio.* @@ -14,29 +16,50 @@ import java.util as ju import java.util.UUID 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( ignoreWithZeroRetries: Boolean ): ZIO[WalletAccessContext, PresentationError, Seq[PresentationRecord]] - def createPresentationPayloadFromRecord( + def createJwtPresentationPayloadFromRecord( record: DidCommID, issuer: Issuer, issuanceDate: Instant ): ZIO[WalletAccessContext, PresentationError, PresentationPayload] + def createAnoncredPresentationPayloadFromRecord( + record: DidCommID, + issuer: Issuer, + anoncredCredentialProof: AnoncredCredentialProofsV1, + issuanceDate: Instant + ): ZIO[WalletAccessContext, PresentationError, AnoncredPresentation] + + def createAnoncredPresentation( + requestPresentation: RequestPresentation, + recordId: DidCommID, + prover: Issuer, + anoncredCredentialProof: AnoncredCredentialProofsV1, + issuanceDate: Instant + ): ZIO[WalletAccessContext, PresentationError, Presentation] + def getPresentationRecordsByStates( ignoreWithZeroRetries: Boolean, limit: Int, @@ -67,6 +90,11 @@ trait PresentationService { credentialsToUse: Seq[String] ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] + def acceptAnoncredRequestPresentation( + recordId: DidCommID, + credentialsToUse: AnoncredCredentialProofsV1 + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] + def rejectRequestPresentation(recordId: DidCommID): ZIO[WalletAccessContext, PresentationError, PresentationRecord] def receiveProposePresentation( @@ -106,6 +134,12 @@ trait PresentationService { recordId: DidCommID ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] + def verifyAnoncredPresentation( + presentation: Presentation, + requestPresentation: RequestPresentation, + recordId: DidCommID + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] + def reportProcessingFailure( recordId: DidCommID, failReason: Option[String] 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..d75e785f19 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 @@ -6,23 +6,31 @@ import io.circe.* import io.circe.parser.* import io.circe.syntax.* import io.iohk.atala.mercury.model.* +import io.iohk.atala.mercury.protocol.issuecredential.IssueCredentialIssuedFormat import io.iohk.atala.mercury.protocol.presentproof.* +import io.iohk.atala.pollux.anoncreds.* import io.iohk.atala.pollux.core.model.* 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.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1 import io.iohk.atala.pollux.core.repository.{CredentialRepository, PresentationRepository} +import io.iohk.atala.pollux.core.service.serdes.* import io.iohk.atala.pollux.vc.jwt.* import io.iohk.atala.shared.models.WalletAccessContext import io.iohk.atala.shared.utils.aspects.CustomMetricsAspect import zio.* +import java.net.URI import java.rmi.UnexpectedException import java.time.Instant import java.util as ju -import java.util.UUID +import java.util.{UUID, Base64 as JBase64} +import scala.util.Try private class PresentationServiceImpl( + uriDereferencer: URIDereferencer, + linkSecretService: LinkSecretService, presentationRepository: PresentationRepository, credentialRepository: CredentialRepository, maxRetries: Int = 5, // TODO move to config @@ -46,7 +54,7 @@ private class PresentationServiceImpl( ) _ <- count match case 1 => ZIO.succeed(()) - case n => ZIO.fail(RecordIdNotFound(recordId)) + case _ => ZIO.fail(RecordIdNotFound(recordId)) record <- presentationRepository .getPresentationRecord(recordId) .mapError(RepositoryError.apply) @@ -57,7 +65,7 @@ private class PresentationServiceImpl( } yield record } - override def createPresentationPayloadFromRecord( + override def createJwtPresentationPayloadFromRecord( recordId: DidCommID, prover: Issuer, issuanceDate: Instant @@ -90,15 +98,93 @@ private class PresentationServiceImpl( ) ) - presentationPayload <- createPresentationPayloadFromCredential( + presentationPayload <- createJwtPresentationPayloadFromCredential( issuedCredentials, - record.credentialFormat, requestPresentation, prover ) } yield presentationPayload } + override def createAnoncredPresentationPayloadFromRecord( + recordId: DidCommID, + prover: Issuer, + anoncredCredentialProof: AnoncredCredentialProofsV1, + issuanceDate: Instant + ): ZIO[WalletAccessContext, PresentationError, AnoncredPresentation] = { + + for { + maybeRecord <- presentationRepository + .getPresentationRecord(recordId) + .mapError(RepositoryError.apply) + record <- ZIO + .fromOption(maybeRecord) + .mapError(_ => RecordIdNotFound(recordId)) + requestPresentation <- ZIO + .fromOption(record.requestPresentationData) + .mapError(_ => InvalidFlowStateError(s"RequestPresentation not found: $recordId")) + issuedValidCredentials <- + credentialRepository + .getValidAnoncredIssuedCredentials( + anoncredCredentialProof.credentialProofs.map(credentialProof => DidCommID(credentialProof.credential)) + ) + .mapError(RepositoryError.apply) + issuedCredentials <- ZIO.fromEither( + Either.cond( + issuedValidCredentials.nonEmpty, + issuedValidCredentials, + PresentationError.IssuedCredentialNotFoundError( + new Throwable("No matching issued credentials found in prover db") + ) + ) + ) + presentationPayload <- createAnoncredPresentationPayloadFromCredential( + issuedCredentials, + issuedValidCredentials.flatMap(_.schemaUri), + issuedValidCredentials.flatMap(_.credentialDefinitionUri), + requestPresentation, + anoncredCredentialProof.credentialProofs + ) + } yield presentationPayload + } + + def createAnoncredPresentation( + requestPresentation: RequestPresentation, + recordId: DidCommID, + prover: Issuer, + anoncredCredentialProof: AnoncredCredentialProofsV1, + issuanceDate: Instant + ): ZIO[WalletAccessContext, PresentationError, Presentation] = { + for { + presentationPayload <- + createAnoncredPresentationPayloadFromRecord( + recordId, + prover, + anoncredCredentialProof, + issuanceDate + ) + presentation <- ZIO.succeed( + Presentation( + body = Presentation.Body( + goal_code = requestPresentation.body.goal_code, + comment = requestPresentation.body.comment + ), + attachments = Seq( + AttachmentDescriptor + .buildBase64Attachment( + payload = presentationPayload.data.getBytes(), + mediaType = Some(PresentCredentialFormat.Anoncred.name), + format = Some(PresentCredentialFormat.Anoncred.name), + ) + ), + thid = requestPresentation.thid.orElse(Some(requestPresentation.id)), + from = requestPresentation.to, + to = requestPresentation.from + ) + ) + } yield presentation + } + override def extractIdFromCredential(credential: W3cCredentialPayload): Option[UUID] = credential.maybeId.map(_.split("/").last).map(UUID.fromString) @@ -141,15 +227,52 @@ 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] = { + createPresentationRecord( + pairwiseVerifierDID, + pairwiseProverDID, + thid, + connectionId, + CredentialFormat.JWT, + proofTypes, + maybeOptions.map(options => Seq(toJWTAttachment(options))).getOrElse(Seq.empty) + ) + } + + override def createAnoncredPresentationRecord( + pairwiseVerifierDID: DidId, + pairwiseProverDID: DidId, + thid: DidCommID, + connectionId: Option[String], + presentationRequest: AnoncredPresentationRequestV1 + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { + createPresentationRecord( + pairwiseVerifierDID, + pairwiseProverDID, + thid, + connectionId, + CredentialFormat.AnonCreds, + Seq.empty, + Seq(toAnoncredAttachment(presentationRequest)) + ) + } + + private def createPresentationRecord( + pairwiseVerifierDID: DidId, + pairwiseProverDID: DidId, + thid: DidCommID, + connectionId: Option[String], + format: CredentialFormat, + proofTypes: Seq[ProofType], + attachments: Seq[AttachmentDescriptor] + ) = { for { request <- ZIO.succeed( createDidCommRequestPresentation( @@ -157,13 +280,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 - } + attachments ) ) record <- ZIO.succeed( @@ -182,12 +299,14 @@ private class PresentationServiceImpl( proposePresentationData = None, presentationData = None, credentialsToUse = None, + anoncredCredentialsToUseJsonSchemaId = None, + anoncredCredentialsToUse = None, metaRetries = maxRetries, metaNextRetry = Some(Instant.now()), metaLastFailure = None, ) ) - count <- presentationRepository + _ <- presentationRepository .createPresentationRecord(record) .flatMap { case 1 => ZIO.succeed(()) @@ -234,9 +353,17 @@ private class PresentationServiceImpl( val jsonF = PresentCredentialRequestFormat.JWT.name // stable identifier val anoncredF = PresentCredentialRequestFormat.Anoncred.name // stable identifier head.format match - case None => ZIO.fail(PresentationError.MissingCredentialFormat) - case Some(`jsonF`) => ZIO.succeed(CredentialFormat.JWT) - case Some(`anoncredF`) => ZIO.succeed(CredentialFormat.AnonCreds) + case None => ZIO.fail(PresentationError.MissingCredentialFormat) + case Some(`jsonF`) => ZIO.succeed(CredentialFormat.JWT) + case Some(`anoncredF`) => + head.data match + case Base64(data) => + val decodedData = new String(JBase64.getUrlDecoder.decode(data)) + AnoncredPresentationRequestV1.schemaSerDes + .validate(decodedData) + .map(_ => CredentialFormat.AnonCreds) + .mapError(error => InvalidAnoncredPresentationRequest(error.error)) + case _ => ZIO.fail(InvalidAnoncredPresentationRequest("Expecting Base64-encoded data")) case Some(unsupportedFormat) => ZIO.fail(PresentationError.UnsupportedCredentialFormat(unsupportedFormat)) case _ => ZIO.fail(PresentationError.UnexpectedError("Presentation with multi attachments")) } @@ -256,12 +383,14 @@ private class PresentationServiceImpl( proposePresentationData = None, presentationData = None, credentialsToUse = None, + anoncredCredentialsToUseJsonSchemaId = None, + anoncredCredentialsToUse = None, metaRetries = maxRetries, metaNextRetry = Some(Instant.now()), metaLastFailure = None, ) ) - count <- presentationRepository + _ <- presentationRepository .createPresentationRecord(record) .flatMap { case 1 => ZIO.succeed(()) @@ -272,32 +401,22 @@ private class PresentationServiceImpl( } /** All credentials MUST be of the same format */ - private def createPresentationPayloadFromCredential( + private def createJwtPresentationPayloadFromCredential( issuedCredentials: Seq[String], - format: CredentialFormat, requestPresentation: RequestPresentation, prover: Issuer ): IO[PresentationError, PresentationPayload] = { val verifiableCredentials: Either[ PresentationError.PresentationDecodingError, - Seq[JwtVerifiableCredentialPayload | AnoncredVerifiableCredentialPayload] + Seq[JwtVerifiableCredentialPayload] ] = issuedCredentials.map { signedCredential => - format match { - case CredentialFormat.JWT => - decode[io.iohk.atala.mercury.model.Base64](signedCredential) - .flatMap(x => Right(new String(java.util.Base64.getDecoder().decode(x.base64)))) - .flatMap(x => Right(JwtVerifiableCredentialPayload(JWT(x)))) - .left - .map(err => PresentationDecodingError(new Throwable(s"JsonData decoding error: $err"))) - case CredentialFormat.AnonCreds => - decode[io.iohk.atala.mercury.model.Base64](signedCredential) - .flatMap(x => Right(new String(java.util.Base64.getDecoder().decode(x.base64)))) - .flatMap(x => Right(AnoncredVerifiableCredentialPayload(x))) - .left - .map(err => PresentationDecodingError(new Throwable(s"JsonData decoding error: $err"))) - } + decode[io.iohk.atala.mercury.model.Base64](signedCredential) + .flatMap(x => Right(new String(java.util.Base64.getDecoder.decode(x.base64)))) + .flatMap(x => Right(JwtVerifiableCredentialPayload(JWT(x)))) + .left + .map(err => PresentationDecodingError(new Throwable(s"JsonData decoding error: $err"))) }.sequence val maybePresentationOptions @@ -349,7 +468,131 @@ private class PresentationServiceImpl( } ) } yield presentationPayload + } + + private case class AnoncredCredentialProof( + credential: String, + requestedAttribute: Seq[String], + requestedPredicate: Seq[String] + ) + + private def createAnoncredPresentationPayloadFromCredential( + issuedCredentialRecords: Seq[ValidFullIssuedCredentialRecord], + schemaIds: Seq[String], + credentialDefinitionIds: Seq[String], + requestPresentation: RequestPresentation, + credentialProofs: List[AnoncredCredentialProofV1], + ): ZIO[WalletAccessContext, PresentationError, AnoncredPresentation] = { + for { + schemaMap <- + ZIO + .collectAll(schemaIds.map { schemaUri => + resolveSchema(schemaUri) + }) + .map(_.toMap) + credentialDefinitionMap <- + ZIO + .collectAll(credentialDefinitionIds.map { credentialDefinitionUri => + resolveCredentialDefinition(credentialDefinitionUri) + }) + .map(_.toMap) + credentialProofsMap = credentialProofs.map(credentialProof => (credentialProof.credential, credentialProof)).toMap + verifiableCredentials <- + ZIO.collectAll( + issuedCredentialRecords + .flatMap(issuedCredentialRecord => { + issuedCredentialRecord.issuedCredential + .map(issuedCredential => + issuedCredential.attachments + .filter(attachment => attachment.format.contains(IssueCredentialIssuedFormat.Anoncred.name)) + .map(_.data) + .map { + case Base64(data) => + Right( + AnoncredCredentialProof( + new String(JBase64.getUrlDecoder.decode(data)), + credentialProofsMap(issuedCredentialRecord.id.value).requestedAttribute, + credentialProofsMap(issuedCredentialRecord.id.value).requestedPredicate + ) + ) + case _ => Left(InvalidAnoncredPresentationRequest("Expecting Base64-encoded data")) + } + .map(ZIO.fromEither(_)) + ) + .toSeq + .flatten + }) + ) + presentationRequestAttachment <- ZIO.fromEither( + requestPresentation.attachments.headOption.toRight(InvalidAnoncredPresentationRequest("Missing Presentation")) + ) + presentationRequestData <- + presentationRequestAttachment.data match + case Base64(data) => ZIO.succeed(new String(JBase64.getUrlDecoder.decode(data))) + case _ => ZIO.fail(InvalidAnoncredPresentationRequest("Expecting Base64-encoded data")) + _ <- + AnoncredPresentationRequestV1.schemaSerDes + .deserialize(presentationRequestData) + .mapError(error => InvalidAnoncredPresentationRequest(error.error)) + linkSecret <- + linkSecretService + .fetchOrCreate() + .map(_.secret) + .mapError(t => AnoncredPresentationCreationError(t.cause)) + credentialRequest = + verifiableCredentials.map(verifiableCredential => + AnoncredCredentialRequests( + AnoncredCredential(verifiableCredential.credential), + verifiableCredential.requestedAttribute, + verifiableCredential.requestedPredicate + ) + ) + presentation <- + ZIO + .fromEither( + AnoncredLib.createPresentation( + AnoncredPresentationRequest(presentationRequestData), + credentialRequest, + Map.empty, // TO FIX + linkSecret, + schemaMap, + credentialDefinitionMap + ) + ) + .mapError((t: Throwable) => AnoncredPresentationCreationError(t)) + } yield presentation + } + + private def resolveSchema(schemaUri: String): IO[UnexpectedError, (String, AnoncredSchemaDef)] = { + for { + uri <- ZIO.attempt(new URI(schemaUri)).mapError(e => UnexpectedError(e.getMessage)) + content <- uriDereferencer.dereference(uri).mapError(e => UnexpectedError(e.error)) + anoncredSchema <- + AnoncredSchemaSerDesV1.schemaSerDes + .deserialize(content) + .mapError(error => UnexpectedError(s"AnonCreds Schema parsing error: $error")) + anoncredLibSchema = + AnoncredSchemaDef( + schemaUri, + anoncredSchema.version, + anoncredSchema.attrNames, + anoncredSchema.issuerId + ) + } yield (schemaUri, anoncredLibSchema) + } + private def resolveCredentialDefinition( + credentialDefinitionUri: String + ): IO[UnexpectedError, (String, AnoncredCredentialDefinition)] = { + for { + uri <- ZIO.attempt(new URI(credentialDefinitionUri)).mapError(e => UnexpectedError(e.getMessage)) + content <- uriDereferencer.dereference(uri).mapError(e => UnexpectedError(e.error)) + _ <- + PublicCredentialDefinitionSerDesV1.schemaSerDes + .validate(content) + .mapError(error => UnexpectedError(s"AnonCreds Schema parsing error: $error")) + anoncredCredentialDefinition = AnoncredCredentialDefinition(content) + } yield (credentialDefinitionUri, anoncredCredentialDefinition) } def acceptRequestPresentation( @@ -359,49 +602,118 @@ private class PresentationServiceImpl( for { record <- getRecordWithState(recordId, ProtocolState.RequestReceived) - issuedValidCredentials <- credentialRepository + issuedCredentials <- credentialRepository .getValidIssuedCredentials(credentialsToUse.map(DidCommID(_))) .mapError(RepositoryError.apply) - _ <- ZIO.cond( - (issuedValidCredentials.map(_.subjectId).toSet.size == 1), - (), - PresentationError.HolderBindingError( - s"Creating a Verifiable Presentation for credential with different subject DID is not supported, found : ${issuedValidCredentials - .map(_.subjectId)}" - ) - ) - signedCredentials = issuedValidCredentials.flatMap(_.issuedCredentialRaw) - // record.credentialFormat match { - // case PresentationRecord.CredentialFormat.JWT => issuedRawCredentials - // case CredentialFormat.AnonCreds => issuedRawCredentials - // } - issuedCredentials <- ZIO.fromEither( - Either.cond( - signedCredentials.nonEmpty, - signedCredentials, - PresentationError.IssuedCredentialNotFoundError( - new Throwable(s"No matching issued credentials found in prover db from the given: $credentialsToUse") - ) - ) + _ <- validateCredentials( + s"No matching issued credentials found in prover db from the given: $credentialsToUse", + record, + issuedCredentials ) count <- presentationRepository .updatePresentationWithCredentialsToUse(recordId, Option(credentialsToUse), ProtocolState.PresentationPending) .mapError(RepositoryError.apply) @@ CustomMetricsAspect.startRecordingTime( s"${record.id}_present_proof_flow_prover_presentation_pending_to_generated_ms_gauge" ) + record <- fetchPresentationRecord(recordId, count) + } yield record + } + + private def fetchPresentationRecord(recordId: DidCommID, count: RuntimeFlags) = { + for { _ <- count match case 1 => ZIO.succeed(()) - case n => ZIO.fail(RecordIdNotFound(recordId)) + case _ => ZIO.fail(RecordIdNotFound(recordId)) record <- presentationRepository .getPresentationRecord(recordId) .mapError(RepositoryError.apply) .flatMap { - case None => ZIO.fail(RecordIdNotFound(record.id)) + case None => ZIO.fail(RecordIdNotFound(recordId)) case Some(value) => ZIO.succeed(value) } } yield record } + override def acceptAnoncredRequestPresentation( + recordId: DidCommID, + credentialsToUse: AnoncredCredentialProofsV1 + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { + + for { + record <- getRecordWithState(recordId, ProtocolState.RequestReceived) + issuedCredentials <- credentialRepository + .getValidIssuedCredentials( + credentialsToUse.credentialProofs.map(credentialProof => DidCommID(credentialProof.credential)) + ) + .mapError(RepositoryError.apply) + _ <- validateCredentials( + s"No matching issued credentials found in prover db from the given: $credentialsToUse", + record, + issuedCredentials + ) + anoncredCredentialProofsV1AsJson <- ZIO + .fromEither( + AnoncredCredentialProofsV1.schemaSerDes.serialize(credentialsToUse) + ) + .mapError(error => + PresentationError.UnexpectedError( + s"Unable to serialize credentialsToUse. credentialsToUse:$credentialsToUse, error:$error" + ) + ) + count <- presentationRepository + .updateAnoncredPresentationWithCredentialsToUse( + recordId, + Option(AnoncredCredentialProofsV1.version), + Option(anoncredCredentialProofsV1AsJson), + ProtocolState.PresentationPending + ) + .mapError(RepositoryError.apply) @@ CustomMetricsAspect.startRecordingTime( + s"${record.id}_present_proof_flow_prover_presentation_pending_to_generated_ms_gauge" + ) + record <- fetchPresentationRecord(recordId, count) + } yield record + } + + private def validateCredentials( + errorMessage: String, + record: PresentationRecord, + issuedCredentials: Seq[ValidIssuedCredentialRecord] + ) = { + for { + _ <- ZIO.cond( + issuedCredentials.map(_.subjectId).toSet.size == 1, + (), + PresentationError.HolderBindingError( + s"Creating a Verifiable Presentation for credential with different subject DID is not supported, found : ${issuedCredentials + .map(_.subjectId)}" + ) + ) + validatedCredentials <- ZIO.fromEither( + Either.cond( + issuedCredentials.forall(issuedValidCredential => + issuedValidCredential.credentialFormat == record.credentialFormat + ), + issuedCredentials, + PresentationError.NotMatchingPresentationCredentialFormat( + new IllegalArgumentException( + s"No matching issued credentials format: expectedFormat=${record.credentialFormat}" + ) + ) + ) + ) + signedCredentials = validatedCredentials.flatMap(_.issuedCredentialRaw) + _ <- ZIO.fromEither( + Either.cond( + signedCredentials.nonEmpty, + signedCredentials, + PresentationError.IssuedCredentialNotFoundError( + new Throwable(errorMessage) + ) + ) + ) + } yield () + } + override def acceptPresentation( recordId: DidCommID ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { @@ -463,7 +775,7 @@ private class PresentationServiceImpl( .mapError(RepositoryError.apply) _ <- count match case 1 => ZIO.succeed(()) - case n => ZIO.fail(RecordIdNotFound(recordId)) + case _ => ZIO.fail(RecordIdNotFound(recordId)) record <- presentationRepository .getPresentationRecord(record.id) .mapError(RepositoryError.apply) @@ -585,6 +897,63 @@ private class PresentationServiceImpl( PresentationRecord.ProtocolState.PresentationVerificationFailed ) + override def verifyAnoncredPresentation( + presentation: Presentation, + requestPresentation: RequestPresentation, + recordId: DidCommID + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { + for { + serializedPresentation <- presentation.attachments.head.data match { + case Base64(data) => ZIO.succeed(AnoncredPresentation(new String(JBase64.getUrlDecoder.decode(data)))) + case _ => ZIO.fail(InvalidAnoncredPresentation("Expecting Base64-encoded data")) + } + deserializedPresentation <- + AnoncredPresentationV1.schemaSerDes + .deserialize(serializedPresentation.data) + .mapError(error => PresentationError.UnexpectedError(error.error)) + schemaIds = deserializedPresentation.identifiers.map(_.schema_id) + schemaMap <- + ZIO + .collectAll(schemaIds.map { schemaId => + resolveSchema(schemaId) + }) + .map(_.toMap) + credentialDefinitionIds = deserializedPresentation.identifiers.map(_.cred_def_id) + credentialDefinitionMap <- + ZIO + .collectAll(credentialDefinitionIds.map { credentialDefinitionId => + resolveCredentialDefinition(credentialDefinitionId) + }) + .map(_.toMap) + serializedPresentationRequest <- requestPresentation.attachments.head.data match { + case Base64(data) => ZIO.succeed(AnoncredPresentationRequest(new String(JBase64.getUrlDecoder.decode(data)))) + case _ => ZIO.fail(InvalidAnoncredPresentationRequest("Expecting Base64-encoded data")) + } + isValid <- + ZIO + .fromTry( + Try( + AnoncredLib.verifyPresentation( + serializedPresentation, + serializedPresentationRequest, + schemaMap, + credentialDefinitionMap + ) + ) + ) + .mapError((t: Throwable) => AnoncredPresentationVerificationError(t)) + .flatMapError(e => + for { + _ <- markPresentationVerificationFailed(recordId) + } yield () + ZIO.succeed(e) + ) + result <- + if isValid then markPresentationVerified(recordId) + else markPresentationVerificationFailed(recordId) + } yield result + } + def reportProcessingFailure( recordId: DidCommID, failReason: Option[String] @@ -621,11 +990,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.serializeToJsonString(presentationRequest).getBytes() ) } @@ -676,19 +1047,16 @@ private class PresentationServiceImpl( case n => ZIO.fail(UnexpectedException(s"Invalid row count result: $n")) } .mapError(RepositoryError.apply) - record <- presentationRepository - .getPresentationRecord(id) - .mapError(RepositoryError.apply) - .flatMap { - case None => ZIO.fail(RecordIdNotFound(id)) - case Some(value) => ZIO.succeed(value) - } + record <- fetchPresentationRecord(id, 1) } yield record } } object PresentationServiceImpl { - val layer: URLayer[PresentationRepository & CredentialRepository, PresentationService] = - ZLayer.fromFunction(PresentationServiceImpl(_, _)) + val layer: URLayer[ + URIDereferencer & LinkSecretService & PresentationRepository & CredentialRepository, + PresentationService + ] = + ZLayer.fromFunction(PresentationServiceImpl(_, _, _, _)) } 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..59afcefd9c 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 @@ -3,16 +3,17 @@ package io.iohk.atala.pollux.core.service import io.iohk.atala.event.notification.{Event, EventNotificationService} import io.iohk.atala.mercury.model.DidId import io.iohk.atala.mercury.protocol.presentproof.{Presentation, ProofType, ProposePresentation, RequestPresentation} +import io.iohk.atala.pollux.anoncreds.AnoncredPresentation 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.{AnoncredCredentialProofsV1, 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 +22,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 ) ) @@ -64,6 +80,13 @@ class PresentationServiceNotifier( ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = notifyOnSuccess(svc.acceptRequestPresentation(recordId, credentialsToUse)) + override def acceptAnoncredRequestPresentation( + recordId: DidCommID, + credentialsToUse: AnoncredCredentialProofsV1 + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = notifyOnSuccess( + svc.acceptAnoncredRequestPresentation(recordId, credentialsToUse) + ) + override def rejectRequestPresentation( recordId: DidCommID ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = @@ -95,6 +118,15 @@ class PresentationServiceNotifier( ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = notifyOnSuccess(svc.markPresentationVerificationFailed(recordId)) + override def verifyAnoncredPresentation( + presentation: Presentation, + requestPresentation: RequestPresentation, + recordId: DidCommID + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = + notifyOnSuccess( + svc.verifyAnoncredPresentation(presentation, requestPresentation, recordId) + ) + override def acceptPresentation( recordId: DidCommID ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = @@ -128,12 +160,29 @@ class PresentationServiceNotifier( ): ZIO[WalletAccessContext, PresentationError, Seq[PresentationRecord]] = svc.getPresentationRecords(ignoreWithZeroRetries) - override def createPresentationPayloadFromRecord( + override def createJwtPresentationPayloadFromRecord( record: DidCommID, issuer: Issuer, issuanceDate: Instant ): ZIO[WalletAccessContext, PresentationError, PresentationPayload] = - svc.createPresentationPayloadFromRecord(record, issuer, issuanceDate) + svc.createJwtPresentationPayloadFromRecord(record, issuer, issuanceDate) + + override def createAnoncredPresentationPayloadFromRecord( + record: DidCommID, + issuer: Issuer, + anoncredCredentialProof: AnoncredCredentialProofsV1, + issuanceDate: Instant + ): ZIO[WalletAccessContext, PresentationError, AnoncredPresentation] = + svc.createAnoncredPresentationPayloadFromRecord(record, issuer, anoncredCredentialProof, issuanceDate) + + override def createAnoncredPresentation( + requestPresentation: RequestPresentation, + recordId: DidCommID, + prover: Issuer, + anoncredCredentialProof: AnoncredCredentialProofsV1, + issuanceDate: Instant + ): ZIO[WalletAccessContext, PresentationError, Presentation] = + svc.createAnoncredPresentation(requestPresentation, recordId, prover, anoncredCredentialProof, issuanceDate) override def getPresentationRecordsByStates( ignoreWithZeroRetries: Boolean, diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredCredentialProofsV1.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredCredentialProofsV1.scala new file mode 100644 index 0000000000..fc8dfdd3ef --- /dev/null +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredCredentialProofsV1.scala @@ -0,0 +1,65 @@ +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 AnoncredCredentialProofV1( + credential: String, + requestedAttribute: Seq[String], + requestedPredicate: Seq[String] +) + +case class AnoncredCredentialProofsV1(credentialProofs: List[AnoncredCredentialProofV1]) + +object AnoncredCredentialProofsV1 { + val version: String = "AnoncredCredentialProofsV1" + private val schema: String = + """ + |{ + | "$schema": "http://json-schema.org/draft-07/schema#", + | "type": "object", + | "properties": { + | "credentialProofs": { + | "type": "array", + | "items": { + | "type": "object", + | "properties": { + | "credential": { + | "type": "string" + | }, + | "requestedAttribute": { + | "type": "array", + | "items": { + | "type": "string" + | } + | }, + | "requestedPredicate": { + | "type": "array", + | "items": { + | "type": "string" + | } + | } + | }, + | "required": ["credential", "requestedAttribute", "requestedPredicate"] + | } + | } + | }, + | "required": ["credentialProofs"] + |} + |""".stripMargin + + val schemaSerDes: SchemaSerDes[AnoncredCredentialProofsV1] = SchemaSerDes(schema) + + given JsonDecoder[AnoncredCredentialProofV1] = + DeriveJsonDecoder.gen[AnoncredCredentialProofV1] + + given JsonEncoder[AnoncredCredentialProofV1] = + DeriveJsonEncoder.gen[AnoncredCredentialProofV1] + + given JsonDecoder[AnoncredCredentialProofsV1] = + DeriveJsonDecoder.gen[AnoncredCredentialProofsV1] + + given JsonEncoder[AnoncredCredentialProofsV1] = + DeriveJsonEncoder.gen[AnoncredCredentialProofsV1] +} 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..e16fe79fff --- /dev/null +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredPresentationRequestV1.scala @@ -0,0 +1,131 @@ +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[Map[String, String]], + non_revoked: Option[AnoncredNonRevokedIntervalV1] +) + +case class AnoncredRequestedPredicateV1( + name: String, + p_type: String, + p_value: Int, + restrictions: List[Map[String, String]], + non_revoked: Option[AnoncredNonRevokedIntervalV1] +) + +case class AnoncredNonRevokedIntervalV1(from: Option[Int], to: Option[Int]) + +object AnoncredPresentationRequestV1 { + val version: String = "AnoncredPresentationRequestV1" + + 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" + | } + | }, + | "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" + | } + | }, + | "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 JsonEncoder[AnoncredNonRevokedIntervalV1] = + DeriveJsonEncoder.gen[AnoncredNonRevokedIntervalV1] + + given JsonDecoder[AnoncredNonRevokedIntervalV1] = + DeriveJsonDecoder.gen[AnoncredNonRevokedIntervalV1] + + given JsonDecoder[AnoncredPresentationRequestV1] = + DeriveJsonDecoder.gen[AnoncredPresentationRequestV1] + + given JsonEncoder[AnoncredPresentationRequestV1] = + DeriveJsonEncoder.gen[AnoncredPresentationRequestV1] + +} diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredPresentationV1.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredPresentationV1.scala new file mode 100644 index 0000000000..f773f8311a --- /dev/null +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredPresentationV1.scala @@ -0,0 +1,512 @@ +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 AnoncredAdditionalPropertiesTypeStringV1(value: String) + +case class AnoncredRevealedAttrV1(sub_proof_index: Int, raw: String, encoded: String) + +case class AnoncredRequestedProofV1( + revealed_attrs: Map[String, AnoncredRevealedAttrV1], + self_attested_attrs: Map[String, String], + unrevealed_attrs: Map[String, String], + predicates: Map[String, AnoncredSubProofIndexV1] +) + +case class AnoncredSubProofIndexV1(sub_proof_index: Int) + +case class AnoncredIdentifierV1( + schema_id: String, + cred_def_id: String, + rev_reg_id: Option[String], + timestamp: Option[Int] +) + +// Adjusted classes for proofs +case class AnoncredEqProofV1( + revealed_attrs: Map[String, String], + a_prime: String, + e: String, + v: String, + m: Map[String, String], + m2: String +) + +case class AnoncredPredicateV1(attr_name: String, p_type: String, value: Int) + +case class AnoncredGeProofV1( + u: Map[String, String], + r: Map[String, String], + mj: String, + alpha: String, + t: Map[String, String], + predicate: AnoncredPredicateV1 +) + +case class AnoncredPrimaryProofV1(eq_proof: AnoncredEqProofV1, ge_proofs: List[AnoncredGeProofV1]) + +case class AnoncredSubProofV1(primary_proof: AnoncredPrimaryProofV1, non_revoc_proof: Option[AnoncredNonRevocProofV1]) + +case class AnoncredProofV1(proofs: List[AnoncredSubProofV1], aggregated_proof: AnoncredAggregatedProofV1) + +case class AnoncredAggregatedProofV1(c_hash: String, c_list: List[List[Int]]) + +case class AnoncredNonRevocProofXListV1( + rho: String, + r: String, + r_prime: String, + r_prime_prime: String, + r_prime_prime_prime: String, + o: String, + o_prime: String, + m: String, + m_prime: String, + t: String, + t_prime: String, + m2: String, + s: String, + c: String +) + +case class AnoncredNonRevocProofCListV1(e: String, d: String, a: String, g: String, w: String, s: String, u: String) + +case class AnoncredNonRevocProofV1(x_list: AnoncredNonRevocProofXListV1, c_list: AnoncredNonRevocProofCListV1) + +case class AnoncredPresentationV1( + proof: AnoncredProofV1, + requested_proof: AnoncredRequestedProofV1, + identifiers: List[AnoncredIdentifierV1] +) + +object AnoncredPresentationV1 { + val version: String = "AnoncredPresentationV1" + + private val schema: String = + """ + |{ + | "$schema":"http://json-schema.org/draft-07/schema#", + | "type":"object", + | "properties":{ + | "proof":{ + | "type":"object", + | "properties":{ + | "proofs":{ + | "type":"array", + | "items":{ + | "type":"object", + | "properties":{ + | "primary_proof":{ + | "type":"object", + | "properties":{ + | "eq_proof":{ + | "type":"object", + | "properties":{ + | "revealed_attrs":{ + | "type":"object", + | "additionalProperties":{ + | "type":"string" + | } + | }, + | "a_prime":{ + | "type":"string" + | }, + | "e":{ + | "type":"string" + | }, + | "v":{ + | "type":"string" + | }, + | "m":{ + | "type":"object", + | "additionalProperties":{ + | "type":"string" + | } + | }, + | "m2":{ + | "type":"string" + | } + | }, + | "required":[ + | "revealed_attrs", + | "a_prime", + | "e", + | "v", + | "m", + | "m2" + | ] + | }, + | "ge_proofs":{ + | "type":"array", + | "items":{ + | "type":"object", + | "properties":{ + | "u":{ + | "type":"object", + | "additionalProperties":{ + | "type":"string" + | } + | }, + | "r":{ + | "type":"object", + | "additionalProperties":{ + | "type":"string" + | } + | }, + | "mj":{ + | "type":"string" + | }, + | "alpha":{ + | "type":"string" + | }, + | "t":{ + | "type":"object", + | "additionalProperties":{ + | "type":"string" + | } + | }, + | "predicate":{ + | "type":"object", + | "properties":{ + | "attr_name":{ + | "type":"string" + | }, + | "p_type":{ + | "type":"string" + | }, + | "value":{ + | "type":"integer" + | } + | }, + | "required":[ + | "attr_name", + | "p_type", + | "value" + | ] + | } + | }, + | "required":[ + | "u", + | "r", + | "mj", + | "alpha", + | "t", + | "predicate" + | ] + | } + | } + | }, + | "required":[ + | "eq_proof", + | "ge_proofs" + | ] + | }, + | "non_revoc_proof":{ + | "oneOf":[ + | { + | "type":"object", + | "properties":{ + | "x_list":{ + | "type":"object", + | "properties":{ + | "rho":{ + | "type":"string" + | }, + | "r":{ + | "type":"string" + | }, + | "r_prime":{ + | "type":"string" + | }, + | "r_prime_prime":{ + | "type":"string" + | }, + | "r_prime_prime_prime":{ + | "type":"string" + | }, + | "o":{ + | "type":"string" + | }, + | "o_prime":{ + | "type":"string" + | }, + | "m":{ + | "type":"string" + | }, + | "m_prime":{ + | "type":"string" + | }, + | "t":{ + | "type":"string" + | }, + | "t_prime":{ + | "type":"string" + | }, + | "m2":{ + | "type":"string" + | }, + | "s":{ + | "type":"string" + | }, + | "c":{ + | "type":"string" + | } + | }, + | "required":[ + | "rho", + | "r", + | "r_prime", + | "r_prime_prime", + | "r_prime_prime_prime", + | "o", + | "o_prime", + | "m", + | "m_prime", + | "t", + | "t_prime", + | "m2", + | "s", + | "c" + | ] + | }, + | "c_list":{ + | "type":"object", + | "properties":{ + | "e":{ + | "type":"string" + | }, + | "d":{ + | "type":"string" + | }, + | "a":{ + | "type":"string" + | }, + | "g":{ + | "type":"string" + | }, + | "w":{ + | "type":"string" + | }, + | "s":{ + | "type":"string" + | }, + | "u":{ + | "type":"string" + | } + | }, + | "required":[ + | "e", + | "d", + | "a", + | "g", + | "w", + | "s", + | "u" + | ] + | } + | }, + | "required":[ + | "x_list", + | "c_list" + | ] + | }, + | { + | "type":"null" + | } + | ] + | } + | }, + | "required":[ + | "primary_proof" + | ] + | } + | }, + | "aggregated_proof":{ + | "type":"object", + | "properties":{ + | "c_hash":{ + | "type":"string" + | }, + | "c_list":{ + | "type":"array", + | "items":{ + | "type":"array", + | "items":{ + | "type":"integer" + | } + | } + | } + | }, + | "required":[ + | "c_hash", + | "c_list" + | ] + | } + | }, + | "required":[ + | "proofs", + | "aggregated_proof" + | ] + | }, + | "requested_proof":{ + | "type":"object", + | "properties":{ + | "revealed_attrs":{ + | "type":"object", + | "additionalProperties":{ + | "type":"object", + | "properties":{ + | "sub_proof_index":{ + | "type":"integer" + | }, + | "raw":{ + | "type":"string" + | }, + | "encoded":{ + | "type":"string" + | } + | }, + | "required":[ + | "sub_proof_index", + | "raw", + | "encoded" + | ] + | } + | }, + | "self_attested_attrs":{ + | "type":"object" + | }, + | "unrevealed_attrs":{ + | "type":"object" + | }, + | "predicates":{ + | "type":"object", + | "additionalProperties":{ + | "type":"object", + | "properties":{ + | "sub_proof_index":{ + | "type":"integer" + | } + | }, + | "required":[ + | "sub_proof_index" + | ] + | } + | } + | }, + | "required":[ + | "revealed_attrs", + | "self_attested_attrs", + | "unrevealed_attrs", + | "predicates" + | ] + | }, + | "identifiers":{ + | "type":"array", + | "items":{ + | "type":"object", + | "properties":{ + | "schema_id":{ + | "type":"string" + | }, + | "cred_def_id":{ + | "type":"string" + | }, + | "rev_reg_id":{ + | "type":[ + | "string", + | "null" + | ] + | }, + | "timestamp":{ + | "type":[ + | "integer", + | "null" + | ] + | } + | }, + | "required":[ + | "schema_id", + | "cred_def_id" + | ] + | } + | } + | }, + | "required":[ + | "proof", + | "requested_proof", + | "identifiers" + | ] + |} + |""".stripMargin + + val schemaSerDes: SchemaSerDes[AnoncredPresentationV1] = SchemaSerDes(schema) + + given JsonDecoder[AnoncredAdditionalPropertiesTypeStringV1] = + DeriveJsonDecoder.gen[AnoncredAdditionalPropertiesTypeStringV1] + + given JsonEncoder[AnoncredAdditionalPropertiesTypeStringV1] = + DeriveJsonEncoder.gen[AnoncredAdditionalPropertiesTypeStringV1] + + given JsonDecoder[AnoncredEqProofV1] = DeriveJsonDecoder.gen[AnoncredEqProofV1] + + given JsonEncoder[AnoncredEqProofV1] = DeriveJsonEncoder.gen[AnoncredEqProofV1] + + given JsonDecoder[AnoncredPredicateV1] = DeriveJsonDecoder.gen[AnoncredPredicateV1] + + given JsonEncoder[AnoncredPredicateV1] = DeriveJsonEncoder.gen[AnoncredPredicateV1] + + given JsonDecoder[AnoncredGeProofV1] = DeriveJsonDecoder.gen[AnoncredGeProofV1] + + given JsonEncoder[AnoncredGeProofV1] = DeriveJsonEncoder.gen[AnoncredGeProofV1] + + given JsonDecoder[AnoncredNonRevocProofXListV1] = DeriveJsonDecoder.gen[AnoncredNonRevocProofXListV1] + + given JsonEncoder[AnoncredNonRevocProofXListV1] = DeriveJsonEncoder.gen[AnoncredNonRevocProofXListV1] + + given JsonDecoder[AnoncredNonRevocProofCListV1] = DeriveJsonDecoder.gen[AnoncredNonRevocProofCListV1] + + given JsonEncoder[AnoncredNonRevocProofCListV1] = DeriveJsonEncoder.gen[AnoncredNonRevocProofCListV1] + + given JsonDecoder[AnoncredNonRevocProofV1] = DeriveJsonDecoder.gen[AnoncredNonRevocProofV1] + + given JsonEncoder[AnoncredNonRevocProofV1] = DeriveJsonEncoder.gen[AnoncredNonRevocProofV1] + + given JsonDecoder[AnoncredPrimaryProofV1] = DeriveJsonDecoder.gen[AnoncredPrimaryProofV1] + + given JsonEncoder[AnoncredPrimaryProofV1] = DeriveJsonEncoder.gen[AnoncredPrimaryProofV1] + + given JsonDecoder[AnoncredAggregatedProofV1] = DeriveJsonDecoder.gen[AnoncredAggregatedProofV1] + + given JsonEncoder[AnoncredAggregatedProofV1] = DeriveJsonEncoder.gen[AnoncredAggregatedProofV1] + + given JsonDecoder[AnoncredSubProofV1] = DeriveJsonDecoder.gen[AnoncredSubProofV1] + + given JsonEncoder[AnoncredSubProofV1] = DeriveJsonEncoder.gen[AnoncredSubProofV1] + + given JsonDecoder[AnoncredProofV1] = DeriveJsonDecoder.gen[AnoncredProofV1] + + given JsonEncoder[AnoncredProofV1] = DeriveJsonEncoder.gen[AnoncredProofV1] + + given JsonDecoder[AnoncredRevealedAttrV1] = DeriveJsonDecoder.gen[AnoncredRevealedAttrV1] + + given JsonEncoder[AnoncredRevealedAttrV1] = DeriveJsonEncoder.gen[AnoncredRevealedAttrV1] + + given JsonDecoder[AnoncredSubProofIndexV1] = DeriveJsonDecoder.gen[AnoncredSubProofIndexV1] + + given JsonEncoder[AnoncredSubProofIndexV1] = DeriveJsonEncoder.gen[AnoncredSubProofIndexV1] + + given JsonDecoder[AnoncredRequestedProofV1] = DeriveJsonDecoder.gen[AnoncredRequestedProofV1] + + given JsonEncoder[AnoncredRequestedProofV1] = DeriveJsonEncoder.gen[AnoncredRequestedProofV1] + + given JsonDecoder[AnoncredIdentifierV1] = DeriveJsonDecoder.gen[AnoncredIdentifierV1] + + given JsonEncoder[AnoncredIdentifierV1] = DeriveJsonEncoder.gen[AnoncredIdentifierV1] + + given JsonDecoder[AnoncredPresentationV1] = DeriveJsonDecoder.gen[AnoncredPresentationV1] + + given JsonEncoder[AnoncredPresentationV1] = DeriveJsonEncoder.gen[AnoncredPresentationV1] + +} diff --git a/pollux/lib/core/src/test/resources/anoncred-presentation-schema-example.json b/pollux/lib/core/src/test/resources/anoncred-presentation-schema-example.json new file mode 100644 index 0000000000..febd78d65d --- /dev/null +++ b/pollux/lib/core/src/test/resources/anoncred-presentation-schema-example.json @@ -0,0 +1,10 @@ +{ + "name": "resource:///anoncred-presentation-schema-example.json", + "version": "1.0", + "attrNames": [ + "name", + "sex", + "age" + ], + "issuerId": "did:prism:issuer" +} \ No newline at end of file diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/repository/CredentialRepositorySpecSuite.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/repository/CredentialRepositorySpecSuite.scala index b363446d36..0d0a1eb1c0 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/repository/CredentialRepositorySpecSuite.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/repository/CredentialRepositorySpecSuite.scala @@ -17,14 +17,15 @@ import java.util.UUID object CredentialRepositorySpecSuite { val maxRetries = 5 // TODO Move to config - private def issueCredentialRecord = IssueCredentialRecord( + private def issueCredentialRecord(credentialFormat: CredentialFormat) = IssueCredentialRecord( id = DidCommID(), createdAt = Instant.now, updatedAt = None, thid = DidCommID(), - schemaId = None, + schemaUri = None, credentialDefinitionId = None, - credentialFormat = CredentialFormat.JWT, + credentialDefinitionUri = None, + credentialFormat = credentialFormat, role = IssueCredentialRecord.Role.Issuer, subjectId = None, validityPeriod = None, @@ -53,7 +54,7 @@ object CredentialRepositorySpecSuite { test("createIssueCredentialRecord creates a new record in DB") { for { repo <- ZIO.service[CredentialRepository] - record = issueCredentialRecord + record = issueCredentialRecord(CredentialFormat.JWT) count <- repo.createIssueCredentialRecord(record) } yield assertTrue(count == 1) }, @@ -61,8 +62,8 @@ object CredentialRepositorySpecSuite { for { repo <- ZIO.service[CredentialRepository] thid = DidCommID() - aRecord = issueCredentialRecord.copy(thid = thid) - bRecord = issueCredentialRecord.copy(thid = thid) + aRecord = issueCredentialRecord(CredentialFormat.JWT).copy(thid = thid) + bRecord = issueCredentialRecord(CredentialFormat.JWT).copy(thid = thid) aCount <- repo.createIssueCredentialRecord(aRecord) bCount <- repo.createIssueCredentialRecord(bRecord).exit } yield assertTrue(aCount == 1) && assert(bCount)(fails(isSubtype[UniqueConstraintViolation](anything))) @@ -71,7 +72,7 @@ object CredentialRepositorySpecSuite { for { repo <- ZIO.service[CredentialRepository] issuingDID <- ZIO.fromEither(PrismDID.buildCanonicalFromSuffix("0" * 64)) - record = issueCredentialRecord.copy(issuingDID = Some(issuingDID)) + record = issueCredentialRecord(CredentialFormat.JWT).copy(issuingDID = Some(issuingDID)) count <- repo.createIssueCredentialRecord(record) readRecord <- repo.getIssueCredentialRecord(record.id) } yield assertTrue(count == 1) && assert(readRecord)(isSome(equalTo(record))) @@ -79,8 +80,8 @@ object CredentialRepositorySpecSuite { test("getIssueCredentialRecord correctly returns an existing record") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord - bRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) + bRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) _ <- repo.createIssueCredentialRecord(bRecord) record <- repo.getIssueCredentialRecord(bRecord.id) @@ -89,8 +90,8 @@ object CredentialRepositorySpecSuite { test("getIssuanceCredentialRecord returns None for an unknown record") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord - bRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) + bRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) _ <- repo.createIssueCredentialRecord(bRecord) record <- repo.getIssueCredentialRecord(DidCommID()) @@ -99,8 +100,8 @@ object CredentialRepositorySpecSuite { test("getIssuanceCredentialRecord returns all records") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord - bRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) + bRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) _ <- repo.createIssueCredentialRecord(bRecord) records <- repo.getIssueCredentialRecords(false).map(_._1) @@ -113,8 +114,8 @@ object CredentialRepositorySpecSuite { test("getIssuanceCredentialRecord returns records with offset") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord - bRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) + bRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) _ <- repo.createIssueCredentialRecord(bRecord) records <- repo.getIssueCredentialRecords(false, offset = Some(1)).map(_._1) @@ -126,8 +127,8 @@ object CredentialRepositorySpecSuite { test("getIssuanceCredentialRecord returns records with limit") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord - bRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) + bRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) _ <- repo.createIssueCredentialRecord(bRecord) records <- repo.getIssueCredentialRecords(false, limit = Some(1)).map(_._1) @@ -139,9 +140,9 @@ object CredentialRepositorySpecSuite { test("getIssuanceCredentialRecord returns records with offset and limit") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord - bRecord = issueCredentialRecord - cRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) + bRecord = issueCredentialRecord(CredentialFormat.JWT) + cRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) _ <- repo.createIssueCredentialRecord(bRecord) _ <- repo.createIssueCredentialRecord(cRecord) @@ -156,8 +157,8 @@ object CredentialRepositorySpecSuite { test("deleteIssueCredentialRecord deletes an exsiting record") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord - bRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) + bRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) _ <- repo.createIssueCredentialRecord(bRecord) count <- repo.deleteIssueCredentialRecord(aRecord.id) @@ -171,8 +172,8 @@ object CredentialRepositorySpecSuite { test("deleteIssueCredentialRecord does nothing for an unknown record") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord - bRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) + bRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) _ <- repo.createIssueCredentialRecord(bRecord) count <- repo.deleteIssueCredentialRecord(DidCommID()) @@ -188,8 +189,8 @@ object CredentialRepositorySpecSuite { for { repo <- ZIO.service[CredentialRepository] thid = DidCommID() - aRecord = issueCredentialRecord.copy(thid = thid) - bRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT).copy(thid = thid) + bRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) _ <- repo.createIssueCredentialRecord(bRecord) record <- repo.getIssueCredentialRecordByThreadId(thid, false) @@ -198,8 +199,8 @@ object CredentialRepositorySpecSuite { test("getIssueCredentialRecordByThreadId returns nothing for an unknown thid") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord - bRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) + bRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) _ <- repo.createIssueCredentialRecord(bRecord) record <- repo.getIssueCredentialRecordByThreadId(DidCommID(), false) @@ -208,9 +209,9 @@ object CredentialRepositorySpecSuite { test("getIssueCredentialRecordsByStates returns valid records") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord - bRecord = issueCredentialRecord - cRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) + bRecord = issueCredentialRecord(CredentialFormat.JWT) + cRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) _ <- repo.createIssueCredentialRecord(bRecord) _ <- repo.createIssueCredentialRecord(cRecord) @@ -242,9 +243,9 @@ object CredentialRepositorySpecSuite { test("getIssueCredentialRecordsByStates returns an empty list if 'states' parameter is empty") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord - bRecord = issueCredentialRecord - cRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) + bRecord = issueCredentialRecord(CredentialFormat.JWT) + cRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) _ <- repo.createIssueCredentialRecord(bRecord) _ <- repo.createIssueCredentialRecord(cRecord) @@ -256,30 +257,55 @@ object CredentialRepositorySpecSuite { test("getValidIssuedCredentials returns valid records") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord - bRecord = issueCredentialRecord - cRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) + bRecord = issueCredentialRecord(CredentialFormat.JWT) + cRecord = issueCredentialRecord(CredentialFormat.JWT) + dRecord = issueCredentialRecord(CredentialFormat.AnonCreds) _ <- repo.createIssueCredentialRecord(aRecord) _ <- repo.createIssueCredentialRecord(bRecord) _ <- repo.createIssueCredentialRecord(cRecord) + _ <- repo.createIssueCredentialRecord(dRecord) _ <- repo.updateWithIssuedRawCredential( aRecord.id, IssueCredential.makeIssueCredentialFromRequestCredential(requestCredential.makeMessage), "RAW_CREDENTIAL_DATA", ProtocolState.CredentialReceived ) - records <- repo.getValidIssuedCredentials(Seq(aRecord.id, bRecord.id)) + _ <- repo.updateWithIssuedRawCredential( + dRecord.id, + IssueCredential.makeIssueCredentialFromRequestCredential(requestCredential.makeMessage), + "RAW_CREDENTIAL_DATA", + ProtocolState.CredentialReceived + ) + records <- repo.getValidIssuedCredentials(Seq(aRecord.id, bRecord.id, dRecord.id)) } yield { - assertTrue(records.size == 1) && + assertTrue(records.size == 2) && assertTrue( - records.contains(ValidIssuedCredentialRecord(aRecord.id, Some("RAW_CREDENTIAL_DATA"), aRecord.subjectId)) + records.contains( + ValidIssuedCredentialRecord( + dRecord.id, + Some("RAW_CREDENTIAL_DATA"), + CredentialFormat.JWT, + aRecord.subjectId + ) + ) + ) + assertTrue( + records.contains( + ValidIssuedCredentialRecord( + dRecord.id, + Some("RAW_CREDENTIAL_DATA"), + CredentialFormat.AnonCreds, + aRecord.subjectId + ) + ) ) } }, test("updateCredentialRecordProtocolState updates the record") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) record <- repo.getIssueCredentialRecord(aRecord.id) count <- repo.updateCredentialRecordProtocolState( @@ -297,7 +323,7 @@ object CredentialRepositorySpecSuite { test("updateCredentialRecordProtocolState doesn't update the record for invalid from state") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) record <- repo.getIssueCredentialRecord(aRecord.id) count <- repo.updateCredentialRecordProtocolState( @@ -315,7 +341,7 @@ object CredentialRepositorySpecSuite { test("updateWithRequestCredential updates record") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) record <- repo.getIssueCredentialRecord(aRecord.id) request = requestCredential @@ -334,7 +360,7 @@ object CredentialRepositorySpecSuite { test("updateWithIssueCredential updates record") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) record <- repo.getIssueCredentialRecord(aRecord.id) issueCredential = IssueCredential.makeIssueCredentialFromRequestCredential(requestCredential.makeMessage) @@ -353,7 +379,7 @@ object CredentialRepositorySpecSuite { test("updateWithIssuedRawCredential updates record") { for { repo <- ZIO.service[CredentialRepository] - aRecord = issueCredentialRecord + aRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aRecord) record <- repo.getIssueCredentialRecord(aRecord.id) issueCredential = IssueCredential.makeIssueCredentialFromRequestCredential(requestCredential.makeMessage) @@ -372,7 +398,7 @@ object CredentialRepositorySpecSuite { } }, test("updateFail (fail one retry) updates record") { - val aRecord = issueCredentialRecord + val aRecord = issueCredentialRecord(CredentialFormat.JWT) val failReason = Some("Just to test") for { @@ -402,7 +428,7 @@ object CredentialRepositorySpecSuite { } }, test("updateFail (fail all retry) updates record") { - val aRecord = issueCredentialRecord + val aRecord = issueCredentialRecord(CredentialFormat.JWT) for { repo <- ZIO.service[CredentialRepository] @@ -443,8 +469,8 @@ object CredentialRepositorySpecSuite { val wallet2 = ZLayer.succeed(WalletAccessContext(walletId2)) for { repo <- ZIO.service[CredentialRepository] - record1 = issueCredentialRecord - record2 = issueCredentialRecord + record1 = issueCredentialRecord(CredentialFormat.JWT) + record2 = issueCredentialRecord(CredentialFormat.JWT) count1 <- repo.createIssueCredentialRecord(record1).provide(wallet1) count2 <- repo.createIssueCredentialRecord(record2).provide(wallet2) ownWalletRecords1 <- repo.getIssueCredentialRecords(false).provide(wallet1) @@ -466,8 +492,8 @@ object CredentialRepositorySpecSuite { val newState = IssueCredentialRecord.ProtocolState.OfferReceived for { repo <- ZIO.service[CredentialRepository] - record1 = issueCredentialRecord - record2 = issueCredentialRecord + record1 = issueCredentialRecord(CredentialFormat.JWT) + record2 = issueCredentialRecord(CredentialFormat.JWT) count1 <- repo.createIssueCredentialRecord(record1).provide(wallet1) update1 <- repo.updateWithSubjectId(record2.id, "my-id", newState).provide(wallet2) update2 <- repo.updateAfterFail(record2.id, Some("fail reason")).provide(wallet2) @@ -486,7 +512,7 @@ object CredentialRepositorySpecSuite { val wallet2 = ZLayer.succeed(WalletAccessContext(walletId2)) for { repo <- ZIO.service[CredentialRepository] - record1 = issueCredentialRecord + record1 = issueCredentialRecord(CredentialFormat.JWT) count1 <- repo.createIssueCredentialRecord(record1).provide(wallet1) delete1 <- repo.deleteIssueCredentialRecord(record1.id).provide(wallet2) } yield assert(count1)(equalTo(1)) && assert(delete1)(isZero) @@ -498,8 +524,8 @@ object CredentialRepositorySpecSuite { val wallet2 = ZLayer.succeed(WalletAccessContext(walletId2)) for { repo <- ZIO.service[CredentialRepository] - record1 = issueCredentialRecord - record2 = issueCredentialRecord + record1 = issueCredentialRecord(CredentialFormat.JWT) + record2 = issueCredentialRecord(CredentialFormat.JWT) count1 <- repo.createIssueCredentialRecord(record1).provide(wallet1) count2 <- repo.createIssueCredentialRecord(record2).provide(wallet2) _ <- repo diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/repository/PresentationRepositorySpecSuite.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/repository/PresentationRepositorySpecSuite.scala index f1e0ed5b7d..b396ee05f5 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/repository/PresentationRepositorySpecSuite.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/repository/PresentationRepositorySpecSuite.scala @@ -4,8 +4,8 @@ import io.iohk.atala.mercury.model.DidId import io.iohk.atala.mercury.protocol.presentproof.{Presentation, ProposePresentation, RequestPresentation} import io.iohk.atala.pollux.core.model.* import io.iohk.atala.pollux.core.model.PresentationRecord.* -import io.iohk.atala.shared.models.WalletAccessContext -import io.iohk.atala.shared.models.WalletId +import io.iohk.atala.pollux.core.service.serdes.{AnoncredCredentialProofV1, AnoncredCredentialProofsV1} +import io.iohk.atala.shared.models.{WalletAccessContext, WalletId} import zio.test.* import zio.test.Assertion.* import zio.{ZIO, ZLayer} @@ -31,6 +31,8 @@ object PresentationRepositorySpecSuite { proposePresentationData = None, presentationData = None, credentialsToUse = None, + anoncredCredentialsToUseJsonSchemaId = None, + anoncredCredentialsToUse = None, metaRetries = maxRetries, metaNextRetry = Some(Instant.now()), metaLastFailure = None, @@ -195,6 +197,34 @@ object PresentationRepositorySpecSuite { assertTrue(records.exists(_.credentialsToUse.contains(Seq("credential1", "credential2")))) } }, + test("updatePresentationWithCredentialsToUse updates the record") { + for { + repo <- ZIO.service[PresentationRepository] + aRecord = presentationRecord + bRecord = presentationRecord + cRecord = presentationRecord + _ <- repo.createPresentationRecord(aRecord) + _ <- repo.createPresentationRecord(bRecord) + _ <- repo.createPresentationRecord(cRecord) + anoncredCredentialProofs = AnoncredCredentialProofsV1( + List(AnoncredCredentialProofV1("credential1", Seq("requestedAttribute"), Seq("requestedPredicate"))) + ) + anoncredCredentialProofsJson <- ZIO.fromEither( + AnoncredCredentialProofsV1.schemaSerDes.serialize(anoncredCredentialProofs) + ) + _ <- repo.updateAnoncredPresentationWithCredentialsToUse( + aRecord.id, + Some(AnoncredCredentialProofsV1.version), + Some(anoncredCredentialProofsJson), + ProtocolState.PresentationPending + ) + records <- repo.getPresentationRecord(aRecord.id) + } yield { + assertTrue(records.size == 1) && + assertTrue(records.exists(_.anoncredCredentialsToUse.contains(anoncredCredentialProofsJson))) && + assertTrue(records.exists(_.anoncredCredentialsToUseJsonSchemaId.contains(AnoncredCredentialProofsV1.version))) + } + }, test("updateCredentialRecordProtocolState updates the record") { for { repo <- ZIO.service[PresentationRepository] diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceImplSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceImplSpec.scala index 7707cb107f..94840909fb 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceImplSpec.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceImplSpec.scala @@ -9,7 +9,7 @@ import io.iohk.atala.castor.core.service.MockDIDService import io.iohk.atala.mercury.model import io.iohk.atala.mercury.model.* import io.iohk.atala.mercury.protocol.issuecredential.* -import io.iohk.atala.pollux.anoncreds.Credential +import io.iohk.atala.pollux.anoncreds.AnoncredCredential import io.iohk.atala.pollux.core.model.* import io.iohk.atala.pollux.core.model.IssueCredentialRecord.{ProtocolState, Role} import io.iohk.atala.pollux.core.model.error.CredentialServiceError @@ -82,7 +82,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS } yield { assertTrue(record.thid == thid) && assertTrue(record.updatedAt.isEmpty) && - assertTrue(record.schemaId.isEmpty) && + assertTrue(record.schemaUri.isEmpty) && assertTrue(record.validityPeriod == validityPeriod) && assertTrue(record.automaticIssuance == automaticIssuance) && assertTrue(record.role == Role.Issuer) && @@ -158,7 +158,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS assertTrue(record.thid == thid) && assertTrue(record.updatedAt.isEmpty) && assertTrue( - record.schemaId.contains("resource:///vc-schema-example.json") + record.schemaUri.contains("resource:///vc-schema-example.json") ) && assertTrue(record.validityPeriod == validityPeriod) && assertTrue(record.automaticIssuance == automaticIssuance) && @@ -290,7 +290,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS } yield { assertTrue(holderRecord.thid.toString == offer.thid.get) && assertTrue(holderRecord.updatedAt.isEmpty) && - assertTrue(holderRecord.schemaId.isEmpty) && + assertTrue(holderRecord.schemaUri.isEmpty) && assertTrue(holderRecord.validityPeriod.isEmpty) && assertTrue(holderRecord.automaticIssuance.isEmpty) && assertTrue(holderRecord.role == Role.Holder) && @@ -605,7 +605,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS assertTrue(record.issueCredentialData.get.attachments.head.data match case model.Base64(value) => val ba = new String(Base64.getUrlDecoder.decode(value)) - Credential(ba).credDefId == credDefId + AnoncredCredential(ba).credDefId == credDefId case _ => false ) } diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifierSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifierSpec.scala index b2babb80ca..4bbba5c2b0 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifierSpec.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/CredentialServiceNotifierSpec.scala @@ -21,6 +21,7 @@ object CredentialServiceNotifierSpec extends MockSpecDefault with CredentialServ DidCommID(), None, None, + None, CredentialFormat.JWT, IssueCredentialRecord.Role.Issuer, None, diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/LinkSecretServiceImplSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/LinkSecretServiceImplSpec.scala index c6ea4e6b49..c65219bc37 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/LinkSecretServiceImplSpec.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/LinkSecretServiceImplSpec.scala @@ -2,7 +2,7 @@ package io.iohk.atala.pollux.core.service import io.iohk.atala.agent.walletapi.memory.GenericSecretStorageInMemory import io.iohk.atala.agent.walletapi.storage.GenericSecretStorage -import io.iohk.atala.pollux.anoncreds.LinkSecret +import io.iohk.atala.pollux.anoncreds.AnoncredLinkSecret import io.iohk.atala.shared.models.WalletId.* import io.iohk.atala.shared.models.{WalletAccessContext, WalletId} import zio.* @@ -30,7 +30,7 @@ object LinkSecretServiceImplSpec extends ZIOSpecDefault { record1 <- svc.fetchOrCreate() storage <- ZIO.service[GenericSecretStorage] maybeDidSecret <- storage - .get[String, LinkSecret](LinkSecretServiceImpl.defaultLinkSecretId) + .get[String, AnoncredLinkSecret](LinkSecretServiceImpl.defaultLinkSecretId) } yield { assertTrue(record.id == LinkSecretServiceImpl.defaultLinkSecretId) assertTrue(record == record1) 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..f08aa46f16 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 @@ -2,7 +2,7 @@ package io.iohk.atala.pollux.core.service import io.iohk.atala.event.notification.{EventNotificationService, EventNotificationServiceImpl} import io.iohk.atala.mercury.model.DidId -import io.iohk.atala.mercury.protocol.presentproof.{Presentation, RequestPresentation} +import io.iohk.atala.mercury.protocol.presentproof.{PresentCredentialRequestFormat, Presentation, RequestPresentation} import io.iohk.atala.pollux.core.model.PresentationRecord.ProtocolState import io.iohk.atala.pollux.core.model.{CredentialFormat, DidCommID, PresentationRecord} import zio.mock.Expectation @@ -30,13 +30,15 @@ object PresentationServiceNotifierSpec extends ZIOSpecDefault with PresentationS None, None, None, + None, + None, 5, None, None ) private val verifierHappyFlowExpectations = - MockPresentationService.CreatePresentationRecord( + MockPresentationService.CreateJwtPresentationRecord( assertion = Assertion.anything, result = Expectation.value(record) ) ++ @@ -100,14 +102,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)) @@ -170,7 +171,7 @@ object PresentationServiceNotifierSpec extends ZIOSpecDefault with PresentationS svc <- ZIO.service[PresentationService] ens <- ZIO.service[EventNotificationService] - _ <- svc.receiveRequestPresentation(None, requestPresentationJWT) + _ <- svc.receiveRequestPresentation(None, requestPresentation(PresentCredentialRequestFormat.JWT)) _ <- svc.acceptRequestPresentation(record.id, Seq.empty) _ <- svc.markPresentationGenerated(record.id, presentation(record.thid.value)) _ <- svc.markPresentationSent(record.id) 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..1032557c38 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,33 +2,47 @@ 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.protocol.issuecredential.IssueCredential +import io.iohk.atala.agent.walletapi.storage.GenericSecretStorage +import io.iohk.atala.mercury.model.{AttachmentDescriptor, Base64, DidId} +import io.iohk.atala.mercury.protocol.issuecredential.{IssueCredential, IssueCredentialIssuedFormat} import io.iohk.atala.mercury.protocol.presentproof.* +import io.iohk.atala.pollux.anoncreds.* import io.iohk.atala.pollux.core.model.* import io.iohk.atala.pollux.core.model.IssueCredentialRecord.* import io.iohk.atala.pollux.core.model.PresentationRecord.* 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.model.schema.CredentialDefinition.Input +import io.iohk.atala.pollux.core.model.secret.CredentialDefinitionSecret import io.iohk.atala.pollux.core.repository.{CredentialRepository, PresentationRepository} +import io.iohk.atala.pollux.core.service.serdes.{ + AnoncredCredentialProofV1, + AnoncredCredentialProofsV1, + AnoncredPresentationRequestV1, + AnoncredPresentationV1 +} 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.nio.charset.StandardCharsets +import java.nio.file.{Files, Path, Paths} +import java.time.{Instant, OffsetDateTime} +import java.util.{UUID, Base64 as JBase64} object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSpecHelper { - override def spec = - suite("PresentationService")(singleWalletSpec, multiWalletSpec).provide(presentationServiceLayer) + override def spec: Spec[Any, Any] = + suite("PresentationService")(singleWalletSpec, multiWalletSpec).provide( + presentationServiceLayer ++ genericSecretStorageLayer + ) 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) @@ -45,7 +59,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp } yield Options(challenge, domain) check( - Gen.uuid.map(e => DidCommID(e.toString())), + Gen.uuid.map(e => DidCommID(e.toString)), Gen.option(Gen.string), Gen.listOfBounded(1, 5)(proofTypeGen), Gen.option(optionsGen) @@ -54,14 +68,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) && @@ -75,7 +88,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp assertTrue(record.requestPresentationData.get.body.goal_code.contains("Request Proof Presentation")) && assertTrue(record.requestPresentationData.get.body.proof_types == proofTypes) && assertTrue( - if (record.requestPresentationData.get.attachments.length != 0) { + if (record.requestPresentationData.get.attachments.nonEmpty) { val maybePresentationOptions = record.requestPresentationData.get.attachments.headOption .map(attachment => @@ -100,12 +113,75 @@ 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 + .serializeToJsonString(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() + _ <- svc.createJwtRecord() + _ <- svc.createJwtRecord() records <- svc.getPresentationRecords(false) } yield { assertTrue(records.size == 2) @@ -114,7 +190,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,23 +208,23 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp test("getPresentationRecord returns the correct record") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() - bRecord <- svc.createRecord() + _ <- 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() + _ <- svc.createJwtRecord() + _ <- svc.createJwtRecord() record <- svc.getPresentationRecord(DidCommID()) } yield assertTrue(record.isEmpty) }, - test("createPresentationPayloadFromRecord returns jwt prsentation payload") { + test("createJwtPresentationPayloadFromRecord returns jwt presentation payload") { for { repo <- ZIO.service[CredentialRepository] - aIssueCredentialRecord = issueCredentialRecord + aIssueCredentialRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aIssueCredentialRecord) rawCredentialData = """{"base64":"ZXlKaGJHY2lPaUpGVXpJMU5rc2lMQ0owZVhBaU9pSktWMVFpZlEuZXlKcFlYUWlPakUyTnprek1qYzROaklzSW1GMVpDSTZJbVJ2YldGcGJpSXNJbTV2Ym1ObElqb2lZMlk1T1RJMk56Z3RPREV3TmkwME1EZzVMV0UxWXprdE5tTmhObU0wWkRBMU1HVTBJaXdpZG5BaU9uc2lRR052Ym5SbGVIUWlPbHNpYUhSMGNITTZMeTkzZDNjdWR6TXViM0puTHpJd01UZ3ZjSEpsYzJWdWRHRjBhVzl1Y3k5Mk1TSmRMQ0owZVhCbElqcGJJbFpsY21sbWFXRmliR1ZRY21WelpXNTBZWFJwYjI0aVhYMHNJbWx6Y3lJNkltUnBaRHB3Y21semJUcGhaR0psT1RJNE9XUXdZelZtWWpVMlptWmhOVEF6T0Rka01UZ3dOR0ZpTkdFeE5UYzJOVEkzWXprME5tRTFNalV5T0RFM1ptRTRaVGhoTW1OalpXUXdPa056YzBKRGMyZENSVzFKUzBSWE1XaGpNMUpzWTJsb2NHSnRVbXhsUTJ0UlFWVktVRU5uYkZSYVYwNTNUV3BWTW1GNlJWTkpSUzFNYVVkTU0xRklaRlZ1VG10d1dXSkthSE5VYTIxWVVGaEpVM0ZXZWpjMll6RlZPWGhvVURseWNFZHBSSEZXTlRselJYcEtWbEpEYWxJMGEwMHdaMGg0YkhWUU5tVk5Ta2wwZHpJMk4yWllWbEpoTUhoRE5XaEthVU5uTVhSWldFNHdXbGhKYjJGWE5XdGFXR2R3UlVGU1ExUjNiMHBWTWxacVkwUkpNVTV0YzNoRmFVSlFhVFJvYVRrd1FqTldTbnBhUzFkSGVWbGlSVFZLYkhveGVVVnhiR010TFc1T1ZsQmpXVlJmWVRaU2IyYzJiR1ZtWWtKTmVWWlZVVzh3WlVwRVRrbENPRnBpYWkxdWFrTlRUR05PZFhVek1URlZWM1JOVVhWWkluMC5CcmFpbEVXa2VlSXhWbjY3dnpkVHZGTXpBMV9oNzFoaDZsODBHRFBpbkRaVVB4ajAxSC0tUC1QZDIxTk9wRDd3am51SDkxdUNBOFZMUW9fS2FnVjlnQQo="}""" @@ -159,7 +235,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, @@ -167,16 +243,48 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp PresentationRecord.ProtocolState.RequestPending ) issuer = createIssuer(DID("did:prism:issuer")) - aPresentationPayload <- svc.createPresentationPayloadFromRecord(aRecord.id, issuer, Instant.now()) + aPresentationPayload <- svc.createJwtPresentationPayloadFromRecord(aRecord.id, issuer, Instant.now()) } yield { assertTrue(aPresentationPayload.toJwtPresentationPayload.iss == "did:prism:issuer") } }, + test("createAnoncredPresentationPayloadFromRecord returns Anoncred presentation payload") { + for { + presentationWithRecord <- createAnoncredPresentation + (presentation, _) = presentationWithRecord + serializedPresentation <- presentation.attachments.head.data match { + case Base64(data) => ZIO.succeed(AnoncredPresentation(new String(JBase64.getUrlDecoder.decode(data)))) + case _ => ZIO.fail(InvalidAnoncredPresentation("Expecting Base64-encoded data")) + } + validation <- AnoncredPresentationV1.schemaSerDes.validate(serializedPresentation.data) + presentation <- AnoncredPresentationV1.schemaSerDes.deserialize(serializedPresentation.data) + } yield { + assert(validation)(isUnit) + assert( + presentation.proof.proofs.headOption.flatMap(_.primary_proof.eq_proof.revealed_attrs.headOption.map(_._1)) + )(isSome(equalTo("sex"))) + } + }, + test("verify anoncred presentation") { + for { + presentationWithRecord <- createAnoncredPresentation + (presentation, aRecord) = presentationWithRecord + svc <- ZIO.service[PresentationService] + _ <- svc.receivePresentation(presentation) + validateRecord <- + svc.verifyAnoncredPresentation( + presentation, + aRecord.requestPresentationData.get, + aRecord.id + ) + } yield { + assert(validateRecord.protocolState)(equalTo(PresentationRecord.ProtocolState.PresentationVerified)) + } + }, test("markRequestPresentationSent returns updated PresentationRecord") { for { svc <- ZIO.service[PresentationService] - pairwiseProverDid = DidId("did:peer:Prover") - record <- svc.createRecord() + record <- svc.createJwtRecord() record <- svc.markRequestPresentationSent(record.id) } yield { @@ -186,8 +294,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp test("markRequestPresentationRejected returns updated PresentationRecord") { for { svc <- ZIO.service[PresentationService] - pairwiseProverDid = DidId("did:peer:Prover") - record <- svc.createRecord() + record <- svc.createJwtRecord() repo <- ZIO.service[PresentationRepository] _ <- repo.updatePresentationRecordProtocolState( record.id, @@ -257,7 +364,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp fails(equalTo(UnsupportedCredentialFormat(vcFormat = "Some/UnsupportedCredentialFormat"))) ) }, - test("receiveRequestPresentation updates the RequestPresentation in PresentatinRecord") { + test("receiveRequestPresentation JWT updates the RequestPresentation in PresentationRecord") { for { svc <- ZIO.service[PresentationService] connectionId = Some("connectionId") @@ -285,13 +392,78 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp } yield { assertTrue(aRecord.connectionId == connectionId) && assertTrue(aRecord.protocolState == PresentationRecord.ProtocolState.RequestReceived) && - assertTrue(aRecord.requestPresentationData == Some(requestPresentation)) + assertTrue(aRecord.requestPresentationData.contains(requestPresentation)) + } + }, + test("receiveRequestPresentation Anoncred updates the RequestPresentation in PresentationRecord") { + val anoncredPresentationRequestV1 = AnoncredPresentationRequestV1( + Map.empty, + Map.empty, + "name", + "nonce", + "version", + None + ) + val attachmentDescriptor = AttachmentDescriptor.buildBase64Attachment( + mediaType = Some("application/json"), + format = Some(PresentCredentialRequestFormat.Anoncred.name), + payload = + AnoncredPresentationRequestV1.schemaSerDes.serializeToJsonString(anoncredPresentationRequestV1).getBytes() + ) + val connectionId = Some("connectionId") + for { + svc <- ZIO.service[PresentationService] + requestPresentationWithRecord <- receiveRequestPresentationTest(attachmentDescriptor) + (_, requestPresentation) = requestPresentationWithRecord + aRecord <- svc.receiveRequestPresentation(connectionId, requestPresentation) + + } yield { + assertTrue(aRecord.connectionId == connectionId) && + assertTrue(aRecord.protocolState == PresentationRecord.ProtocolState.RequestReceived) && + assertTrue(aRecord.requestPresentationData.contains(requestPresentation)) } }, - test("acceptRequestPresentation updates the PresentatinRecord JWT") { + test("receiveRequestPresentation Anoncred should fail given invalid attachment") { + + val presentationAttachmentAsJson = + """{ + "challenge": "1f44d55f-f161-4938-a659-f8026467f126", + "domain": "us.gov/DriverLicense", + "credential_manifest": {} + }""" + + val attachmentDescriptor = AttachmentDescriptor.buildJsonAttachment( + payload = presentationAttachmentAsJson, + format = Some(PresentCredentialProposeFormat.Anoncred.name) + ) + for { + requestPresentation <- receiveRequestPresentationTest(attachmentDescriptor).exit + } yield assert(requestPresentation)( + fails(isSubtype[InvalidAnoncredPresentationRequest](anything)) + ) + }, + test("receiveRequestPresentation Anoncred should fail given invalid anoncred format") { + val presentationAttachmentAsJson = + """{ + "challenge": "1f44d55f-f161-4938-a659-f8026467f126", + "domain": "us.gov/DriverLicense", + "credential_manifest": {} + }""" + val attachmentDescriptor = AttachmentDescriptor.buildBase64Attachment( + mediaType = Some("application/json"), + format = Some(PresentCredentialRequestFormat.Anoncred.name), + payload = presentationAttachmentAsJson.getBytes() + ) + for { + requestPresentation <- receiveRequestPresentationTest(attachmentDescriptor).exit + } yield assert(requestPresentation)( + fails(isSubtype[InvalidAnoncredPresentationRequest](anything)) + ) + }, + test("acceptRequestPresentation updates the PresentationRecord JWT") { for { repo <- ZIO.service[CredentialRepository] - aIssueCredentialRecord = issueCredentialRecord + aIssueCredentialRecord = issueCredentialRecord(CredentialFormat.JWT) _ <- repo.createIssueCredentialRecord(aIssueCredentialRecord) rawCredentialData = """{"base64":"ZXlKaGJHY2lPaUpGVXpJMU5rc2lMQ0owZVhBaU9pSktWMVFpZlEuZXlKcFlYUWlPakUyTnprek1qYzROaklzSW1GMVpDSTZJbVJ2YldGcGJpSXNJbTV2Ym1ObElqb2lZMlk1T1RJMk56Z3RPREV3TmkwME1EZzVMV0UxWXprdE5tTmhObU0wWkRBMU1HVTBJaXdpZG5BaU9uc2lRR052Ym5SbGVIUWlPbHNpYUhSMGNITTZMeTkzZDNjdWR6TXViM0puTHpJd01UZ3ZjSEpsYzJWdWRHRjBhVzl1Y3k5Mk1TSmRMQ0owZVhCbElqcGJJbFpsY21sbWFXRmliR1ZRY21WelpXNTBZWFJwYjI0aVhYMHNJbWx6Y3lJNkltUnBaRHB3Y21semJUcGhaR0psT1RJNE9XUXdZelZtWWpVMlptWmhOVEF6T0Rka01UZ3dOR0ZpTkdFeE5UYzJOVEkzWXprME5tRTFNalV5T0RFM1ptRTRaVGhoTW1OalpXUXdPa056YzBKRGMyZENSVzFKUzBSWE1XaGpNMUpzWTJsb2NHSnRVbXhsUTJ0UlFWVktVRU5uYkZSYVYwNTNUV3BWTW1GNlJWTkpSUzFNYVVkTU0xRklaRlZ1VG10d1dXSkthSE5VYTIxWVVGaEpVM0ZXZWpjMll6RlZPWGhvVURseWNFZHBSSEZXTlRselJYcEtWbEpEYWxJMGEwMHdaMGg0YkhWUU5tVk5Ta2wwZHpJMk4yWllWbEpoTUhoRE5XaEthVU5uTVhSWldFNHdXbGhKYjJGWE5XdGFXR2R3UlVGU1ExUjNiMHBWTWxacVkwUkpNVTV0YzNoRmFVSlFhVFJvYVRrd1FqTldTbnBhUzFkSGVWbGlSVFZLYkhveGVVVnhiR010TFc1T1ZsQmpXVlJmWVRaU2IyYzJiR1ZtWWtKTmVWWlZVVzh3WlVwRVRrbENPRnBpYWkxdWFrTlRUR05PZFhVek1URlZWM1JOVVhWWkluMC5CcmFpbEVXa2VlSXhWbjY3dnpkVHZGTXpBMV9oNzFoaDZsODBHRFBpbkRaVVB4ajAxSC0tUC1QZDIxTk9wRDd3am51SDkxdUNBOFZMUW9fS2FnVjlnQQo="}""" @@ -304,7 +476,10 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp svc <- ZIO.service[PresentationService] connectionId = Some("connectionId") - aRecord <- svc.receiveRequestPresentation(connectionId, requestPresentationJWT) + aRecord <- svc.receiveRequestPresentation( + connectionId, + requestPresentation(PresentCredentialRequestFormat.JWT) + ) credentialsToUse = Seq(aIssueCredentialRecord.id.value) updateRecord <- svc.acceptRequestPresentation(aRecord.id, credentialsToUse) @@ -314,11 +489,114 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp assertTrue(updateRecord.credentialsToUse.contains(credentialsToUse)) } }, - test("rejectRequestPresentation updates the PresentatinRecord") { + test("acceptRequestPresentation updates the PresentationRecord AnonCreds") { for { + repo <- ZIO.service[CredentialRepository] + aIssueCredentialRecord = issueCredentialRecord(CredentialFormat.AnonCreds) + _ <- repo.createIssueCredentialRecord(aIssueCredentialRecord) + rawCredentialData = + """{"base64":"ZXlKaGJHY2lPaUpGVXpJMU5rc2lMQ0owZVhBaU9pSktWMVFpZlEuZXlKcFlYUWlPakUyTnprek1qYzROaklzSW1GMVpDSTZJbVJ2YldGcGJpSXNJbTV2Ym1ObElqb2lZMlk1T1RJMk56Z3RPREV3TmkwME1EZzVMV0UxWXprdE5tTmhObU0wWkRBMU1HVTBJaXdpZG5BaU9uc2lRR052Ym5SbGVIUWlPbHNpYUhSMGNITTZMeTkzZDNjdWR6TXViM0puTHpJd01UZ3ZjSEpsYzJWdWRHRjBhVzl1Y3k5Mk1TSmRMQ0owZVhCbElqcGJJbFpsY21sbWFXRmliR1ZRY21WelpXNTBZWFJwYjI0aVhYMHNJbWx6Y3lJNkltUnBaRHB3Y21semJUcGhaR0psT1RJNE9XUXdZelZtWWpVMlptWmhOVEF6T0Rka01UZ3dOR0ZpTkdFeE5UYzJOVEkzWXprME5tRTFNalV5T0RFM1ptRTRaVGhoTW1OalpXUXdPa056YzBKRGMyZENSVzFKUzBSWE1XaGpNMUpzWTJsb2NHSnRVbXhsUTJ0UlFWVktVRU5uYkZSYVYwNTNUV3BWTW1GNlJWTkpSUzFNYVVkTU0xRklaRlZ1VG10d1dXSkthSE5VYTIxWVVGaEpVM0ZXZWpjMll6RlZPWGhvVURseWNFZHBSSEZXTlRselJYcEtWbEpEYWxJMGEwMHdaMGg0YkhWUU5tVk5Ta2wwZHpJMk4yWllWbEpoTUhoRE5XaEthVU5uTVhSWldFNHdXbGhKYjJGWE5XdGFXR2R3UlVGU1ExUjNiMHBWTWxacVkwUkpNVTV0YzNoRmFVSlFhVFJvYVRrd1FqTldTbnBhUzFkSGVWbGlSVFZLYkhveGVVVnhiR010TFc1T1ZsQmpXVlJmWVRaU2IyYzJiR1ZtWWtKTmVWWlZVVzh3WlVwRVRrbENPRnBpYWkxdWFrTlRUR05PZFhVek1URlZWM1JOVVhWWkluMC5CcmFpbEVXa2VlSXhWbjY3dnpkVHZGTXpBMV9oNzFoaDZsODBHRFBpbkRaVVB4ajAxSC0tUC1QZDIxTk9wRDd3am51SDkxdUNBOFZMUW9fS2FnVjlnQQo="}""" + _ <- repo.updateWithIssuedRawCredential( + aIssueCredentialRecord.id, + IssueCredential.makeIssueCredentialFromRequestCredential(requestCredential.makeMessage), + rawCredentialData, + IssueCredentialRecord.ProtocolState.CredentialReceived + ) + svc <- ZIO.service[PresentationService] + connectionId = Some("connectionId") + anoncredPresentationRequestV1 = AnoncredPresentationRequestV1( + Map.empty, + Map.empty, + "name", + "nonce", + "version", + None + ) + attachmentDescriptor = AttachmentDescriptor.buildBase64Attachment( + mediaType = Some("application/json"), + format = Some(PresentCredentialRequestFormat.Anoncred.name), + payload = + AnoncredPresentationRequestV1.schemaSerDes.serializeToJsonString(anoncredPresentationRequestV1).getBytes() + ) + requestPresentation = RequestPresentation( + body = RequestPresentation.Body(goal_code = Some("Presentation Request")), + attachments = Seq(attachmentDescriptor), + to = DidId("did:peer:Prover"), + from = DidId("did:peer:Verifier"), + ) + aRecord <- svc.receiveRequestPresentation(connectionId, requestPresentation) + credentialsToUse = + AnoncredCredentialProofsV1( + List( + AnoncredCredentialProofV1( + aIssueCredentialRecord.id.value, + Seq("requestedAttribute"), + Seq("requestedPredicate") + ) + ) + ) + anoncredCredentialProofsJson <- ZIO.fromEither( + AnoncredCredentialProofsV1.schemaSerDes.serialize(credentialsToUse) + ) + updateRecord <- svc.acceptAnoncredRequestPresentation(aRecord.id, credentialsToUse) + + } yield { + assertTrue(updateRecord.connectionId == connectionId) && + assertTrue(updateRecord.anoncredCredentialsToUse.contains(anoncredCredentialProofsJson)) && + assertTrue(updateRecord.anoncredCredentialsToUseJsonSchemaId.contains(AnoncredCredentialProofsV1.version)) + } + }, + test("acceptRequestPresentation should fail given unmatching format") { + for { + repo <- ZIO.service[CredentialRepository] + aIssueCredentialRecord = issueCredentialRecord(CredentialFormat.JWT) + _ <- repo.createIssueCredentialRecord(aIssueCredentialRecord) + rawCredentialData = + """{"base64":"ZXlKaGJHY2lPaUpGVXpJMU5rc2lMQ0owZVhBaU9pSktWMVFpZlEuZXlKcFlYUWlPakUyTnprek1qYzROaklzSW1GMVpDSTZJbVJ2YldGcGJpSXNJbTV2Ym1ObElqb2lZMlk1T1RJMk56Z3RPREV3TmkwME1EZzVMV0UxWXprdE5tTmhObU0wWkRBMU1HVTBJaXdpZG5BaU9uc2lRR052Ym5SbGVIUWlPbHNpYUhSMGNITTZMeTkzZDNjdWR6TXViM0puTHpJd01UZ3ZjSEpsYzJWdWRHRjBhVzl1Y3k5Mk1TSmRMQ0owZVhCbElqcGJJbFpsY21sbWFXRmliR1ZRY21WelpXNTBZWFJwYjI0aVhYMHNJbWx6Y3lJNkltUnBaRHB3Y21semJUcGhaR0psT1RJNE9XUXdZelZtWWpVMlptWmhOVEF6T0Rka01UZ3dOR0ZpTkdFeE5UYzJOVEkzWXprME5tRTFNalV5T0RFM1ptRTRaVGhoTW1OalpXUXdPa056YzBKRGMyZENSVzFKUzBSWE1XaGpNMUpzWTJsb2NHSnRVbXhsUTJ0UlFWVktVRU5uYkZSYVYwNTNUV3BWTW1GNlJWTkpSUzFNYVVkTU0xRklaRlZ1VG10d1dXSkthSE5VYTIxWVVGaEpVM0ZXZWpjMll6RlZPWGhvVURseWNFZHBSSEZXTlRselJYcEtWbEpEYWxJMGEwMHdaMGg0YkhWUU5tVk5Ta2wwZHpJMk4yWllWbEpoTUhoRE5XaEthVU5uTVhSWldFNHdXbGhKYjJGWE5XdGFXR2R3UlVGU1ExUjNiMHBWTWxacVkwUkpNVTV0YzNoRmFVSlFhVFJvYVRrd1FqTldTbnBhUzFkSGVWbGlSVFZLYkhveGVVVnhiR010TFc1T1ZsQmpXVlJmWVRaU2IyYzJiR1ZtWWtKTmVWWlZVVzh3WlVwRVRrbENPRnBpYWkxdWFrTlRUR05PZFhVek1URlZWM1JOVVhWWkluMC5CcmFpbEVXa2VlSXhWbjY3dnpkVHZGTXpBMV9oNzFoaDZsODBHRFBpbkRaVVB4ajAxSC0tUC1QZDIxTk9wRDd3am51SDkxdUNBOFZMUW9fS2FnVjlnQQo="}""" + _ <- repo.updateWithIssuedRawCredential( + aIssueCredentialRecord.id, + IssueCredential.makeIssueCredentialFromRequestCredential(requestCredential.makeMessage), + rawCredentialData, + IssueCredentialRecord.ProtocolState.CredentialReceived + ) svc <- ZIO.service[PresentationService] connectionId = Some("connectionId") - aRecord <- svc.receiveRequestPresentation(connectionId, requestPresentationJWT) + anoncredPresentationRequestV1 = AnoncredPresentationRequestV1( + Map.empty, + Map.empty, + "name", + "nonce", + "version", + None + ) + attachmentDescriptor = AttachmentDescriptor.buildBase64Attachment( + mediaType = Some("application/json"), + format = Some(PresentCredentialRequestFormat.Anoncred.name), + payload = + AnoncredPresentationRequestV1.schemaSerDes.serializeToJsonString(anoncredPresentationRequestV1).getBytes() + ) + requestPresentation = RequestPresentation( + body = RequestPresentation.Body(goal_code = Some("Presentation Request")), + attachments = Seq(attachmentDescriptor), + to = DidId("did:peer:Prover"), + from = DidId("did:peer:Verifier"), + ) + aRecord <- svc.receiveRequestPresentation(connectionId, requestPresentation) + credentialsToUse = Seq(aIssueCredentialRecord.id.value) + result <- svc.acceptRequestPresentation(aRecord.id, credentialsToUse).exit + + } yield assert(result)( + fails(isSubtype[NotMatchingPresentationCredentialFormat](anything)) + ) + }, + test("rejectRequestPresentation updates the PresentationRecord") { + for { + svc <- ZIO.service[PresentationService] + connectionId = Some("connectionId") + aRecord <- svc.receiveRequestPresentation( + connectionId, + requestPresentation(PresentCredentialRequestFormat.JWT) + ) updateRecord <- svc.rejectRequestPresentation(aRecord.id) } yield { @@ -330,8 +608,8 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp test("markPresentationSent returns updated PresentationRecord") { for { svc <- ZIO.service[PresentationService] - pairwiseProverDid = DidId("did:peer:Prover") - record <- svc.createRecord() + _ = DidId("did:peer:Prover") + record <- svc.createJwtRecord() repo <- ZIO.service[PresentationRepository] _ <- repo.updatePresentationRecordProtocolState( record.id, @@ -344,22 +622,22 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp assertTrue(record.protocolState == PresentationRecord.ProtocolState.PresentationSent) } }, - test("receivePresentation updates the PresentatinRecord") { + test("receivePresentation updates the PresentationRecord") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() p = presentation(aRecord.thid.value) aRecordReceived <- svc.receivePresentation(p) } yield { assertTrue(aRecordReceived.id == aRecord.id) && - assertTrue(aRecordReceived.presentationData == Some(p)) + assertTrue(aRecordReceived.presentationData.contains(p)) } }, - test("acceptPresentation updates the PresentatinRecord") { + test("acceptPresentation updates the PresentationRecord") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() p = presentation(aRecord.thid.value) aRecordReceived <- svc.receivePresentation(p) repo <- ZIO.service[PresentationRepository] @@ -368,16 +646,16 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp PresentationRecord.ProtocolState.PresentationReceived, PresentationRecord.ProtocolState.PresentationVerified ) - aRecordAccept <- svc.acceptPresentation(aRecord.id) + _ <- svc.acceptPresentation(aRecord.id) } yield { assertTrue(aRecordReceived.id == aRecord.id) && - assertTrue(aRecordReceived.presentationData == Some(p)) + assertTrue(aRecordReceived.presentationData.contains(p)) } }, - test("markPresentationRejected updates the PresentatinRecord") { + test("markPresentationRejected updates the PresentationRecord") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() p = presentation(aRecord.thid.value) _ <- svc.receivePresentation(p) repo <- ZIO.service[PresentationRepository] @@ -389,16 +667,16 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp aRecordReject <- svc.markPresentationRejected(aRecord.id) } yield { assertTrue(aRecordReject.id == aRecord.id) && - assertTrue(aRecordReject.presentationData == Some(p)) && + assertTrue(aRecordReject.presentationData.contains(p)) && assertTrue(aRecordReject.protocolState == PresentationRecord.ProtocolState.PresentationRejected) } }, - test("rejectPresentation updates the PresentatinRecord") { + test("rejectPresentation updates the PresentationRecord") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() p = presentation(aRecord.thid.value) - aRecordReceived <- svc.receivePresentation(p) + _ <- svc.receivePresentation(p) repo <- ZIO.service[PresentationRepository] _ <- repo.updatePresentationRecordProtocolState( aRecord.id, @@ -408,15 +686,14 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp aRecordReject <- svc.rejectPresentation(aRecord.id) } yield { assertTrue(aRecordReject.id == aRecord.id) && - assertTrue(aRecordReject.presentationData == Some(p)) && + assertTrue(aRecordReject.presentationData.contains(p)) && assertTrue(aRecordReject.protocolState == PresentationRecord.ProtocolState.PresentationRejected) } }, test("markPresentationGenerated returns updated PresentationRecord") { 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( @@ -432,8 +709,7 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp test("markProposePresentationSent returns updated PresentationRecord") { for { svc <- ZIO.service[PresentationService] - pairwiseProverDid = DidId("did:peer:Prover") - record <- svc.createRecord() + record <- svc.createJwtRecord() repo <- ZIO.service[PresentationRepository] _ <- repo.updatePresentationRecordProtocolState( record.id, @@ -445,21 +721,21 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp assertTrue(record.protocolState == PresentationRecord.ProtocolState.ProposalSent) } }, - test("receiveProposePresentation updates the PresentatinRecord") { + test("receiveProposePresentation updates the PresentationRecord") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() p = proposePresentation(aRecord.thid.value) aRecordReceived <- svc.receiveProposePresentation(p) } yield { assertTrue(aRecordReceived.id == aRecord.id) && - assertTrue(aRecordReceived.proposePresentationData == Some(p)) + assertTrue(aRecordReceived.proposePresentationData.contains(p)) } }, - test("acceptProposePresentation updates the PresentatinRecord") { + test("acceptProposePresentation updates the PresentationRecord") { for { svc <- ZIO.service[PresentationService] - aRecord <- svc.createRecord() + aRecord <- svc.createJwtRecord() p = proposePresentation(aRecord.thid.value) aRecordReceived <- svc.receiveProposePresentation(p) repo <- ZIO.service[PresentationRepository] @@ -468,14 +744,161 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp PresentationRecord.ProtocolState.ProposalPending, PresentationRecord.ProtocolState.ProposalReceived ) - aRecordAccept <- svc.acceptProposePresentation(aRecord.id) + _ <- svc.acceptProposePresentation(aRecord.id) } yield { assertTrue(aRecordReceived.id == aRecord.id) && - assertTrue(aRecordReceived.proposePresentationData == Some(p)) + assertTrue(aRecordReceived.proposePresentationData.contains(p)) } }, ).provideSomeLayer(ZLayer.succeed(WalletAccessContext(WalletId.random))) + private def receiveRequestPresentationTest(attachment: AttachmentDescriptor) = { + for { + 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") + attachmentDescriptor = attachment + requestPresentation = RequestPresentation( + body = body, + attachments = Seq(attachmentDescriptor), + to = prover, + from = verifier, + ) + requestPresentationRecord <- svc.receiveRequestPresentation(connectionId, requestPresentation) + } yield (requestPresentationRecord, requestPresentation) + } + + private def createAnoncredPresentation = { + for { + credentialDefinitionService <- ZIO.service[CredentialDefinitionService] + issuerId = "did:prism:issuer" + holderID = "did:prism:holder" + schemaId = "resource:///anoncred-presentation-schema-example.json" + credentialDefinitionDb <- credentialDefinitionService.create( + Input( + name = "Credential Definition Name", + description = "Credential Definition Description", + version = "1.2", + signatureType = "CL", + tag = "tag", + author = issuerId, + authored = Some(OffsetDateTime.parse("2022-03-10T12:00:00Z")), + schemaId = schemaId, + supportRevocation = false + ) + ) + repo <- ZIO.service[CredentialRepository] + linkSecretService <- ZIO.service[LinkSecretService] + linkSecret <- linkSecretService.fetchOrCreate() + genericSecretStorage <- ZIO.service[GenericSecretStorage] + maybeCredentialDefintionPrivate <- + genericSecretStorage + .get[UUID, CredentialDefinitionSecret](credentialDefinitionDb.guid) + credentialDefinition = AnoncredCreateCredentialDefinition( + AnoncredCredentialDefinition(credentialDefinitionDb.definition.toString()), + AnoncredCredentialDefinitionPrivate(maybeCredentialDefintionPrivate.get.json.toString()), + AnoncredCredentialKeyCorrectnessProof(credentialDefinitionDb.keyCorrectnessProof.toString()) + ) + file = createTempJsonFile(credentialDefinition.cd.data, "anoncred-presentation-credential-definition-example") + credentialDefinitionId = "resource:///" + file.getFileName + credentialOffer = AnoncredLib.createOffer(credentialDefinition, credentialDefinitionId) + credentialRequest = AnoncredLib.createCredentialRequest(linkSecret, credentialDefinition.cd, credentialOffer) + processedCredential = + AnoncredLib.processCredential( + AnoncredLib + .createCredential( + credentialDefinition.cd, + credentialDefinition.cdPrivate, + credentialOffer, + credentialRequest.request, + Seq( + ("name", "Miguel"), + ("sex", "M"), + ("age", "31"), + ) + ), + credentialRequest.metadata, + linkSecret, + credentialDefinition.cd + ) + issueCredential = + IssueCredential( + from = DidId(issuerId), + to = DidId(holderID), + body = IssueCredential.Body(), + attachments = Seq( + AttachmentDescriptor.buildBase64Attachment( + mediaType = Some("application/json"), + format = Some(IssueCredentialIssuedFormat.Anoncred.name), + payload = processedCredential.data.getBytes() + ) + ) + ) + aIssueCredentialRecord = + IssueCredentialRecord( + id = DidCommID(), + createdAt = Instant.now, + updatedAt = None, + thid = DidCommID(), + schemaUri = Some(schemaId), + credentialDefinitionId = Some(credentialDefinitionDb.guid), + credentialDefinitionUri = Some(credentialDefinitionId), + credentialFormat = CredentialFormat.AnonCreds, + role = IssueCredentialRecord.Role.Issuer, + subjectId = None, + validityPeriod = None, + automaticIssuance = None, + protocolState = IssueCredentialRecord.ProtocolState.CredentialReceived, + offerCredentialData = None, + requestCredentialData = None, + anonCredsRequestMetadata = None, + issueCredentialData = Some(issueCredential), + issuedCredentialRaw = + Some(issueCredential.attachments.map(_.data.asJson.noSpaces).headOption.getOrElse("???")), + issuingDID = None, + metaRetries = 5, + metaNextRetry = Some(Instant.now()), + metaLastFailure = None, + ) + _ <- repo.createIssueCredentialRecord(aIssueCredentialRecord) + svc <- ZIO.service[PresentationService] + aRecord <- svc.createAnoncredRecord( + credentialDefinitionId = credentialDefinitionId + ) + repo <- ZIO.service[PresentationRepository] + credentialsToUse = + AnoncredCredentialProofsV1( + List( + AnoncredCredentialProofV1( + aIssueCredentialRecord.id.value, + Seq("sex"), + Seq("age") + ) + ) + ) + credentialsToUseJson <- ZIO.fromEither( + AnoncredCredentialProofsV1.schemaSerDes.serialize(credentialsToUse) + ) + _ <- + repo.updateAnoncredPresentationWithCredentialsToUse( + aRecord.id, + Some(AnoncredPresentationV1.version), + Some(credentialsToUseJson), + PresentationRecord.ProtocolState.RequestPending + ) + issuer = createIssuer(DID("did:prism:issuer")) + presentation <- svc.createAnoncredPresentation( + aRecord.requestPresentationData.get, + aRecord.id, + issuer, + credentialsToUse, + Instant.now() + ) + } yield (presentation, aRecord) + } + private val multiWalletSpec = suite("multi-wallet spec")( test("createPresentation for different wallet and isolate records") { @@ -485,8 +908,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) @@ -498,4 +921,16 @@ object PresentationServiceSpec extends ZIOSpecDefault with PresentationServiceSp } ) + def createTempJsonFile(jsonContent: String, fileName: String): Path = { + val resourceURI = this.getClass.getResource("/").toURI + val resourcePath = Paths.get(resourceURI) + + val filePath = resourcePath.resolve(fileName + ".json") + + Files.write(filePath, jsonContent.getBytes(StandardCharsets.UTF_8)) + + filePath.toFile.deleteOnExit() + filePath + } + } 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..bbb32df467 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 @@ -1,17 +1,16 @@ package io.iohk.atala.pollux.core.service import com.nimbusds.jose.jwk.* +import io.iohk.atala.agent.walletapi.memory.GenericSecretStorageInMemory 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.repository.{ - CredentialRepository, - CredentialRepositoryInMemory, - PresentationRepositoryInMemory -} +import io.iohk.atala.pollux.core.model.error.PresentationError +import io.iohk.atala.pollux.core.repository.* +import io.iohk.atala.pollux.core.service.serdes.* import io.iohk.atala.pollux.vc.jwt.* +import io.iohk.atala.shared.models.{WalletAccessContext, WalletId} import zio.* import java.security.* @@ -20,14 +19,28 @@ import java.util.UUID trait PresentationServiceSpecHelper { + protected val defaultWalletLayer = ZLayer.succeed(WalletAccessContext(WalletId.default)) + val peerDidAgentLayer = AgentPeerService.makeLayer(PeerDID.makePeerDid(serviceEndpoint = Some("http://localhost:9099"))) - val presentationServiceLayer = ZLayer.make[PresentationService & PresentationRepository & CredentialRepository]( + val genericSecretStorageLayer = GenericSecretStorageInMemory.layer + val uriDereferencerLayer = ResourceURIDereferencerImpl.layer + val credentialDefLayer = + CredentialDefinitionRepositoryInMemory.layer ++ uriDereferencerLayer >>> CredentialDefinitionServiceImpl.layer + val linkSecretLayer = genericSecretStorageLayer >+> LinkSecretServiceImpl.layer + + val presentationServiceLayer = ZLayer.make[ + PresentationService & CredentialDefinitionService & URIDereferencer & LinkSecretService & PresentationRepository & + CredentialRepository + ]( PresentationServiceImpl.layer, + credentialDefLayer, + uriDereferencerLayer, + linkSecretLayer, PresentationRepositoryInMemory.layer, CredentialRepositoryInMemory.layer - ) + ) ++ defaultWalletLayer def createIssuer(did: DID) = { val keyGen = KeyPairGenerator.getInstance("EC") @@ -51,7 +64,7 @@ trait PresentationServiceSpecHelper { attachments = Nil ) - protected def requestPresentationJWT: RequestPresentation = { + protected def requestPresentation(credentialFormat: PresentCredentialRequestFormat): RequestPresentation = { val body = RequestPresentation.Body(goal_code = Some("Presentation Request")) val presentationAttachmentAsJson = """{ @@ -64,7 +77,7 @@ trait PresentationServiceSpecHelper { val attachmentDescriptor = AttachmentDescriptor.buildJsonAttachment( payload = presentationAttachmentAsJson, - format = Some(PresentCredentialRequestFormat.JWT.name) + format = Some(credentialFormat.name) ) RequestPresentation( body = body, @@ -114,14 +127,15 @@ trait PresentationServiceSpecHelper { ) } - protected def issueCredentialRecord = IssueCredentialRecord( + protected def issueCredentialRecord(credentialFormat: CredentialFormat) = IssueCredentialRecord( id = DidCommID(), createdAt = Instant.now, updatedAt = None, thid = DidCommID(), - schemaId = None, + schemaUri = None, credentialDefinitionId = None, - credentialFormat = CredentialFormat.JWT, + credentialDefinitionUri = None, + credentialFormat = credentialFormat, role = IssueCredentialRecord.Role.Issuer, subjectId = None, validityPeriod = None, @@ -139,23 +153,63 @@ 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 ) } + def createAnoncredRecord( + credentialDefinitionId: String = "$CRED_DEF_ID", + pairwiseVerifierDID: DidId = DidId("did:prism:issuer"), + pairwiseProverDID: DidId = DidId("did:prism:prover-pairwise"), + thid: DidCommID = DidCommID() + ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = { + val anoncredPresentationRequestV1 = AnoncredPresentationRequestV1( + requested_attributes = Map( + "sex" -> AnoncredRequestedAttributeV1( + name = "sex", + restrictions = List( + Map( + ("attr::sex::value" -> "M"), + ("cred_def_id" -> credentialDefinitionId) + ) + ), + non_revoked = None + ) + ), + requested_predicates = Map( + "age" -> AnoncredRequestedPredicateV1( + name = "age", + p_type = ">=", + p_value = 18, + restrictions = List.empty, + non_revoked = None + ) + ), + name = "proof_req_1", + nonce = "1103253414365527824079144", + version = "0.1", + non_revoked = None + ) + svc.createAnoncredPresentationRecord( + thid = thid, + pairwiseVerifierDID = pairwiseVerifierDID, + pairwiseProverDID = pairwiseProverDID, + connectionId = Some("connectionId"), + anoncredPresentationRequestV1 + ) + } } diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredPresentationRequestSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredPresentationRequestSpec.scala new file mode 100644 index 0000000000..85a18f3204 --- /dev/null +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredPresentationRequestSpec.scala @@ -0,0 +1,99 @@ +package io.iohk.atala.pollux.core.service.serdes + +import zio.* +import zio.test.* +import zio.test.Assertion.* + +object AnoncredPresentationRequestSpec extends ZIOSpecDefault { + val json: String = + """ + |{ + | "requested_attributes": { + | "attribute1": { + | "name": "Attribute 1", + | "restrictions": [ + | { + | "cred_def_id": "credential_definition_id_of_attribute1" + | } + | ], + | "non_revoked": { + | "from": 1635734400, + | "to": 1735734400 + | } + | } + | }, + | "requested_predicates": { + | "predicate1": { + | "name": "Predicate 1", + | "p_type": ">=", + | "p_value": 18, + | "restrictions": [ + | { + | "schema_id": "schema_id_of_predicate1" + | } + | ], + | "non_revoked": { + | "from": 1635734400 + | } + | } + | }, + | "name": "Example Presentation Request", + | "nonce": "1234567890", + | "version": "1.0" + |} + |""".stripMargin + + override def spec: Spec[TestEnvironment with Scope, Any] = suite("AnoncredPresentationRequestSerDes")( + test("should validate a correct schema") { + assertZIO(AnoncredPresentationRequestV1.schemaSerDes.validate(json))(isUnit) + }, + test("should deserialize correctly") { + val expectedPresentationRequest = + 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 + ) + + assertZIO(AnoncredPresentationRequestV1.schemaSerDes.deserialize(json))( + Assertion.equalTo(expectedPresentationRequest) + ) + } + ) +} diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredPresentationSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredPresentationSpec.scala new file mode 100644 index 0000000000..275bc181be --- /dev/null +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/serdes/AnoncredPresentationSpec.scala @@ -0,0 +1,213 @@ +package io.iohk.atala.pollux.core.service.serdes + +import io.iohk.atala.pollux.core.service.serdes.AnoncredPresentationV1.* +import zio.* +import zio.test.* +import zio.test.Assertion.* + +object AnoncredPresentationSpec extends ZIOSpecDefault { + val json: String = + """ + |{ + | "proof": { + | "proofs": [ + | { + | "primary_proof": { + | "eq_proof": { + | "revealed_attrs": { + | "sex": "4046863..." + | }, + | "a_prime": "2329247...", + | "e": "1666946...", + | "v": "2137556...", + | "m": { + | "age": "1025474...", + | "master_secret": "8005118...", + | "name": "1031839..." + | }, + | "m2": "5852217..." + | }, + | "ge_proofs": [ + | { + | "u": { + | "1": "1135835...", + | "0": "5209733...", + | "3": "6777506...", + | "2": "8790500..." + | }, + | "r": { + | "DELTA": "1094759...", + | "3": "3908998...", + | "1": "3551550...", + | "2": "4594656...", + | "0": "7769940..." + | }, + | "mj": "1025474...", + | "alpha": "4089177...", + | "t": { + | "0": "1208121...", + | "DELTA": "9078219...", + | "1": "5769003...", + | "3": "4089901...", + | "2": "2261914..." + | }, + | "predicate": { + | "attr_name": "age", + | "p_type": "GE", + | "value": 18 + | } + | } + | ] + | }, + | "non_revoc_proof": null + | } + | ], + | "aggregated_proof": { + | "c_hash": "3880251...", + | "c_list": [ + | [184, 131, 15], + | [95, 179], + | [1, 200, 254], + | [179, 45, 156], + | [1, 67, 251], + | [2, 207, 34, 43] + | ] + | } + | }, + | "requested_proof": { + | "revealed_attrs": { + | "sex": { + | "sub_proof_index": 0, + | "raw": "M", + | "encoded": "4046863..." + | } + | }, + | "self_attested_attrs":{}, + | "unrevealed_attrs":{}, + | "predicates": { + | "age": { + | "sub_proof_index": 0 + | } + | } + | }, + | "identifiers": [ + | { + | "schema_id": "resource:///anoncred-presentation-schema-example.json", + | "cred_def_id": "resource:///anoncred-presentation-credential-definition-example.json", + | "rev_reg_id": null, + | "timestamp": null + | } + | ] + |} + | + |""".stripMargin + + override def spec: Spec[TestEnvironment with Scope, Any] = suite("AnoncredPresentationRequestSerDes")( + test("should validate a correct schema") { + assertZIO(AnoncredPresentationV1.schemaSerDes.validate(json))(isUnit) + }, + test("should deserialize correctly") { + val predicate = AnoncredPredicateV1( + attr_name = "age", + p_type = "GE", + value = 18 + ) + + val geProof = AnoncredGeProofV1( + u = Map( + "1" -> "1135835...", + "0" -> "5209733...", + "3" -> "6777506...", + "2" -> "8790500..." + ), + r = Map( + "DELTA" -> "1094759...", + "3" -> "3908998...", + "1" -> "3551550...", + "2" -> "4594656...", + "0" -> "7769940..." + ), + mj = "1025474...", + alpha = "4089177...", + t = Map( + "0" -> "1208121...", + "DELTA" -> "9078219...", + "1" -> "5769003...", + "3" -> "4089901...", + "2" -> "2261914..." + ), + predicate = predicate + ) + + val eqProof = AnoncredEqProofV1( + revealed_attrs = Map("sex" -> "4046863..."), + a_prime = "2329247...", + e = "1666946...", + v = "2137556...", + m = Map( + "age" -> "1025474...", + "master_secret" -> "8005118...", + "name" -> "1031839..." + ), + m2 = "5852217..." + ) + + val primaryProof = AnoncredPrimaryProofV1( + eq_proof = eqProof, + ge_proofs = List(geProof) + ) + + val subProof = AnoncredSubProofV1( + primary_proof = primaryProof, + non_revoc_proof = None + ) + + val aggregatedProof = AnoncredAggregatedProofV1( + c_hash = "3880251...", + c_list = List( + List(184, 131, 15), + List(95, 179), + List(1, 200, 254), + List(179, 45, 156), + List(1, 67, 251), + List(2, 207, 34, 43) + ) + ) + + val revealedAttr = AnoncredRevealedAttrV1( + sub_proof_index = 0, + raw = "M", + encoded = "4046863..." + ) + + val requestedProof = AnoncredRequestedProofV1( + revealed_attrs = Map("sex" -> revealedAttr), + self_attested_attrs = Map.empty, + unrevealed_attrs = Map.empty, + predicates = Map("age" -> AnoncredSubProofIndexV1(0)) + ) + + val identifier = AnoncredIdentifierV1( + schema_id = "resource:///anoncred-presentation-schema-example.json", + cred_def_id = "resource:///anoncred-presentation-credential-definition-example.json", + rev_reg_id = None, + timestamp = None + ) + + val anoncredPresentationV1 = AnoncredPresentationV1( + proof = AnoncredProofV1( + proofs = List(subProof), + aggregated_proof = aggregatedProof + ), + requested_proof = requestedProof, + identifiers = List(identifier) + ) + + assert(AnoncredPresentationV1.schemaSerDes.serializeToJsonString(anoncredPresentationV1))(Assertion.equalTo(json)) + + assertZIO(AnoncredPresentationV1.schemaSerDes.deserialize(json))( + Assertion.equalTo(anoncredPresentationV1) + ) + } + ) +} diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/helper/PublicCredentialDefinitionSerDesSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/serdes/PublicCredentialDefinitionSchemaSerDesSpec.scala similarity index 85% rename from pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/helper/PublicCredentialDefinitionSerDesSpec.scala rename to pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/serdes/PublicCredentialDefinitionSchemaSerDesSpec.scala index 4a05b09fd5..aa10648519 100644 --- a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/helper/PublicCredentialDefinitionSerDesSpec.scala +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/serdes/PublicCredentialDefinitionSchemaSerDesSpec.scala @@ -1,16 +1,11 @@ -package io.iohk.atala.pollux.core.service.helper +package io.iohk.atala.pollux.core.service.serdes -import io.iohk.atala.pollux.core.service.serdes.PublicCredentialDefinitionSerDesV1 -import io.iohk.atala.pollux.core.service.serdes.PublicCredentialPrimaryPublicKeyV1 -import io.iohk.atala.pollux.core.service.serdes.PublicCredentialRevocationKeyV1 -import io.iohk.atala.pollux.core.service.serdes.PublicCredentialValueV1 import zio.* import zio.test.* import zio.test.Assertion.* -import zio.test.assertZIO -object PublicCredentialDefinitionSerDesSpec extends ZIOSpecDefault { - val json = +object PublicCredentialDefinitionSchemaSerDesSpec extends ZIOSpecDefault { + val json: String = """ |{ | "schemaId": "resource:///anoncred-schema-example.json", @@ -51,7 +46,7 @@ object PublicCredentialDefinitionSerDesSpec extends ZIOSpecDefault { override def spec: Spec[TestEnvironment with Scope, Any] = suite("PublicCredentialDefinitionSerDes")( test("should validate a correct schema") { - assertZIO(PublicCredentialDefinitionSerDesV1.schemaSerDes.validate(json))(isTrue) + assertZIO(PublicCredentialDefinitionSerDesV1.schemaSerDes.validate(json))(isUnit) }, test("should deserialise") { val primary = PublicCredentialPrimaryPublicKeyV1( diff --git a/pollux/lib/sql-doobie/src/main/resources/sql/pollux/V17__add_anoncred_credentials_to_use_columns.sql b/pollux/lib/sql-doobie/src/main/resources/sql/pollux/V17__add_anoncred_credentials_to_use_columns.sql new file mode 100644 index 0000000000..dde029abeb --- /dev/null +++ b/pollux/lib/sql-doobie/src/main/resources/sql/pollux/V17__add_anoncred_credentials_to_use_columns.sql @@ -0,0 +1,4 @@ +-- presentation_records +ALTER TABLE public.presentation_records + ADD COLUMN "anoncred_credentials_to_use_json_schema_id" VARCHAR(64), + ADD COLUMN "anoncred_credentials_to_use" JSON; \ No newline at end of file diff --git a/pollux/lib/sql-doobie/src/main/resources/sql/pollux/V18__issue_credential_rename_schema_id.sql b/pollux/lib/sql-doobie/src/main/resources/sql/pollux/V18__issue_credential_rename_schema_id.sql new file mode 100644 index 0000000000..6fc8f3c693 --- /dev/null +++ b/pollux/lib/sql-doobie/src/main/resources/sql/pollux/V18__issue_credential_rename_schema_id.sql @@ -0,0 +1,2 @@ +ALTER TABLE public.issue_credential_records RENAME COLUMN schema_id TO schema_uri; +ALTER TABLE public.issue_credential_records ADD COLUMN credential_definition_uri VARCHAR(500); diff --git a/pollux/lib/sql-doobie/src/main/scala/io/iohk/atala/pollux/sql/repository/JdbcCredentialRepository.scala b/pollux/lib/sql-doobie/src/main/scala/io/iohk/atala/pollux/sql/repository/JdbcCredentialRepository.scala index cd3b187ba7..00ce0c9492 100644 --- a/pollux/lib/sql-doobie/src/main/scala/io/iohk/atala/pollux/sql/repository/JdbcCredentialRepository.scala +++ b/pollux/lib/sql-doobie/src/main/scala/io/iohk/atala/pollux/sql/repository/JdbcCredentialRepository.scala @@ -9,7 +9,7 @@ import io.circe.parser.* import io.circe.syntax.* import io.iohk.atala.castor.core.model.did.* import io.iohk.atala.mercury.protocol.issuecredential.{IssueCredential, OfferCredential, RequestCredential} -import io.iohk.atala.pollux.anoncreds.CredentialRequestMetadata +import io.iohk.atala.pollux.anoncreds.AnoncredCredentialRequestMetadata import io.iohk.atala.pollux.core.model.* import io.iohk.atala.pollux.core.model.error.CredentialRepositoryError import io.iohk.atala.pollux.core.model.error.CredentialRepositoryError.* @@ -48,9 +48,9 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ given requestCredentialGet: Get[RequestCredential] = Get[String].map(decode[RequestCredential](_).getOrElse(???)) given requestCredentialPut: Put[RequestCredential] = Put[String].contramap(_.asJson.toString) - given acRequestMetadataGet: Get[CredentialRequestMetadata] = - Get[String].map(_.fromJson[CredentialRequestMetadata].getOrElse(???)) - given acRequestMetadataPut: Put[CredentialRequestMetadata] = Put[String].contramap(_.toJson) + given acRequestMetadataGet: Get[AnoncredCredentialRequestMetadata] = + Get[String].map(_.fromJson[AnoncredCredentialRequestMetadata].getOrElse(???)) + given acRequestMetadataPut: Put[AnoncredCredentialRequestMetadata] = Put[String].contramap(_.toJson) given issueCredentialGet: Get[IssueCredential] = Get[String].map(decode[IssueCredential](_).getOrElse(???)) given issueCredentialPut: Put[IssueCredential] = Put[String].contramap(_.asJson.toString) @@ -62,8 +62,9 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_id, + | schema_uri, | credential_definition_id, + | credential_definition_uri, | credential_format, | role, | subject_id, @@ -85,8 +86,9 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | ${record.createdAt}, | ${record.updatedAt}, | ${record.thid}, - | ${record.schemaId}, + | ${record.schemaUri}, | ${record.credentialDefinitionId}, + | ${record.credentialDefinitionUri}, | ${record.credentialFormat}, | ${record.role}, | ${record.subjectId}, @@ -129,8 +131,9 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_id, + | schema_uri, | credential_definition_id, + | credential_definition_uri, | credential_format, | role, | subject_id, @@ -196,8 +199,9 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_id, + | schema_uri, | credential_definition_id, + | credential_definition_uri, | credential_format, | role, | subject_id, @@ -246,8 +250,9 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_id, + | schema_uri, | credential_definition_id, + | credential_definition_uri, | credential_format, | role, | subject_id, @@ -287,8 +292,9 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | created_at, | updated_at, | thid, - | schema_id, + | schema_uri, | credential_definition_id, + | credential_definition_uri, | credential_format, | role, | subject_id, @@ -377,7 +383,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ override def updateWithAnonCredsRequestCredential( recordId: DidCommID, request: RequestCredential, - metadata: CredentialRequestMetadata, + metadata: AnoncredCredentialRequestMetadata, protocolState: ProtocolState ): RIO[WalletAccessContext, Int] = { val cxnIO = @@ -426,6 +432,7 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ | SELECT | id, | issued_credential_raw, + | credential_format, | subject_id | FROM public.issue_credential_records | WHERE @@ -440,6 +447,37 @@ class JdbcCredentialRepository(xa: Transactor[ContextAwareTask], xb: Transactor[ } + override def getValidAnoncredIssuedCredentials( + recordIds: Seq[DidCommID] + ): RIO[WalletAccessContext, Seq[ValidFullIssuedCredentialRecord]] = { + val idAsStrings = recordIds.map(_.toString) + val nel = NonEmptyList.of(idAsStrings.head, idAsStrings.tail: _*) + val inClauseFragment = Fragments.in(fr"id", nel) + + val cxnIO = sql""" + | SELECT + | id, + | issue_credential_data, + | credential_format, + | schema_uri, + | credential_definition_uri, + | subject_id + | FROM public.issue_credential_records + | WHERE 1=1 + | AND issue_credential_data IS NOT NULL + | AND schema_uri IS NOT NULL + | AND credential_definition_uri IS NOT NULL + | AND credential_format = 'AnonCreds' + | AND $inClauseFragment + """.stripMargin + .query[ValidFullIssuedCredentialRecord] + .to[Seq] + + cxnIO + .transactWallet(xa) + + } + override def deleteIssueCredentialRecord(recordId: DidCommID): RIO[WalletAccessContext, Int] = { val cxnIO = sql""" | DELETE diff --git a/pollux/lib/sql-doobie/src/main/scala/io/iohk/atala/pollux/sql/repository/JdbcPresentationRepository.scala b/pollux/lib/sql-doobie/src/main/scala/io/iohk/atala/pollux/sql/repository/JdbcPresentationRepository.scala index ef0623a4e1..19acb64d76 100644 --- a/pollux/lib/sql-doobie/src/main/scala/io/iohk/atala/pollux/sql/repository/JdbcPresentationRepository.scala +++ b/pollux/lib/sql-doobie/src/main/scala/io/iohk/atala/pollux/sql/repository/JdbcPresentationRepository.scala @@ -2,9 +2,12 @@ package io.iohk.atala.pollux.sql.repository import cats.data.NonEmptyList import doobie.* +import doobie.free.connection import doobie.implicits.* import doobie.postgres.* import doobie.postgres.implicits.* +import doobie.postgres.circe.json.implicits._ +import io.circe import io.circe.* import io.circe.parser.* import io.circe.syntax.* @@ -18,9 +21,11 @@ import io.iohk.atala.shared.db.Implicits.* import io.iohk.atala.shared.models.WalletAccessContext import io.iohk.atala.shared.utils.BytesOps import zio.* -import doobie.free.connection -import java.time.Instant import zio.interop.catz.* +import zio.json.* +import zio.json.ast.Json +import zio.json.ast.Json.* +import java.time.Instant // TODO: replace with actual implementation class JdbcPresentationRepository( xa: Transactor[ContextAwareTask], @@ -51,6 +56,31 @@ class JdbcPresentationRepository( .transactWallet(xa) } + def updateAnoncredPresentationWithCredentialsToUse( + recordId: DidCommID, + anoncredCredentialsToUseJsonSchemaId: Option[String], + anoncredCredentialsToUse: Option[AnoncredCredentialProofs], + protocolState: ProtocolState + ): RIO[WalletAccessContext, Int] = { + val cxnIO = + sql""" + | UPDATE public.presentation_records + | SET + | anoncred_credentials_to_use_json_schema_id = ${anoncredCredentialsToUseJsonSchemaId}, + | anoncred_credentials_to_use = ${anoncredCredentialsToUse}, + | protocol_state = $protocolState, + | updated_at = ${Instant.now}, + | meta_retries = $maxRetries, + | meta_next_retry = ${Instant.now}, + | meta_last_failure = null + | WHERE + | id = $recordId + """.stripMargin.update + + cxnIO.run + .transactWallet(xa) + } + // deserializes from the hex string private def deserializeInclusionProof(proof: String): MerkleInclusionProof = MerkleInclusionProof.decode( @@ -64,6 +94,23 @@ class JdbcPresentationRepository( import PresentationRecord.* + def zioJsonToCirceJson(zioJson: Json): circe.Json = { + parse(zioJson.toString).getOrElse(circe.Json.Null) + } + + def circeJsonToZioJson(circeJson: circe.Json): Json = { + circeJson.noSpaces.fromJson[Json].getOrElse(Json.Null) + } + + given jsonGet: Get[AnoncredCredentialProofs] = Get[circe.Json].map { jsonString => + circeJsonToZioJson(jsonString) + } + + given jsonPut: Put[AnoncredCredentialProofs] = Put[circe.Json].contramap(zioJsonToCirceJson(_)) + + given didCommIDGet: Get[DidCommID] = Get[String].map(DidCommID(_)) + given didCommIDPut: Put[DidCommID] = Put[String].contramap(_.value) + given protocolStateGet: Get[ProtocolState] = Get[String].map(ProtocolState.valueOf) given protocolStatePut: Put[ProtocolState] = Put[String].contramap(_.toString) @@ -106,6 +153,8 @@ class JdbcPresentationRepository( | credential_format, | request_presentation_data, | credentials_to_use, + | anoncred_credentials_to_use_json_schema_id, + | anoncred_credentials_to_use, | meta_retries, | meta_next_retry, | meta_last_failure, @@ -123,6 +172,8 @@ class JdbcPresentationRepository( | ${record.credentialFormat}, | ${record.requestPresentationData}, | ${record.credentialsToUse.map(_.toList)}, + | ${record.anoncredCredentialsToUseJsonSchemaId}, + | ${record.anoncredCredentialsToUse}, | ${record.metaRetries}, | ${record.metaNextRetry}, | ${record.metaLastFailure}, @@ -156,6 +207,8 @@ class JdbcPresentationRepository( | propose_presentation_data, | presentation_data, | credentials_to_use, + | anoncred_credentials_to_use_json_schema_id, + | anoncred_credentials_to_use, | meta_retries, | meta_next_retry, | meta_last_failure @@ -202,6 +255,8 @@ class JdbcPresentationRepository( | propose_presentation_data, | presentation_data, | credentials_to_use, + | anoncred_credentials_to_use_json_schema_id, + | anoncred_credentials_to_use, | meta_retries, | meta_next_retry, | meta_last_failure @@ -246,6 +301,8 @@ class JdbcPresentationRepository( | propose_presentation_data, | presentation_data, | credentials_to_use, + | anoncred_credentials_to_use_json_schema_id, + | anoncred_credentials_to_use, | meta_retries, | meta_next_retry, | meta_last_failure @@ -278,6 +335,8 @@ class JdbcPresentationRepository( | propose_presentation_data, | presentation_data, | credentials_to_use, + | anoncred_credentials_to_use_json_schema_id, + | anoncred_credentials_to_use, | meta_retries, | meta_next_retry, | meta_last_failure diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 85c13f45e4..73afd223b0 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -165,7 +165,7 @@ object MainApp extends ZIOAppDefault { DIDServiceImpl.layer, EntityServiceImpl.layer, ManagedDIDServiceWithEventNotificationImpl.layer, - PresentationServiceImpl.layer >>> PresentationServiceNotifier.layer, + LinkSecretServiceImpl.layer >>> PresentationServiceImpl.layer >>> PresentationServiceNotifier.layer, VerificationPolicyServiceImpl.layer, WalletManagementServiceImpl.layer, // authentication diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/IssueBackgroundJobs.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/IssueBackgroundJobs.scala index d033da7af8..581625d808 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/IssueBackgroundJobs.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/IssueBackgroundJobs.scala @@ -2,6 +2,7 @@ package io.iohk.atala.agent.server.jobs import io.iohk.atala.agent.server.config.AppConfig import io.iohk.atala.agent.server.jobs.BackgroundJobError.ErrorResponseReceivedFromPeerAgent +import io.iohk.atala.agent.walletapi.model.error.DIDSecretStorageError.WalletNotFoundError import io.iohk.atala.castor.core.model.did.* import io.iohk.atala.mercury.* import io.iohk.atala.mercury.protocol.issuecredential.* @@ -12,7 +13,6 @@ import io.iohk.atala.shared.utils.DurationOps.toMetricsSeconds import io.iohk.atala.shared.utils.aspects.CustomMetricsAspect import zio.* import zio.metrics.* -import io.iohk.atala.agent.walletapi.model.error.DIDSecretStorageError.WalletNotFoundError object IssueBackgroundJobs extends BackgroundJobsHelper { @@ -148,6 +148,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, Role.Issuer, _, _, @@ -202,6 +203,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, CredentialFormat.JWT, Role.Holder, Some(subjectId), @@ -242,6 +244,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, CredentialFormat.AnonCreds, Role.Holder, None, @@ -284,6 +287,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, Role.Holder, _, _, @@ -339,6 +343,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, Role.Issuer, _, _, @@ -377,6 +382,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, CredentialFormat.JWT, Role.Issuer, _, @@ -421,6 +427,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, CredentialFormat.AnonCreds, Role.Issuer, _, @@ -463,6 +470,7 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, Role.Issuer, _, _, diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/PresentBackgroundJobs.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/PresentBackgroundJobs.scala index 9eb3f15697..4a9405f546 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/PresentBackgroundJobs.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/PresentBackgroundJobs.scala @@ -9,7 +9,11 @@ import io.iohk.atala.agent.server.jobs.BackgroundJobError.{ InvalidState, NotImplemented } +import io.iohk.atala.agent.walletapi.model.error.DIDSecretStorageError.WalletNotFoundError +import io.iohk.atala.agent.walletapi.service.ManagedDIDService +import io.iohk.atala.agent.walletapi.storage.DIDNonSecretStorage import io.iohk.atala.castor.core.model.did.* +import io.iohk.atala.castor.core.service.DIDService import io.iohk.atala.mercury.* import io.iohk.atala.mercury.model.* import io.iohk.atala.mercury.protocol.presentproof.* @@ -17,11 +21,14 @@ import io.iohk.atala.mercury.protocol.reportproblem.v2.* import io.iohk.atala.pollux.core.model.* import io.iohk.atala.pollux.core.model.error.PresentationError.* import io.iohk.atala.pollux.core.model.error.{CredentialServiceError, PresentationError} +import io.iohk.atala.pollux.core.service.serdes.AnoncredCredentialProofsV1 import io.iohk.atala.pollux.core.service.{CredentialService, PresentationService} import io.iohk.atala.pollux.vc.jwt.{JWT, JwtPresentation, DidResolver as JwtDidResolver} +import io.iohk.atala.resolvers.DIDResolver import io.iohk.atala.shared.utils.DurationOps.toMetricsSeconds import io.iohk.atala.shared.utils.aspects.CustomMetricsAspect import zio.* +import zio.json.ast.Json import zio.metrics.* import zio.prelude.Validation import zio.prelude.ZValidation.* @@ -29,11 +36,11 @@ import io.iohk.atala.agent.walletapi.storage.DIDNonSecretStorage import io.iohk.atala.agent.walletapi.model.error.DIDSecretStorageError.WalletNotFoundError import io.iohk.atala.resolvers.DIDResolver import io.iohk.atala.shared.models.WalletAccessContext - import java.time.{Clock, Instant, ZoneId} import io.iohk.atala.castor.core.service.DIDService import io.iohk.atala.agent.walletapi.service.ManagedDIDService import io.iohk.atala.shared.http.* + object PresentBackgroundJobs extends BackgroundJobsHelper { val presentProofExchanges = { @@ -163,21 +170,41 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { // ########################## // ### PresentationRecord ### // ########################## - case PresentationRecord(id, _, _, _, _, _, _, _, ProposalPending, _, _, _, _, _, _, _, _) => + 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, _, _, _, _, _, _, _, RequestPending, _, oRecord, _, _, _, _, _, _) => // Verifier + case PresentationRecord( + id, + _, + _, + _, + _, + _, + _, + _, + RequestPending, + _, + oRecord, + _, + _, + _, + _, + _, + _, + _, + _ + ) => // Verifier oRecord match case None => ZIO.fail(InvalidState("PresentationRecord 'RequestPending' with no Record")) case Some(record) => val verifierReqPendingToSentFlow = for { - _ <- ZIO.log(s"PresentationRecord: RequestPending (Send Massage)") + _ <- ZIO.log(s"PresentationRecord: RequestPending (Send Message)") walletAccessContext <- buildWalletAccessContextLayer(record.from) result <- (for { didOps <- ZIO.service[DidOps] @@ -211,17 +238,17 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { .gauge("present_proof_flow_verifier_req_pending_to_sent_flow_ms_gauge") .trackDurationWith(_.toMetricsSeconds) - case PresentationRecord(id, _, _, _, _, _, _, _, RequestSent, _, _, _, _, _, _, _, _) => // Verifier + case PresentationRecord(id, _, _, _, _, _, _, _, RequestSent, _, _, _, _, _, _, _, _, _, _) => // Verifier ZIO.logDebug("PresentationRecord: RequestSent") *> ZIO.unit - case PresentationRecord(id, _, _, _, _, _, _, _, RequestReceived, _, _, _, _, _, _, _, _) => // Prover + case PresentationRecord(id, _, _, _, _, _, _, _, RequestReceived, _, _, _, _, _, _, _, _, _, _) => // Prover ZIO.logDebug("PresentationRecord: RequestReceived") *> ZIO.unit - case PresentationRecord(id, _, _, _, _, _, _, _, RequestRejected, _, _, _, _, _, _, _, _) => // Prover + case PresentationRecord(id, _, _, _, _, _, _, _, RequestRejected, _, _, _, _, _, _, _, _, _, _) => // 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, @@ -233,16 +260,17 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _, _, PresentationPending, - _, + CredentialFormat.JWT, oRequestPresentation, _, _, credentialsToUse, _, _, + _, + _, _ ) => // Prover - // signedJwtPresentation = JwtPresentation.toEncodedJwt(w3cPresentationPayload, prover) oRequestPresentation match case None => ZIO.fail(InvalidState("PresentationRecord 'RequestPending' with no Record")) case Some(requestPresentation) => // TODO create build method in mercury for Presentation @@ -252,35 +280,120 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { presentationService <- ZIO.service[PresentationService] prover <- createPrismDIDIssuerFromPresentationCredentials(id, credentialsToUse.getOrElse(Nil)) .provideSomeLayer(ZLayer.succeed(walletAccessContext)) - presentationPayload <- presentationService - .createPresentationPayloadFromRecord( - id, - prover, - Instant.now() - ) - .provideSomeLayer(ZLayer.succeed(walletAccessContext)) - signedJwtPresentation = JwtPresentation.toEncodedJwt( - presentationPayload.toW3CPresentationPayload, - prover - ) - presentation <- ZIO.succeed( - Presentation( - body = Presentation.Body( - goal_code = requestPresentation.body.goal_code, - comment = requestPresentation.body.comment - ), - attachments = Seq( - AttachmentDescriptor - .buildBase64Attachment( - payload = signedJwtPresentation.value.getBytes(), - mediaType = Some("prism/jwt") + presentation <- + for { + presentationPayload <- + presentationService + .createJwtPresentationPayloadFromRecord( + id, + prover, + Instant.now() ) - ), - thid = requestPresentation.thid.orElse(Some(requestPresentation.id)), - from = requestPresentation.to, - to = requestPresentation.from - ) - ) + .provideSomeLayer(ZLayer.succeed(walletAccessContext)) + signedJwtPresentation = JwtPresentation.toEncodedJwt( + presentationPayload.toW3CPresentationPayload, + prover + ) + presentation <- ZIO.succeed( + Presentation( + body = Presentation.Body( + goal_code = requestPresentation.body.goal_code, + comment = requestPresentation.body.comment + ), + attachments = Seq( + AttachmentDescriptor + .buildBase64Attachment( + payload = signedJwtPresentation.value.getBytes(), + mediaType = Some("prism/jwt") + ) + ), + thid = requestPresentation.thid.orElse(Some(requestPresentation.id)), + from = requestPresentation.to, + to = requestPresentation.from + ) + ) + } yield presentation + _ <- presentationService + .markPresentationGenerated(id, presentation) + .provideSomeLayer(ZLayer.succeed(walletAccessContext)) + } yield ()).mapError(e => (walletAccessContext, handlePresentationErrors(e))) + } yield result + + proverPresentationPendingToGeneratedFlow + @@ ProverPresentationPendingToGeneratedSuccess.trackSuccess + @@ ProverPresentationPendingToGeneratedFailed.trackError + @@ ProverPresentationPendingToGenerated + @@ Metric + .gauge("present_proof_flow_prover_presentation_pending_to_generated_flow_ms_gauge") + .trackDurationWith(_.toMetricsSeconds) + case PresentationRecord( + id, + _, + _, + _, + _, + _, + _, + _, + PresentationPending, + CredentialFormat.AnonCreds, + oRequestPresentation, + _, + _, + _, + _, + None, + _, + _, + _ + ) => // Prover + ZIO.fail(NotImplemented) + case PresentationRecord( + id, + _, + _, + _, + _, + _, + _, + _, + PresentationPending, + CredentialFormat.AnonCreds, + oRequestPresentation, + _, + _, + _, + _, + Some(credentialsToUseJson), + _, + _, + _ + ) => // Prover + oRequestPresentation match + case None => ZIO.fail(InvalidState("PresentationRecord 'RequestPending' with no Record")) + case Some(requestPresentation) => // TODO create build method in mercury for Presentation + val proverPresentationPendingToGeneratedFlow = for { + walletAccessContext <- buildWalletAccessContextLayer(requestPresentation.to) + result <- (for { + presentationService <- ZIO.service[PresentationService] + anoncredCredentialProofs <- + AnoncredCredentialProofsV1.schemaSerDes + .deserialize(credentialsToUseJson) + .mapError(error => PresentationError.UnexpectedError(error.error)) + prover <- createPrismDIDIssuerFromPresentationCredentials( + id, + anoncredCredentialProofs.credentialProofs.map(_.credential) + ).provideSomeLayer(ZLayer.succeed(walletAccessContext)) + presentation <- + presentationService + .createAnoncredPresentation( + requestPresentation, + id, + prover, + anoncredCredentialProofs, + Instant.now() + ) + .provideSomeLayer(ZLayer.succeed(walletAccessContext)) _ <- presentationService .markPresentationGenerated(id, presentation) .provideSomeLayer(ZLayer.succeed(walletAccessContext)) @@ -312,6 +425,8 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _, _, _, + _, + _, _ ) => // Prover ZIO.logDebug("PresentationRecord: PresentationGenerated") *> ZIO.unit @@ -354,7 +469,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { .gauge("present_proof_flow_prover_presentation_generated_to_sent_flow_ms_gauge") .trackDurationWith(_.toMetricsSeconds) - case PresentationRecord(id, _, _, _, _, _, _, _, PresentationSent, _, _, _, _, _, _, _, _) => + case PresentationRecord(id, _, _, _, _, _, _, _, PresentationSent, _, _, _, _, _, _, _, _, _, _) => ZIO.logDebug("PresentationRecord: PresentationSent") *> ZIO.unit case PresentationRecord( id, @@ -366,13 +481,15 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { _, _, PresentationReceived, - _, + CredentialFormat.JWT, mayBeRequestPresentation, _, presentation, _, _, _, + _, + _, _ ) => // Verifier ZIO.logDebug("PresentationRecord: PresentationReceived") *> ZIO.unit @@ -387,8 +504,7 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { didResolverService <- ZIO.service[JwtDidResolver] credentialsValidationResult <- p.attachments.head.data match { case Base64(data) => - // JWT verifiable presentation decided frin base64 - val base64Decoded = new String(java.util.Base64.getDecoder().decode(data)) + val base64Decoded = new String(java.util.Base64.getDecoder.decode(data)) val maybePresentationOptions : Either[PresentationError, Option[io.iohk.atala.pollux.core.model.presentation.Options]] = mayBeRequestPresentation @@ -505,14 +621,141 @@ object PresentBackgroundJobs extends BackgroundJobsHelper { "present_proof_flow_verifier_presentation_received_to_verification_success_or_failure_flow_ms_gauge" ) .trackDurationWith(_.toMetricsSeconds) - - case PresentationRecord(id, _, _, _, _, _, _, _, PresentationVerificationFailed, _, _, _, _, _, _, _, _) => + case PresentationRecord( + _, + _, + _, + _, + _, + _, + _, + _, + PresentationReceived, + CredentialFormat.AnonCreds, + None, + _, + _, + _, + _, + _, + _, + _, + _ + ) => + ZIO.fail(InvalidState("PresentationRecord in 'PresentationReceived' with no Presentation Request")) + case PresentationRecord( + _, + _, + _, + _, + _, + _, + _, + _, + PresentationReceived, + CredentialFormat.AnonCreds, + _, + _, + None, + _, + _, + _, + _, + _, + _ + ) => + ZIO.fail(InvalidState("PresentationRecord in 'PresentationReceived' with no Presentation")) + case PresentationRecord( + id, + _, + _, + _, + _, + _, + _, + _, + PresentationReceived, + CredentialFormat.AnonCreds, + Some(requestPresentation), + _, + Some(presentation), + _, + _, + _, + _, + _, + _ + ) => // Verifier + ZIO.logDebug("PresentationRecord: PresentationReceived") *> ZIO.unit + val verifierPresentationReceivedToProcessed = + for { + walletAccessContext <- buildWalletAccessContextLayer(presentation.to) + presReceivedToProcessedAspect = CustomMetricsAspect.endRecordingTime( + s"${record.id}_present_proof_flow_verifier_presentation_received_to_verification_success_or_failure_ms_gauge", + "present_proof_flow_verifier_presentation_received_to_verification_success_or_failure_ms_gauge" + ) + result <- (for { + service <- ZIO.service[PresentationService] + _ <- (service + .verifyAnoncredPresentation(presentation, requestPresentation, id) + .provideSomeLayer(ZLayer.succeed(walletAccessContext)) @@ presReceivedToProcessedAspect) + .flatMapError(e => + for { + didCommAgent <- buildDIDCommAgent(presentation.from).provideSomeLayer( + ZLayer.succeed(walletAccessContext) + ) + reportproblem = ReportProblem.build( + fromDID = presentation.to, + toDID = presentation.from, + pthid = presentation.thid.getOrElse(presentation.id), + code = ProblemCode("e.p.presentation-verification-failed"), + comment = Some(e.toString) + ) + _ <- MessagingService + .send(reportproblem.toMessage) + .provideSomeLayer(didCommAgent) + _ <- ZIO.log(s"CredentialsValidationResult: ${e.toString}") + } yield () + ZIO.succeed(e) + ) + } yield ()).mapError(e => (walletAccessContext, handlePresentationErrors(e))) + } yield result + verifierPresentationReceivedToProcessed + @@ VerifierPresentationReceivedToProcessedSuccess.trackSuccess + @@ VerifierPresentationReceivedToProcessedFailed.trackError + @@ VerifierPresentationReceivedToProcessed + @@ Metric + .gauge( + "present_proof_flow_verifier_presentation_received_to_verification_success_or_failure_flow_ms_gauge" + ) + .trackDurationWith(_.toMetricsSeconds) + case PresentationRecord( + id, + _, + _, + _, + _, + _, + _, + _, + PresentationVerificationFailed, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _ + ) => 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 } } yield () diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/IssueControllerImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/IssueControllerImpl.scala index a88521701b..bba5c66d68 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/IssueControllerImpl.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/issue/controller/IssueControllerImpl.scala @@ -78,21 +78,23 @@ class IssueControllerImpl( .mapError(_ => ErrorResponse.badRequest(detail = Some("Missing request parameter: credentialDefinitionId")) ) + credentialDefinitionId = { + val publicEndpointUrl = appConfig.agent.httpEndpoint.publicEndpointUrl + val urlSuffix = + s"credential-definition-registry/definitions/${credentialDefinitionGUID.toString}/definition" + val urlPrefix = if (publicEndpointUrl.endsWith("/")) publicEndpointUrl else publicEndpointUrl + "/" + s"$urlPrefix$urlSuffix" + } record <- credentialService .createAnonCredsIssueCredentialRecord( pairwiseIssuerDID = didIdPair.myDID, pairwiseHolderDID = didIdPair.theirDid, thid = DidCommID(), credentialDefinitionGUID = credentialDefinitionGUID, + credentialDefinitionId = credentialDefinitionId, claims = jsonClaims, validityPeriod = request.validityPeriod, - automaticIssuance = request.automaticIssuance.orElse(Some(true)), { - val publicEndpointUrl = appConfig.agent.httpEndpoint.publicEndpointUrl - val urlSuffix = - s"credential-definition-registry/definitions/${credentialDefinitionGUID.toString}/definition" - val urlPrefix = if (publicEndpointUrl.endsWith("/")) publicEndpointUrl else publicEndpointUrl + "/" - s"$urlPrefix$urlSuffix" - } + automaticIssuance = request.automaticIssuance.orElse(Some(true)) ) } yield record } yield IssueCredentialRecord.fromDomain(outcome) 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..12b4c74c09 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,21 @@ 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.AnoncredPresentationCreationError(cause) => + ErrorResponse.badRequest(title = "Error Creating Anoncred Presentation", detail = Some(cause.toString)) + case PresentationError.AnoncredPresentationVerificationError(cause) => + ErrorResponse.badRequest(title = "Error Verifying Prensetation", detail = Some(cause.toString)) + case PresentationError.InvalidAnoncredPresentationRequest(msg) => + ErrorResponse.badRequest(title = "Invalid Anoncred Presentation Request", detail = Some(msg)) + case PresentationError.InvalidAnoncredPresentation(msg) => + ErrorResponse.badRequest(title = "Invalid Anoncred Presentation", detail = Some(msg)) + case PresentationError.NotMatchingPresentationCredentialFormat(cause) => + ErrorResponse.badRequest( + title = "Presentation and Credential Format Not Matching", + detail = Some(cause.toString) + ) 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..1e05838d68 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 @@ -11,7 +11,7 @@ import io.iohk.atala.mercury.protocol.presentproof.ProofType 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.{CredentialFormat, DidCommID, PresentationRecord} -import io.iohk.atala.pollux.core.service.PresentationService +import io.iohk.atala.pollux.core.service.{PresentationService} import io.iohk.atala.presentproof.controller.PresentProofController.toDidCommID import io.iohk.atala.presentproof.controller.http.* import io.iohk.atala.shared.models.WalletAccessContext @@ -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 { @@ -92,10 +111,16 @@ class PresentProofControllerImpl( didCommId <- ZIO.succeed(DidCommID(id.toString)) record <- requestPresentationAction.action match { case "request-accept" => - presentationService.acceptRequestPresentation( - recordId = didCommId, - credentialsToUse = requestPresentationAction.proofId.getOrElse(Seq.empty) - ) + (requestPresentationAction.proofId, requestPresentationAction.anoncredPresentationRequest) match + case (Some(proofs), None) => + presentationService.acceptRequestPresentation(recordId = didCommId, credentialsToUse = proofs) + case (None, Some(proofs)) => + presentationService.acceptAnoncredRequestPresentation( + recordId = didCommId, + credentialsToUse = proofs + ) + case _ => presentationService.acceptRequestPresentation(recordId = didCommId, credentialsToUse = Seq()) + case "request-reject" => presentationService.rejectRequestPresentation(didCommId) case "presentation-accept" => presentationService.acceptPresentation(didCommId) case "presentation-reject" => presentationService.rejectPresentation(didCommId) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/http/RequestPresentationAction.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/http/RequestPresentationAction.scala index c88a97747e..114b08893a 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/http/RequestPresentationAction.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/presentproof/controller/http/RequestPresentationAction.scala @@ -1,6 +1,7 @@ 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.RequestPresentationAction.annotations import sttp.tapir.Schema.annotations.{description, encodedExample, validate} import sttp.tapir.{Schema, Validator} @@ -13,7 +14,10 @@ final case class RequestPresentationAction( action: String, @description(annotations.proofId.description) @encodedExample(annotations.proofId.example) - proofId: Option[Seq[String]] = None + proofId: Option[Seq[String]] = None, + @description(annotations.anoncredProof.description) + @encodedExample(annotations.anoncredProof.example) + anoncredPresentationRequest: Option[AnoncredCredentialProofsV1], ) object RequestPresentationAction { @@ -37,13 +41,33 @@ object RequestPresentationAction { "The unique identifier of the issue credential record - and hence VC - to use as the prover accepts the presentation request. Only applicable on the prover side when the action is `request-accept`.", example = None ) + + object anoncredProof + extends Annotation[Option[AnoncredCredentialProofsV1]]( + description = "A list of proofs from the Anoncred library, each corresponding to a credential.", + example = None + ) + + object credential + extends Annotation[String]( + description = + "The unique identifier of the issue credential record - and hence VC - to use as the prover accepts the presentation request. Only applicable on the prover side when the action is `request-accept`.", + example = "id" + ) } - given encoder: JsonEncoder[RequestPresentationAction] = + given RequestPresentationActionEncoder: JsonEncoder[RequestPresentationAction] = DeriveJsonEncoder.gen[RequestPresentationAction] - given decoder: JsonDecoder[RequestPresentationAction] = + given RequestPresentationActionDecoder: JsonDecoder[RequestPresentationAction] = DeriveJsonDecoder.gen[RequestPresentationAction] - given schema: Schema[RequestPresentationAction] = Schema.derived + given RequestPresentationActionSchema: Schema[RequestPresentationAction] = Schema.derived + + import AnoncredCredentialProofsV1.given + + given Schema[AnoncredCredentialProofsV1] = Schema.derived + + given Schema[AnoncredCredentialProofV1] = Schema.derived + } 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..9f3c677fa8 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.anoncredPresentationRequest.description) + @encodedExample(annotations.anoncredPresentationRequest.example) + anoncredPresentationRequest: Option[AnoncredPresentationRequestV1], @description(annotations.credentialFormat.description) @encodedExample(annotations.credentialFormat.example) credentialFormat: Option[String], @@ -42,6 +46,54 @@ object RequestPresentationInput { 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 credentialFormat extends Annotation[Option[String]]( description = "The credential format (default to 'JWT')", @@ -61,5 +113,15 @@ 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: Schema[RequestPresentationInput] = Schema.derived } diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/issue/controller/IssueControllerTestTools.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/issue/controller/IssueControllerTestTools.scala index 0ae1658d10..d835a2724d 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/issue/controller/IssueControllerTestTools.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/issue/controller/IssueControllerTestTools.scala @@ -16,7 +16,7 @@ import io.iohk.atala.issue.controller.http.{ IssueCredentialRecord, IssueCredentialRecordPage } -import io.iohk.atala.pollux.anoncreds.LinkSecretWithId +import io.iohk.atala.pollux.anoncreds.AnoncredLinkSecretWithId import io.iohk.atala.pollux.core.model.CredentialFormat import io.iohk.atala.pollux.core.repository.{ CredentialDefinitionRepositoryInMemory, diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala index 4b53450a27..ffabec1c77 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/pollux/credentialdefinition/CredentialDefinitionBasicSpec.scala @@ -1,7 +1,6 @@ package io.iohk.atala.pollux.credentialdefinition -import io.iohk.atala.agent.walletapi.model.BaseEntity -import io.iohk.atala.agent.walletapi.model.Entity +import io.iohk.atala.agent.walletapi.model.{BaseEntity, Entity} import io.iohk.atala.agent.walletapi.storage.GenericSecretStorage import io.iohk.atala.api.http.ErrorResponse import io.iohk.atala.container.util.MigrationAspects.* @@ -112,25 +111,22 @@ object CredentialDefinitionBasicSpec extends ZIOSpecDefault with CredentialDefin maybeValidPublicDefinition <- PublicCredentialDefinitionSerDesV1.schemaSerDes.validate( fetchedCredentialDefinition.definition.toString() ) - assertValidPublicDefinition = assert(maybeValidPublicDefinition)(Assertion.isTrue) + assertValidPublicDefinition = assert(maybeValidPublicDefinition)(Assertion.isUnit) maybeValidKeyCorrectnessProof <- ProofKeyCredentialDefinitionSchemaSerDesV1.schemaSerDes.validate( fetchedCredentialDefinition.keyCorrectnessProof.toString() ) - assertValidKeyCorrectnessProof = assert(maybeValidKeyCorrectnessProof)(Assertion.isTrue) + assertValidKeyCorrectnessProof = assert(maybeValidKeyCorrectnessProof)(Assertion.isUnit) storage <- ZIO.service[GenericSecretStorage] maybeDidSecret <- storage .get[UUID, CredentialDefinitionSecret](fetchedCredentialDefinition.guid) .provideSomeLayer(Entity.Default.wacLayer) maybeValidPrivateDefinitionZIO = maybeDidSecret match { case Some(didSecret) => - val validPrivateDefinition = - PrivateCredentialDefinitionSchemaSerDesV1.schemaSerDes.validate(didSecret.json.toString()) - validPrivateDefinition - case None => - ZIO.succeed(false) + PrivateCredentialDefinitionSchemaSerDesV1.schemaSerDes.validate(didSecret.json.toString()) + case None => ZIO.unit } maybeValidPrivateDefinition <- maybeValidPrivateDefinitionZIO - assertValidPrivateDefinition = assert(maybeValidPrivateDefinition)(Assertion.isTrue) + assertValidPrivateDefinition = assert(maybeValidPrivateDefinition)(Assertion.isUnit) } yield statusCodeIs201 && credentialDefinitionIsCreated && credentialDefinitionIsFetched &&