Skip to content

Commit

Permalink
feat: use the compact format in SD-JWT
Browse files Browse the repository at this point in the history
  • Loading branch information
FabioPinheiro committed Jun 10, 2024
1 parent d6dfb72 commit 5136dd7
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -840,25 +840,25 @@ object PresentBackgroundJobs extends BackgroundJobsHelper {
val base64Decoded = new String(java.util.Base64.getDecoder.decode(data))
val verifiedClaims = for {
sdJwtPresentationPayload <- ZIO.fromEither(base64Decoded.fromJson[SdJwtPresentationPayload])
iss <- ZIO.fromEither(sdJwtPresentationPayload.presentation.iss)
iss <- ZIO.fromEither(sdJwtPresentationPayload.iss)
ed25519PublicKey <- resolveToEd25519PublicKey(iss)
verifiedClaims = SDJWT.getVerifiedClaims(
ret = SDJWT.getVerifiedClaims(
IssuerPublicKey(ed25519PublicKey),
sdJwtPresentationPayload.presentation,
sdJwtPresentationPayload.claimsToDisclose.toJson
sdJwtPresentationPayload,
)
_ <- ZIO.logInfo(s"ClaimsValidationResult: $verifiedClaims")
_ <- ZIO.logInfo(s"ClaimsValidationResult: ${sdJwtPresentationPayload.claimsToDisclose}")
result: SDJWT.ClaimsValidationResult =
verifiedClaims match {
case validClaims: SDJWT.ValidClaims =>
validClaims.verifyDiscoseClaims(
sdJwtPresentationPayload.claimsToDisclose.asObject.getOrElse(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
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 @@ -1381,7 +1381,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 @@ -145,19 +145,13 @@ private class PresentationServiceImpl(
)
)

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

} yield presentationPayload
}
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

0 comments on commit 5136dd7

Please sign in to comment.