diff --git a/mercury/mercury-library/agent-cli-didcommx/src/main/scala/io/iohk/atala/mercury/AgentCli.scala b/mercury/mercury-library/agent-cli-didcommx/src/main/scala/io/iohk/atala/mercury/AgentCli.scala index 12cbca93ff..de46f6b496 100644 --- a/mercury/mercury-library/agent-cli-didcommx/src/main/scala/io/iohk/atala/mercury/AgentCli.scala +++ b/mercury/mercury-library/agent-cli-didcommx/src/main/scala/io/iohk/atala/mercury/AgentCli.scala @@ -20,6 +20,9 @@ import io.iohk.atala.resolvers.UniversalDidResolver import io.iohk.atala.mercury.protocol.connection.* import io.iohk.atala.mercury.protocol.invitation.v2.Invitation import io.iohk.atala.resolvers.DIDResolver +import io.circe.Json +import io.circe.parser.* +import io.circe.syntax.* /** AgentCli * {{{ @@ -187,12 +190,14 @@ object AgentCli extends ZIOAppDefault { } // Make a Request - body = RequestPresentation.Body(goal_code = Some("Propose Presentation")) - attachmentDescriptor = AttachmentDescriptor( - "1", - Some("application/json"), - LinkData(links = Seq("http://test"), hash = "1234") + body = RequestPresentation.Body(goal_code = Some("Presentation Request")) + presentationAttachment = PresentationAttachment.build( + Some(Options(challenge = "somechallenge", domain = "somedomain")) ) + // attachmentDescriptor = AttachmentDescriptor.buildBase64Attachment(payload = + // presentationAttachment.asJson.noSpaces.getBytes() + // ) + attachmentDescriptor = AttachmentDescriptor.buildJsonAttachment(payload = presentationAttachment) requestPresentation = RequestPresentation( body = body, attachments = Seq(attachmentDescriptor), diff --git a/mercury/mercury-library/agent-didcommx/src/main/scala/io/iohk/atala/mercury/model/Conversions.scala b/mercury/mercury-library/agent-didcommx/src/main/scala/io/iohk/atala/mercury/model/Conversions.scala index f2bcc311ab..3df958781b 100644 --- a/mercury/mercury-library/agent-didcommx/src/main/scala/io/iohk/atala/mercury/model/Conversions.scala +++ b/mercury/mercury-library/agent-didcommx/src/main/scala/io/iohk/atala/mercury/model/Conversions.scala @@ -56,6 +56,37 @@ def json2Map(json: Json): Any = json match { case _ => null // Impossible case but Json cases are private in circe ... } +def mapValueToJson(obj: java.lang.Object): Json = { + obj match { + case null => Json.Null + case b: java.lang.Boolean => Json.fromBoolean(b) + case i: java.lang.Integer => Json.fromInt(i) + case d: java.lang.Double => + Json.fromDouble(d).getOrElse(Json.fromDouble(0d).get) + case l: java.lang.Long => Json.fromLong(l) + case s: java.lang.String => Json.fromString(String.valueOf(s)) + case array: com.nimbusds.jose.shaded.json.JSONArray => { + Json.fromValues(array.iterator().asScala.map(mapValueToJson).toList) + } + case joseObject: com.nimbusds.jose.shaded.json.JSONObject => + Json.fromJsonObject { + JsonObject.fromMap( + joseObject + .asInstanceOf[java.util.Map[String, Object]] + .asScala + .toMap + .view + .mapValues(mapValueToJson) + .toMap + ) + } + case any => { + println("*****NotImplemented***" + any.getClass().getCanonicalName() + "**********") // FIXME + ??? + } + } +} + given Conversion[AttachmentDescriptor, XAttachment] with { def apply(attachment: AttachmentDescriptor): XAttachment = { val id = attachment.id @@ -82,8 +113,8 @@ given Conversion[XAttachment, AttachmentDescriptor] with { val data: AttachmentData = attachment.getData().toJSONObject.asScala.toMap match { case e if e contains ("json") => val aux = e("json") - println(aux.getClass().getCanonicalName()) // TODO - ??? + val x = aux.asInstanceOf[java.util.Map[String, Object]].asScala.toMap.view.mapValues(mapValueToJson) + JsonData(JsonObject.fromMap(x.toMap)) case e if e contains ("base64") => val tmp = e("base64").asInstanceOf[String] // ... Base64(tmp) diff --git a/mercury/mercury-library/models/src/main/scala/io/iohk/atala/mercury/model/AttachmentDescriptor.scala b/mercury/mercury-library/models/src/main/scala/io/iohk/atala/mercury/model/AttachmentDescriptor.scala index c6da09d962..75bb99a202 100644 --- a/mercury/mercury-library/models/src/main/scala/io/iohk/atala/mercury/model/AttachmentDescriptor.scala +++ b/mercury/mercury-library/models/src/main/scala/io/iohk/atala/mercury/model/AttachmentDescriptor.scala @@ -43,7 +43,7 @@ object LinkData { } -final case class JsonData(data: JsonObject) extends AttachmentData +final case class JsonData(json: JsonObject) extends AttachmentData object JsonData { given Encoder[JsonData] = deriveEncoder[JsonData] given Decoder[JsonData] = deriveDecoder[JsonData] diff --git a/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/Utils.scala b/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/Utils.scala index 42a74a83ff..5cda6ded11 100644 --- a/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/Utils.scala +++ b/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/Utils.scala @@ -31,7 +31,7 @@ private[this] trait ReadAttachmentsUtils { case obj: JsonData => java.util.Base64 .getUrlEncoder() - .encode(obj.data.asJson.noSpaces.getBytes()) + .encode(obj.json.asJson.noSpaces.getBytes()) }) maybeAttachament.map(formatName -> _) } diff --git a/mercury/mercury-library/protocol-present-proof/src/main/scala/io/iohk/atala/mercury/protocol/presentproof/PresentationAttachment.scala b/mercury/mercury-library/protocol-present-proof/src/main/scala/io/iohk/atala/mercury/protocol/presentproof/PresentationAttachment.scala new file mode 100644 index 0000000000..b22dafd23e --- /dev/null +++ b/mercury/mercury-library/protocol-present-proof/src/main/scala/io/iohk/atala/mercury/protocol/presentproof/PresentationAttachment.scala @@ -0,0 +1,79 @@ +package io.iohk.atala.mercury.protocol.presentproof + +import io.circe._ +import io.circe.generic.semiauto._ +import io.circe.syntax._ + +case class Field( + id: Option[String] = None, + path: Seq[String] = Seq.empty, + name: Option[String] = None, + purpose: Option[String] = None, +) +object Field { + given Encoder[Field] = deriveEncoder[Field] + given Decoder[Field] = deriveDecoder[Field] +} + +case class Jwt(alg: Seq[String], proof_type: Seq[String]) +object Jwt { + given Encoder[Jwt] = deriveEncoder[Jwt] + given Decoder[Jwt] = deriveDecoder[Jwt] +} +case class ClaimFormat(jwt: Jwt) +object ClaimFormat { + given Encoder[ClaimFormat] = deriveEncoder[ClaimFormat] + given Decoder[ClaimFormat] = deriveDecoder[ClaimFormat] +} +case class Constraints(fields: Option[Seq[Field]]) +object Constraints { + given Encoder[Constraints] = deriveEncoder[Constraints] + given Decoder[Constraints] = deriveDecoder[Constraints] +} + +/** Refer to Input Descriptors + */ +case class InputDescriptor( + id: String = java.util.UUID.randomUUID.toString(), + name: Option[String] = None, + purpose: Option[String] = None, + format: Option[ClaimFormat] = None, + constraints: Constraints +) +object InputDescriptor { + given Encoder[InputDescriptor] = deriveEncoder[InputDescriptor] + given Decoder[InputDescriptor] = deriveDecoder[InputDescriptor] +} + +/** Refer to Presentation + * Definition + */ +case class PresentationDefinition( + id: String = java.util.UUID.randomUUID.toString(), // UUID + input_descriptors: Seq[InputDescriptor] = Seq.empty, + name: Option[String] = None, + purpose: Option[String] = None, + format: Option[ClaimFormat] = None, +) +object PresentationDefinition { + given Encoder[PresentationDefinition] = deriveEncoder[PresentationDefinition] + given Decoder[PresentationDefinition] = deriveDecoder[PresentationDefinition] +} + +case class Options(challenge: String, domain: String) +object Options { + given Encoder[Options] = deriveEncoder[Options] + given Decoder[Options] = deriveDecoder[Options] +} + +case class PresentationAttachment(options: Option[Options] = None, presentation_definition: PresentationDefinition) +object PresentationAttachment { + given Encoder[PresentationAttachment] = deriveEncoder[PresentationAttachment] + given Decoder[PresentationAttachment] = deriveDecoder[PresentationAttachment] + + def build(options: Option[Options] = None): PresentationAttachment = { + val presentationDefinition = + PresentationDefinition(input_descriptors = Seq.empty) + PresentationAttachment(options, presentationDefinition) + } +} diff --git a/mercury/mercury-library/protocol-present-proof/src/test/scala/io/iohk/atala/mercury/protocol/presentproof/PresentationAttachmentSpec.scala b/mercury/mercury-library/protocol-present-proof/src/test/scala/io/iohk/atala/mercury/protocol/presentproof/PresentationAttachmentSpec.scala new file mode 100644 index 0000000000..5fbf8778cc --- /dev/null +++ b/mercury/mercury-library/protocol-present-proof/src/test/scala/io/iohk/atala/mercury/protocol/presentproof/PresentationAttachmentSpec.scala @@ -0,0 +1,141 @@ +package io.iohk.atala.mercury.protocol.presentproof + +import cats.implicits.* +import io.circe.Json +import io.circe.parser.* +import io.circe.syntax.* +import io.iohk.atala.mercury.model.AttachmentDescriptor.attachmentDescriptorEncoderV2 +import io.iohk.atala.mercury.model.{AttachmentDescriptor, DidId} +import munit.* +import zio.* +import io.iohk.atala.mercury.model._ + +class PresentationAttachmentSpec extends ZSuite { + + test("Verifier Request Presentation Attachment") { + val expectedConstraintJson = parse(s""" + { + "fields": [ + { + "path": [ + "credentialSubject.dateOfBirth", + "credentialSubject.dob", + "vc.credentialSubject.dateOfBirth", + "vc.credentialSubject.dob" + ] + } + ] + } + """.stripMargin).getOrElse(Json.Null) + val field = Field( + None, + path = Seq( + "credentialSubject.dateOfBirth", + "credentialSubject.dob", + "vc.credentialSubject.dateOfBirth", + "vc.credentialSubject.dob" + ) + ) + val constraints = Constraints(fields = Some(Seq(field))) + val result = constraints.asJson.deepDropNullValues + assertEquals(result, expectedConstraintJson) + + val expectedInputDescriptorJson = parse(s""" + { + "id": "wa_driver_license", + "name": "Washington State Business License", + "purpose": "We can only allow licensed Washington State business representatives into the WA Business Conference", + "constraints": { + "fields": [ + { + "path": [ + "credentialSubject.dateOfBirth", + "credentialSubject.dob", + "vc.credentialSubject.dateOfBirth", + "vc.credentialSubject.dob" + ] + } + ] + } + } + """.stripMargin).getOrElse(Json.Null) + + val inputDescriptor = InputDescriptor( + id = "wa_driver_license", + name = Some("Washington State Business License"), + purpose = + Some("We can only allow licensed Washington State business representatives into the WA Business Conference"), + constraints = constraints + ) + val resultInputDescriptor = inputDescriptor.asJson.deepDropNullValues + assertEquals(resultInputDescriptor, expectedInputDescriptorJson) + + val expectedPresentationDefinitionJson = parse(s""" + { + "id": "32f54163-7166-48f1-93d8-ff217bdb0653", + "input_descriptors": [ + { + "id": "wa_driver_license", + "name": "Washington State Business License", + "purpose": "We can only allow licensed Washington State business representatives into the WA Business Conference", + "constraints": { + "fields": [ + { + "path": [ + "credentialSubject.dateOfBirth", + "credentialSubject.dob", + "vc.credentialSubject.dateOfBirth", + "vc.credentialSubject.dob" + ] + } + ] + } + } + ] + } + """.stripMargin).getOrElse(Json.Null) + + val presentationDefinition = + PresentationDefinition(id = "32f54163-7166-48f1-93d8-ff217bdb0653", input_descriptors = Seq(inputDescriptor)) + val resultPresentationDefinition = presentationDefinition.asJson.deepDropNullValues + assertEquals(resultPresentationDefinition, expectedPresentationDefinitionJson) + + val expectedPresentationAttachmentJson = parse(s""" + { + "options": { + "challenge": "23516943-1d79-4ebd-8981-623f036365ef", + "domain": "us.gov/DriversLicense" + }, + "presentation_definition": { + "id": "32f54163-7166-48f1-93d8-ff217bdb0653", + "input_descriptors": [ + { + "id": "wa_driver_license", + "name": "Washington State Business License", + "purpose": "We can only allow licensed Washington State business representatives into the WA Business Conference", + "constraints": { + "fields": [ + { + "path": [ + "credentialSubject.dateOfBirth", + "credentialSubject.dob", + "vc.credentialSubject.dateOfBirth", + "vc.credentialSubject.dob" + ] + } + ] + } + } + ] + } + } + """.stripMargin).getOrElse(Json.Null) + val options = Options(challenge = "23516943-1d79-4ebd-8981-623f036365ef", domain = "us.gov/DriversLicense") + val presentationAttachment = + PresentationAttachment(presentation_definition = presentationDefinition, options = Some(options)) + val resultPresentationAttachment = presentationAttachment.asJson.deepDropNullValues + println(resultPresentationAttachment) + assertEquals(resultPresentationAttachment, expectedPresentationAttachmentJson) + + } +}