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: use the compact format in SD-JWT #1169

Merged
merged 5 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -842,21 +842,23 @@ object PresentBackgroundJobs extends BackgroundJobsHelper {
presentation <- ZIO.succeed(PresentationJson(base64Decoded))
iss <- ZIO.fromEither(presentation.iss)
ed25519PublicKey <- resolveToEd25519PublicKey(iss)
verifiedClaims = SDJWT.getVerifiedClaims(
ret = SDJWT.getVerifiedClaims(
IssuerPublicKey(ed25519PublicKey),
presentation
)
_ <- ZIO.logInfo(s"ClaimsValidationResult: $verifiedClaims")
result: SDJWT.ClaimsValidationResult =
verifiedClaims match {
case validClaims: SDJWT.ValidClaims =>
validClaims.verifyDiscoseClaims(
Json.Obj()
)
case validAnyMatch: SDJWT.ValidAnyMatch.type => validAnyMatch
case invalid: SDJWT.Invalid => invalid
}
} yield result
_ <- ZIO.logInfo(s"ClaimsValidationResult: $ret")
// FIXME REMOVE cleanup
// _ <- ZIO.logInfo(s"ClaimsValidationResult: ${sdJwtPresentationPayload.claimsToDisclose}")
// result: SDJWT.ClaimsValidationResult =
// verifiedClaims match {
// case validClaims: SDJWT.ValidClaims =>
// validClaims.claims // This is all claims
// // TODO
// // .verifyDiscoseClaims(sdJwtPresentationPayload.claimsToDisclose.asObject.getOrElse(Json.Obj()))
// case validAnyMatch: SDJWT.ValidAnyMatch.type => validAnyMatch
// case invalid: SDJWT.Invalid => invalid
// }
} yield ret
mineme0110 marked this conversation as resolved.
Show resolved Hide resolved
verifiedClaims
.mapError(error =>
UnexpectedError(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package org.hyperledger.identus.pollux.core.model.presentation

import org.hyperledger.identus.pollux.sdjwt.PresentationJson
import org.hyperledger.identus.pollux.sdjwt.PresentationCompact
import zio.json.*

case class SdJwtPresentationPayload(
claimsToDisclose: ast.Json.Obj,
presentation: PresentationJson,
options: Option[Options]
)
object SdJwtPresentationPayload {
given JsonDecoder[Options] = DeriveJsonDecoder.gen[Options]
given JsonEncoder[Options] = DeriveJsonEncoder.gen[Options]
given JsonDecoder[SdJwtPresentationPayload] = DeriveJsonDecoder.gen[SdJwtPresentationPayload]
given JsonEncoder[SdJwtPresentationPayload] = DeriveJsonEncoder.gen[SdJwtPresentationPayload]
}
//FIXME
type SdJwtPresentationPayload = PresentationCompact
// case class SdJwtPresentationPayload(
// claimsToDisclose: ast.Json.Obj,
// presentation: PresentationCompact,
// options: Option[Options]
// )
// object SdJwtPresentationPayload {
// given JsonDecoder[Options] = DeriveJsonDecoder.gen[Options]
// given JsonEncoder[Options] = DeriveJsonEncoder.gen[Options]
// given JsonDecoder[SdJwtPresentationPayload] = DeriveJsonDecoder.gen[SdJwtPresentationPayload]
// given JsonEncoder[SdJwtPresentationPayload] = DeriveJsonEncoder.gen[SdJwtPresentationPayload]
// }
Original file line number Diff line number Diff line change
Expand Up @@ -1387,7 +1387,7 @@ private class CredentialServiceImpl(
fromDID = issue.from,
toDID = issue.to,
thid = issue.thid,
credentials = Seq(IssueCredentialIssuedFormat.SDJWT -> credential.value.getBytes)
credentials = Seq(IssueCredentialIssuedFormat.SDJWT -> credential.compact.getBytes)
)
record <- markCredentialGenerated(record, issueCredential)
} yield record
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import org.hyperledger.identus.pollux.core.model.presentation.{SdJwtPresentation
import org.hyperledger.identus.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1
import org.hyperledger.identus.pollux.core.repository.{CredentialRepository, PresentationRepository}
import org.hyperledger.identus.pollux.core.service.serdes.*
import org.hyperledger.identus.pollux.sdjwt.{CredentialJson, PresentationJson, SDJWT}
import org.hyperledger.identus.pollux.sdjwt.{CredentialCompact, PresentationCompact, SDJWT}
import org.hyperledger.identus.pollux.vc.jwt.*
import org.hyperledger.identus.shared.models.WalletAccessContext
import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect
Expand Down Expand Up @@ -144,20 +144,14 @@ private class PresentationServiceImpl(
)
)
)
// return presentationJson
presentationJson <- createSDJwtPresentationPayloadFromCredential(

presentationCompact <- createSDJwtPresentationPayloadFromCredential(
issuedCredentials,
sdJwtClaimsToDisclose,
requestPresentation,
prover
)
presentationPayload <- ZIO.succeed(
SdJwtPresentationPayload(
claimsToDisclose = sdJwtClaimsToDisclose,
presentation = presentationJson,
options = None
)
)
presentationPayload <- ZIO.succeed(presentationCompact)

} yield presentationJson
}
Expand Down Expand Up @@ -514,14 +508,16 @@ private class PresentationServiceImpl(
claimsToDisclose: SdJwtCredentialToDisclose,
requestPresentation: RequestPresentation,
prover: Issuer
): IO[PresentationError, PresentationJson] = {
): IO[PresentationError, PresentationCompact] = {

val verifiableCredentials: Either[
PresentationError.PresentationDecodingError,
Seq[CredentialJson]
Seq[CredentialCompact]
] = issuedCredentials.map { signedCredential =>
decode[org.hyperledger.identus.mercury.model.Base64](signedCredential)
.flatMap(x => Right(CredentialJson(new String(java.util.Base64.getDecoder.decode(x.base64)))))
.flatMap(x =>
Right(CredentialCompact.unsafeFromCompact(new String(java.util.Base64.getDecoder.decode(x.base64))))
)
.left
.map(err => PresentationDecodingError(s"JsonData decoding error: $err"))
}.sequence
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.hyperledger.identus.pollux.sdjwt

import zio.json.*

type Header = String
type Payload = String
type Signature = String
type Disclosure = String
type KBJWT = (String, String, String)

case class CredentialCompact(
override val jwtHeader: Header,
override val jwtPayload: Payload,
override val jwtSignature: Signature,
override val disclosures: Seq[Disclosure],
) extends ModelsExtensionMethods {
override def kbJWT: Option[KBJWT] = None
}

object CredentialCompact {
// given encoder: JsonEncoder[CredentialCompact] = DeriveJsonEncoder.gen[CredentialCompact]
// given decoder: JsonDecoder[CredentialCompact] = DeriveJsonDecoder.gen[CredentialCompact]
given decoder: JsonDecoder[CredentialCompact] = JsonDecoder.string.map(CredentialCompact.unsafeFromCompact(_))
given encoder: JsonEncoder[CredentialCompact] = JsonEncoder.string.contramap[CredentialCompact](_.compact)

// /**
// * // <Issuer-signed JWT>~<Disclosure 1>~<Disclosure 2>~...~<Disclosure N>~<optional KB-JWT>
// *
// * @param value
// * @return
// */
def unsafeFromCompact(str: String): CredentialCompact = {
import scala.util.matching.Regex
// var str = "hhh.ppp.sss~a~b~xxxx"
val patternCompact = new Regex("(^[\\w-]*)\\.([\\w-]*)\\.([\\w-]*)((?:~(?:[\\w-]*))*)~$")
val patternDisclosure = new Regex("(~([\\w]+))")

val patternCompact(h, p, s, disclosuresStr) = str: @unchecked
CredentialCompact(
jwtHeader = h,
jwtPayload = p,
jwtSignature = s,
disclosures = patternDisclosure.findAllIn(disclosuresStr).toSeq.map(_.drop(1)),
)
}
}

case class PresentationCompact(
override val jwtHeader: Header,
override val jwtPayload: Payload,
override val jwtSignature: Signature,
override val disclosures: Seq[Disclosure],
override val kbJWT: Option[KBJWT]
) extends ModelsExtensionMethods

object PresentationCompact {
// given encoder: JsonEncoder[PresentationCompact] = DeriveJsonEncoder.gen[PresentationCompact]
// given decoder: JsonDecoder[PresentationCompact] = DeriveJsonDecoder.gen[PresentationCompact]
given decoder: JsonDecoder[PresentationCompact] = JsonDecoder.string.map(PresentationCompact.unsafeFromCompact(_))
given encoder: JsonEncoder[PresentationCompact] = JsonEncoder.string.contramap[PresentationCompact](_.compact)

// /**
// * // <Issuer-signed JWT>~<Disclosure 1>~<Disclosure 2>~...~<Disclosure N>~<optional KB-JWT>
// *
// * @param value
// * @return
// */
def unsafeFromCompact(str: String): PresentationCompact = {
import scala.util.matching.Regex
// var str = "hhh.ppp.sss~a~b~xxxx"
val patternCompact = new Regex(
"^([\\w-]*)\\.([\\w-]*)\\.([\\w-]*)((?:~(?:[\\w-]*))*)~(?:([\\w-]*)\\.([\\w-]*)\\.([\\w-]*))?$"
)
val patternDisclosure = new Regex("(~([\\w]+))")

val patternCompact(h, p, s, disclosuresStr, kb_h, kb_p, kb_s) = str: @unchecked

val kbJWT: Option[KBJWT] = (
Option(kb_h).filterNot(_.isBlank()),
Option(kb_p).filterNot(_.isBlank()),
Option(kb_s).filterNot(_.isBlank())
) match
case (None, None, None) => None
case (Some(h), Some(p), Some(s)) => Some((h, p, s))
case _ => None // FIXME error

PresentationCompact(
jwtHeader = h,
jwtPayload = p,
jwtSignature = s,
disclosures = patternDisclosure.findAllIn(disclosuresStr).toSeq.map(_.drop(1)),
kbJWT = kbJWT,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,35 +87,3 @@ object HolderPrivateKey {
fromEdPem(data)
}
}

opaque type CredentialJson = String
object CredentialJson {
given decoder: JsonDecoder[CredentialJson] = JsonDecoder.string.map(CredentialJson(_))
given encoder: JsonEncoder[CredentialJson] = JsonEncoder.string.contramap[CredentialJson](_.value)

def apply(value: String): CredentialJson = value
extension (c: CredentialJson)
def value: String = c
def payload: Either[String, String] = ModelsExtensionMethods.payload(c)
def iss: Either[String, String] = ModelsExtensionMethods.iss(c)
def sub: Either[String, String] = ModelsExtensionMethods.sub(c)
def iat: Either[String, BigDecimal] = ModelsExtensionMethods.iat(c)
def exp: Either[String, BigDecimal] = ModelsExtensionMethods.exp(c)

}

opaque type PresentationJson = String
object PresentationJson {
given decoder: JsonDecoder[PresentationJson] = JsonDecoder.string.map(PresentationJson(_))
given encoder: JsonEncoder[PresentationJson] = JsonEncoder.string.contramap[PresentationJson](_.value)

def apply(value: String): PresentationJson = value
extension (c: PresentationJson)
def value: String = c
def payload: Either[String, String] = ModelsExtensionMethods.payload(c)
def iss: Either[String, String] = ModelsExtensionMethods.iss(c)
def sub: Either[String, String] = ModelsExtensionMethods.sub(c)
def iat: Either[String, BigDecimal] = ModelsExtensionMethods.iat(c)
def exp: Either[String, BigDecimal] = ModelsExtensionMethods.exp(c)

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,58 @@ import zio.json.*

import java.util.Base64

private[sdjwt] object ModelsExtensionMethods {
extension (c: String) {
private def asJsonObject: Either[String, ast.Json.Obj] = c
private[sdjwt] trait ModelsExtensionMethods {

def jwtHeader: Header
def jwtPayload: Payload
def jwtSignature: Signature
def disclosures: Seq[Disclosure]
def kbJWT: Option[KBJWT]

def compact: String =
jwtHeader + "." + jwtPayload + "." + jwtSignature +
disclosures.map("~" + _).mkString("") + // note the case where disclosures is empty
"~" + kbJWT.map(e => e._1 + "." + e._2 + "." + e._3).getOrElse("")

def payloadAsJsonObj: Either[String, zio.json.ast.Json.Obj] =
new String(java.util.Base64.getUrlDecoder.decode(jwtPayload))
.fromJson[ast.Json]
.map(_.asObject)
.flatMap {
case None => Left("PresentationJson must the a Json Object")
case None => Left("The payload in PresentationCompact must the a Json Object")
case Some(jsonObj) => Right(jsonObj)
}
def payload: Either[String, String] =
asJsonObject.flatMap {
_.get("payload") match
case None => Left("PresentationJson must have the field 'payload'")
case Some(ast.Json.Str(payload)) =>
Right(
String(
Base64.getDecoder().decode(payload) // TODO make it safe
)
)
case Some(_) => Left("PresentationJson must have the field 'payload' as a Base64 String")
}
private def payloadAsJsonObj: Either[String, zio.json.ast.Json.Obj] =
payload.flatMap {
_.fromJson[ast.Json]
.map(_.asObject)
.flatMap {
case None => Left("The payload in PresentationJson must the a Json Object")
case Some(jsonObj) => Right(jsonObj)
}
}

def iss: Either[String, String] =
payloadAsJsonObj.flatMap {
_.get("iss") match
case None => Left("The payload in PresentationJson must have the field 'iss'")
case Some(ast.Json.Str(iss)) => Right(iss)
case Some(_) => Left("PresentationJson must have the field 'iss' as a String")
}
def iss: Either[String, String] =
payloadAsJsonObj.flatMap {
_.get("iss") match
case None => Left("The payload in PresentationCompact must have the field 'iss'")
case Some(ast.Json.Str(iss)) => Right(iss)
case Some(_) => Left("PresentationCompact must have the field 'iss' as a String")
}

def sub: Either[String, String] =
payloadAsJsonObj.flatMap {
_.get("sub") match
case None => Left("The payload in PresentationJson must have the field 'sub'")
case Some(ast.Json.Str(sub)) => Right(sub)
case Some(_) => Left("PresentationJson must have the field 'sub' as a String")
}
def sub: Either[String, String] =
payloadAsJsonObj.flatMap {
_.get("sub") match
case None => Left("The payload in PresentationCompact must have the field 'sub'")
case Some(ast.Json.Str(sub)) => Right(sub)
case Some(_) => Left("PresentationCompact must have the field 'sub' as a String")
}

def iat: Either[String, BigDecimal] =
payloadAsJsonObj.flatMap {
_.get("iat") match
case None => Left("The payload in PresentationJson must have the field 'iat'")
case Some(ast.Json.Num(iat)) => Right(iat)
case Some(_) => Left("PresentationJson must have the field 'iat' as a Num")
}
def iat: Either[String, BigDecimal] =
payloadAsJsonObj.flatMap {
_.get("iat") match
case None => Left("The payload in PresentationCompact must have the field 'iat'")
case Some(ast.Json.Num(iat)) => Right(iat)
case Some(_) => Left("PresentationCompact must have the field 'iat' as a Num")
}

def exp: Either[String, BigDecimal] =
payloadAsJsonObj.flatMap {
_.get("exp") match
case None => Left("The payload in PresentationCompact must have the field 'exp'")
case Some(ast.Json.Num(exp)) => Right(exp)
case Some(_) => Left("PresentationCompact must have the field 'exp' as a Num")
}

def exp: Either[String, BigDecimal] =
payloadAsJsonObj.flatMap {
_.get("exp") match
case None => Left("The payload in PresentationJson must have the field 'exp'")
case Some(ast.Json.Num(exp)) => Right(exp)
case Some(_) => Left("PresentationJson must have the field 'exp' as a Num")
}
}
}
Loading
Loading