Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mercury): add utils methods on issue-credential-protocol #131

Merged
merged 1 commit into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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