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 d374128c20..a6031e4725 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 @@ -102,13 +102,13 @@ final case class AttachmentDescriptor( object AttachmentDescriptor { - def buildAttachment[A: Encoder]( + def buildAttachment[A]( id: String = java.util.UUID.randomUUID.toString, payload: A, mediaType: Option[String] = Some("application/json") - ): AttachmentDescriptor = { + )(using Encoder[A]): AttachmentDescriptor = { val encoded = JBase64.getUrlEncoder.encodeToString(payload.asJson.noSpaces.getBytes) - AttachmentDescriptor(id, mediaType, Base64(encoded)) + AttachmentDescriptor(id, mediaType, Base64(encoded)) // use JsonData or Base64 by default? } given attachmentDescriptorEncoderV1: Encoder[AttachmentDescriptor] = (a: AttachmentDescriptor) => { diff --git a/mercury/mercury-library/protocol-issue-credential/src/Issue-Credential-Protocol.md b/mercury/mercury-library/protocol-issue-credential/Issue-Credential-Protocol.md similarity index 92% rename from mercury/mercury-library/protocol-issue-credential/src/Issue-Credential-Protocol.md rename to mercury/mercury-library/protocol-issue-credential/Issue-Credential-Protocol.md index 99b3a28b6c..4b31439ab4 100644 --- a/mercury/mercury-library/protocol-issue-credential/src/Issue-Credential-Protocol.md +++ b/mercury/mercury-library/protocol-issue-credential/Issue-Credential-Protocol.md @@ -6,13 +6,18 @@ Its a Issue Credential protocol based on DIDCOMMv2 message format. A standard protocol for issuing credentials. This is the basis of interoperability between Issuers and Holders. -See [https://identity.foundation/didcomm-messaging/spec] -See [https://github.com/hyperledger/aries-rfcs/tree/main/features/0453-issue-credential-v2] +- See [https://identity.foundation/didcomm-messaging/spec] +- See [https://github.com/hyperledger/aries-rfcs/tree/main/features/0453-issue-credential-v2] + +Others: +- See [https://didcomm.org/issue-credential/3.0] +- See [https://github.com/decentralized-identity/waci-didcomm/tree/main/issue_credential] ## PIURI Version 1.0: `https://didcomm.org/issue-credential/1.0/propose-credential` + ### Roles - Issuer diff --git a/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/IssueCredential.scala b/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/IssueCredential.scala index 9f5ac8bb3c..66c9e5b41a 100644 --- a/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/IssueCredential.scala +++ b/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/IssueCredential.scala @@ -24,7 +24,7 @@ final case class IssueCredential( thid: Option[String] = None, from: DidId, to: DidId, -) { +) extends ReadAttachmentsUtils { assert(`type` == IssueCredential.`type`) def makeMessage: Message = Message( @@ -41,20 +41,37 @@ final case class IssueCredential( object IssueCredential { import AttachmentDescriptor.attachmentDescriptorEncoderV2 - given Encoder[IssueCredential] = deriveEncoder[IssueCredential] - given Decoder[IssueCredential] = deriveDecoder[IssueCredential] def `type`: PIURI = "https://didcomm.org/issue-credential/2.0/issue-credential" + def build[A]( + fromDID: DidId, + toDID: DidId, + thid: Option[String] = None, + credentials: Map[String, A], + )(using Encoder[A]): IssueCredential = { + val aux = credentials.map { case (formatName, singleCredential) => + val attachment = AttachmentDescriptor.buildAttachment(payload = singleCredential) + val credentialFormat: CredentialFormat = CredentialFormat(attachment.id, formatName) + (credentialFormat, attachment) + } + IssueCredential( + thid = thid, + from = fromDID, + to = toDID, + body = Body(formats = aux.keys.toSeq), + attachments = aux.values.toSeq + ) + } final case class Body( goal_code: Option[String] = None, comment: Option[String] = None, replacement_id: Option[String] = None, more_available: Option[String] = None, formats: Seq[CredentialFormat] = Seq.empty[CredentialFormat], - ) + ) extends BodyUtils object Body { given Encoder[Body] = deriveEncoder[Body] given Decoder[Body] = deriveDecoder[Body] diff --git a/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/OfferCredential.scala b/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/OfferCredential.scala index 7d8f66b1b4..50df52d7dd 100644 --- a/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/OfferCredential.scala +++ b/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/OfferCredential.scala @@ -27,7 +27,7 @@ final case class OfferCredential( thid: Option[String] = None, from: DidId, to: DidId, -) { +) extends ReadAttachmentsUtils { assert(`type` == OfferCredential.`type`) def makeMessage: Message = Message( @@ -51,6 +51,27 @@ object OfferCredential { def `type`: PIURI = "https://didcomm.org/issue-credential/2.0/offer-credential" + def build[A]( + fromDID: DidId, + toDID: DidId, + thid: Option[String] = None, + credential_preview: CredentialPreview, + credentials: Map[String, A], + )(using Encoder[A]): OfferCredential = { + val aux = credentials.map { case (formatName, singleCredential) => + val attachment = AttachmentDescriptor.buildAttachment(payload = singleCredential) + val credentialFormat: CredentialFormat = CredentialFormat(attachment.id, formatName) + (credentialFormat, attachment) + } + OfferCredential( + thid = thid, + from = fromDID, + to = toDID, + body = Body(credential_preview = credential_preview, formats = aux.keys.toSeq), + attachments = aux.values.toSeq + ) + } + final case class Body( goal_code: Option[String] = None, comment: Option[String] = None, @@ -58,7 +79,7 @@ object OfferCredential { multiple_available: Option[String] = None, credential_preview: CredentialPreview, formats: Seq[CredentialFormat] = Seq.empty[CredentialFormat] - ) + ) extends BodyUtils object Body { given Encoder[Body] = deriveEncoder[Body] diff --git a/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/ProposeCredential.scala b/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/ProposeCredential.scala index 6d94003f4e..f0ec70d865 100644 --- a/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/ProposeCredential.scala +++ b/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/ProposeCredential.scala @@ -25,7 +25,7 @@ final case class ProposeCredential( thid: Option[String] = None, from: DidId, to: DidId, -) { +) extends ReadAttachmentsUtils { assert(`type` == ProposeCredential.`type`) def makeMessage: Message = Message( @@ -41,6 +41,27 @@ object ProposeCredential { // TODD will this be version RCF Issue Credential 2.0 as we use didcomm2 message format def `type`: PIURI = "https://didcomm.org/issue-credential/2.0/propose-credential" + def build[A]( + fromDID: DidId, + toDID: DidId, + thid: Option[String] = None, + credential_preview: CredentialPreview, + credentials: Map[String, A] = Map.empty, + )(using Encoder[A]): ProposeCredential = { + val aux = credentials.map { case (formatName, singleCredential) => + val attachment = AttachmentDescriptor.buildAttachment(payload = singleCredential) + val credentialFormat: CredentialFormat = CredentialFormat(attachment.id, formatName) + (credentialFormat, attachment) + } + ProposeCredential( + thid = thid, + from = fromDID, + to = toDID, + body = Body(credential_preview = credential_preview, formats = aux.keys.toSeq), + attachments = aux.values.toSeq + ) + } + import AttachmentDescriptor.attachmentDescriptorEncoderV2 given Encoder[ProposeCredential] = deriveEncoder[ProposeCredential] given Decoder[ProposeCredential] = deriveDecoder[ProposeCredential] @@ -55,7 +76,7 @@ object ProposeCredential { comment: Option[String] = None, credential_preview: CredentialPreview, // JSON STRinf formats: Seq[CredentialFormat] = Seq.empty[CredentialFormat] - ) + ) extends BodyUtils object Body { given Encoder[Body] = deriveEncoder[Body] diff --git a/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/RequestCredential.scala b/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/RequestCredential.scala index 01f8ac434f..acb0aab303 100644 --- a/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/RequestCredential.scala +++ b/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/RequestCredential.scala @@ -16,7 +16,7 @@ final case class RequestCredential( thid: Option[String] = None, from: DidId, to: DidId, -) { +) extends ReadAttachmentsUtils { def makeMessage: Message = Message( id = this.id, @@ -31,18 +31,36 @@ final case class RequestCredential( object RequestCredential { import AttachmentDescriptor.attachmentDescriptorEncoderV2 - given Encoder[RequestCredential] = deriveEncoder[RequestCredential] - given Decoder[RequestCredential] = deriveDecoder[RequestCredential] def `type`: PIURI = "https://didcomm.org/issue-credential/2.0/request-credential" + def build[A]( + fromDID: DidId, + toDID: DidId, + thid: Option[String] = None, + credentials: Map[String, A] = Map.empty, + )(using Encoder[A]): RequestCredential = { + val aux = credentials.map { case (formatName, singleCredential) => + val attachment = AttachmentDescriptor.buildAttachment(payload = singleCredential) + val credentialFormat: CredentialFormat = CredentialFormat(attachment.id, formatName) + (credentialFormat, attachment) + } + RequestCredential( + thid = thid, + from = fromDID, + to = toDID, + body = Body(formats = aux.keys.toSeq), + attachments = aux.values.toSeq + ) + } + final case class Body( goal_code: Option[String] = None, comment: Option[String] = None, formats: Seq[CredentialFormat] = Seq.empty[CredentialFormat] - ) + ) extends BodyUtils object Body { given Encoder[Body] = deriveEncoder[Body] 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 new file mode 100644 index 0000000000..42a74a83ff --- /dev/null +++ b/mercury/mercury-library/protocol-issue-credential/src/main/scala/io/iohk/atala/mercury/protocol/issuecredential/Utils.scala @@ -0,0 +1,49 @@ +package io.iohk.atala.mercury.protocol.issuecredential + +import io.circe.syntax._ +import io.circe.parser._ + +import io.iohk.atala.mercury.model._ +import io.circe.Decoder + +private[this] trait BodyUtils { + def formats: Seq[CredentialFormat] +} + +private[this] trait ReadAttachmentsUtils { + + def body: BodyUtils + def attachments: Seq[AttachmentDescriptor] + + /** @return + * maping between the credential format name and the credential data in an array of Bytes encoded in base 64 + */ + // protected inline + lazy val getCredentialFormatAndCredential: Map[String, Array[Byte]] = + body.formats + .map { case CredentialFormat(id, formatName) => + val maybeAttachament = attachments + .find(_.id == id) + .map(_.data match { + case obj: JwsData => ??? // TODO + case obj: Base64 => obj.base64.getBytes() + case obj: LinkData => ??? // TODO Does this make sens + case obj: JsonData => + java.util.Base64 + .getUrlEncoder() + .encode(obj.data.asJson.noSpaces.getBytes()) + }) + maybeAttachament.map(formatName -> _) + } + .flatten + .toMap +// eyJhIjoiYSIsImIiOjEsIngiOjIsIm5hbWUiOiJNeU5hbWUiLCJkb2IiOiI_PyJ9 + def getCredential[A](credentialFormatName: String)(using decodeA: Decoder[A]): Option[A] = + getCredentialFormatAndCredential + .get(credentialFormatName) + .map(java.util.Base64.getUrlDecoder().decode(_)) + .map(String(_)) + .map(e => decode[A](e)) + .flatMap(_.toOption) + +} diff --git a/mercury/mercury-library/protocol-issue-credential/src/test/scala/io/iohk/atala/mercury/protocol/anotherclasspath/UtilsCredentialSpec.scala b/mercury/mercury-library/protocol-issue-credential/src/test/scala/io/iohk/atala/mercury/protocol/anotherclasspath/UtilsCredentialSpec.scala new file mode 100644 index 0000000000..414a15ea87 --- /dev/null +++ b/mercury/mercury-library/protocol-issue-credential/src/test/scala/io/iohk/atala/mercury/protocol/anotherclasspath/UtilsCredentialSpec.scala @@ -0,0 +1,109 @@ +package io.iohk.atala.mercury.protocol.anotherclasspath + +import cats.implicits.* +import io.circe.* +import io.circe.parser.* +import io.circe.syntax.* +import io.circe.generic.semiauto.* +import zio.* +import munit.* + +import io.iohk.atala.mercury.model._ +import io.iohk.atala.mercury.protocol.issuecredential._ + +private[this] case class TestCredentialType(a: String, b: Int, x: Long, name: String, dob: String) +private[this] object TestCredentialType { + given Encoder[TestCredentialType] = deriveEncoder[TestCredentialType] + given Decoder[TestCredentialType] = deriveDecoder[TestCredentialType] +} + +/** testOnly io.iohk.atala.mercury.protocol.anotherclasspath.UtilsCredentialSpec + */ +class UtilsCredentialSpec extends ZSuite { + val nameCredentialType = "prism/TestCredentialType" + + val credential = TestCredentialType( + a = "a", + b = 1, + x = 2, + name = "MyName", + dob = "??" + ) + + val credentialPreview = CredentialPreview(attributes = Seq()) + val body = OfferCredential.Body( + goal_code = Some("Offer Credential"), + credential_preview = credentialPreview + ) + val attachmentDescriptor = AttachmentDescriptor.buildAttachment(payload = credential) + + test("IssueCredential encode and decode any type of Credential into the attachments") { + + val msg = IssueCredential + .build( + fromDID = DidId("did:prism:test123from"), + toDID = DidId("did:prism:test123to"), + credentials = Map(nameCredentialType -> credential), + ) + .makeMessage + + val obj = IssueCredential.readFromMessage(msg) + + assertEquals(obj.getCredentialFormatAndCredential.size, 1) + assertEquals(obj.getCredentialFormatAndCredential.keySet, Set(nameCredentialType)) + assertEquals(obj.getCredential[TestCredentialType](nameCredentialType), Some(credential)) + } + + test("OfferCredential encode and decode any type of Credential into the attachments") { + + val msg = OfferCredential + .build( + fromDID = DidId("did:prism:test123from"), + toDID = DidId("did:prism:test123to"), + credential_preview = credentialPreview, + credentials = Map(nameCredentialType -> credential), + ) + .makeMessage + + val obj = OfferCredential.readFromMessage(msg) + + assertEquals(obj.getCredentialFormatAndCredential.size, 1) + assertEquals(obj.getCredentialFormatAndCredential.keySet, Set(nameCredentialType)) + assertEquals(obj.getCredential[TestCredentialType](nameCredentialType), Some(credential)) + } + + test("ProposeCredential encode and decode any type of Credential into the attachments") { + + val msg = ProposeCredential + .build( + fromDID = DidId("did:prism:test123from"), + toDID = DidId("did:prism:test123to"), + credential_preview = credentialPreview, + credentials = Map(nameCredentialType -> credential), + ) + .makeMessage + + val obj = ProposeCredential.readFromMessage(msg) + + assertEquals(obj.getCredentialFormatAndCredential.size, 1) + assertEquals(obj.getCredentialFormatAndCredential.keySet, Set(nameCredentialType)) + assertEquals(obj.getCredential[TestCredentialType](nameCredentialType), Some(credential)) + } + + test("RequestCredential encode and decode any type of Credential into the attachments") { + + val msg = RequestCredential + .build( + fromDID = DidId("did:prism:test123from"), + toDID = DidId("did:prism:test123to"), + credentials = Map(nameCredentialType -> credential), + ) + .makeMessage + + val obj = RequestCredential.readFromMessage(msg) + + assertEquals(obj.getCredentialFormatAndCredential.size, 1) + assertEquals(obj.getCredentialFormatAndCredential.keySet, Set(nameCredentialType)) + assertEquals(obj.getCredential[TestCredentialType](nameCredentialType), Some(credential)) + } +} diff --git a/mercury/mercury-library/protocol-issue-credential/src/test/scala/io/iohk/atala/mercury/protocol/issuecredential/IssueCredentialSpec.scala b/mercury/mercury-library/protocol-issue-credential/src/test/scala/io/iohk/atala/mercury/protocol/issuecredential/IssueCredentialSpec.scala index dd499289ef..cfe5e0e034 100644 --- a/mercury/mercury-library/protocol-issue-credential/src/test/scala/io/iohk/atala/mercury/protocol/issuecredential/IssueCredentialSpec.scala +++ b/mercury/mercury-library/protocol-issue-credential/src/test/scala/io/iohk/atala/mercury/protocol/issuecredential/IssueCredentialSpec.scala @@ -4,7 +4,6 @@ import io.circe.Json import io.circe.parser.* import io.circe.syntax.* import io.iohk.atala.mercury.model.AttachmentDescriptor -import io.iohk.atala.mercury.model.AttachmentDescriptor.attachmentDescriptorEncoderV2 import munit.* import io.iohk.atala.mercury.model._ import zio.* @@ -18,7 +17,11 @@ class IssueCredentialSpec extends ZSuite { val credentialPreview = CredentialPreview(attributes = Seq(attribute1, attribute2)) val body = IssueCredential.Body(goal_code = Some("Issued Credential")) val attachmentDescriptor = AttachmentDescriptor.buildAttachment[CredentialPreview](payload = credentialPreview) - val attachmentDescriptorJson = attachmentDescriptor.asJson.deepDropNullValues.noSpaces + val attachmentDescriptorJson = + attachmentDescriptor + .asJson(AttachmentDescriptor.attachmentDescriptorEncoderV2) + .deepDropNullValues + .noSpaces val expectedProposalJson = parse(s"""{ | "id": "061bf917-2cbe-460b-8d12-b1a9609505c2",