Skip to content

Commit

Permalink
feat(mercury): add utils methods on issue-credential-protocol (#131)
Browse files Browse the repository at this point in the history
  • Loading branch information
FabioPinheiro authored Nov 16, 2022
1 parent b39e38f commit 5a3e2fd
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -51,14 +51,35 @@ 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,
replacement_id: Option[String] = None,
multiple_available: Option[String] = None,
credential_preview: CredentialPreview,
formats: Seq[CredentialFormat] = Seq.empty[CredentialFormat]
)
) extends BodyUtils

object Body {
given Encoder[Body] = deriveEncoder[Body]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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]
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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)

}
Original file line number Diff line number Diff line change
@@ -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))
}
}
Loading

0 comments on commit 5a3e2fd

Please sign in to comment.