Skip to content

Commit

Permalink
feat: VC support for Array of credential Status (hyperledger#1383)
Browse files Browse the repository at this point in the history
Signed-off-by: Bassam Riman <[email protected]>
  • Loading branch information
CryptoKnightIOG authored Sep 30, 2024
1 parent d81b4f0 commit ad946cf
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ sealed trait CredentialPayload {

def issuer: String | CredentialIssuer

def maybeCredentialStatus: Option[CredentialStatus]
def maybeCredentialStatus: Option[CredentialStatus | List[CredentialStatus]]

def maybeRefreshService: Option[RefreshService]

Expand Down Expand Up @@ -145,7 +145,7 @@ case class JwtVc(
maybeValidFrom: Option[Instant],
maybeValidUntil: Option[Instant],
maybeIssuer: Option[String | CredentialIssuer],
maybeCredentialStatus: Option[CredentialStatus],
maybeCredentialStatus: Option[CredentialStatus | List[CredentialStatus]],
maybeRefreshService: Option[RefreshService],
maybeEvidence: Option[Json],
maybeTermsOfUse: Option[Json]
Expand Down Expand Up @@ -182,7 +182,7 @@ case class W3cCredentialPayload(
maybeExpirationDate: Option[Instant],
override val maybeCredentialSchema: Option[CredentialSchema | List[CredentialSchema]],
override val credentialSubject: Json,
override val maybeCredentialStatus: Option[CredentialStatus],
override val maybeCredentialStatus: Option[CredentialStatus | List[CredentialStatus]],
override val maybeRefreshService: Option[RefreshService],
override val maybeEvidence: Option[Json],
override val maybeTermsOfUse: Option[Json],
Expand Down Expand Up @@ -239,6 +239,11 @@ object CredentialPayload {
("statusListCredential", credentialStatus.statusListCredential.asJson)
)

implicit val credentialStatusOrListEncoder: Encoder[CredentialStatus | List[CredentialStatus]] = Encoder.instance {
case status: CredentialStatus => Encoder[CredentialStatus].apply(status)
case statusList: List[CredentialStatus] => Encoder[List[CredentialStatus]].apply(statusList)
}

implicit val stringOrCredentialIssuerEncoder: Encoder[String | CredentialIssuer] = Encoder.instance {
case string: String => Encoder[String].apply(string)
case credentialIssuer: CredentialIssuer => Encoder[CredentialIssuer].apply(credentialIssuer)
Expand Down Expand Up @@ -383,6 +388,11 @@ object CredentialPayload {
.map(schema => schema: CredentialSchema | List[CredentialSchema])
.or(Decoder[List[CredentialSchema]].map(schema => schema: CredentialSchema | List[CredentialSchema]))

implicit val credentialStatusOrListDecoder: Decoder[CredentialStatus | List[CredentialStatus]] =
Decoder[CredentialStatus]
.map(status => status: CredentialStatus | List[CredentialStatus])
.or(Decoder[List[CredentialStatus]].map(status => status: CredentialStatus | List[CredentialStatus]))

implicit val w3cCredentialPayloadDecoder: Decoder[W3cCredentialPayload] =
(c: HCursor) =>
for {
Expand All @@ -404,7 +414,7 @@ object CredentialPayload {
.downField("credentialSchema")
.as[Option[CredentialSchema | List[CredentialSchema]]]
credentialSubject <- c.downField("credentialSubject").as[Json]
maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus]]
maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus | List[CredentialStatus]]]
maybeRefreshService <- c.downField("refreshService").as[Option[RefreshService]]
maybeEvidence <- c.downField("evidence").as[Option[Json]]
maybeTermsOfUse <- c.downField("termsOfUse").as[Option[Json]]
Expand Down Expand Up @@ -443,7 +453,7 @@ object CredentialPayload {
.downField("credentialSchema")
.as[Option[CredentialSchema | List[CredentialSchema]]]
credentialSubject <- c.downField("credentialSubject").as[Json]
maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus]]
maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus | List[CredentialStatus]]]
maybeRefreshService <- c.downField("refreshService").as[Option[RefreshService]]
maybeEvidence <- c.downField("evidence").as[Option[Json]]
maybeTermsOfUse <- c.downField("termsOfUse").as[Option[Json]]
Expand Down Expand Up @@ -837,7 +847,7 @@ object JwtCredential {
} yield Validation.validateWith(signatureValidation, dateVerification, revocationVerification)((a, _, _) => a)
}

private def verifyRevocationStatusJwt(jwt: JWT)(uriResolver: UriResolver): IO[String, Validation[String, Unit]] = {
def verifyRevocationStatusJwt(jwt: JWT)(uriResolver: UriResolver): IO[String, Validation[String, Unit]] = {
val decodeJWT =
ZIO
.fromTry(JwtCirce.decodeRaw(jwt.value, options = JwtOptions(false, false, false)))
Expand All @@ -847,12 +857,19 @@ object JwtCredential {
decodedJWT <- decodeJWT
jwtCredentialPayload <- ZIO.fromEither(decode[JwtCredentialPayload](decodedJWT)).mapError(_.getMessage)
credentialStatus = jwtCredentialPayload.vc.maybeCredentialStatus
result = credentialStatus.fold(ZIO.succeed(Validation.unit))(status =>
CredentialVerification.verifyCredentialStatus(status)(uriResolver)
.map {
{
case status: CredentialStatus => List(status)
case statusList: List[CredentialStatus] => statusList
}
}
.getOrElse(List.empty)
results <- ZIO.collectAll(
credentialStatus.map(status => CredentialVerification.verifyCredentialStatus(status)(uriResolver))
)
result = Validation.validateAll(results).flatMap(_ => Validation.unit)
} yield result

res.flatten
res
}
}

Expand Down Expand Up @@ -927,11 +944,20 @@ object W3CCredential {
private def verifyRevocationStatusW3c(
w3cPayload: W3cVerifiableCredentialPayload,
)(uriResolver: UriResolver): IO[String, Validation[String, Unit]] = {
// If credential does not have credential status list, it does not support revocation
// and we assume revocation status is valid.
w3cPayload.payload.maybeCredentialStatus.fold(ZIO.succeed(Validation.unit))(status =>
CredentialVerification.verifyCredentialStatus(status)(uriResolver)
)
val credentialStatus = w3cPayload.payload.maybeCredentialStatus
.map {
{
case status: CredentialStatus => List(status)
case statusList: List[CredentialStatus] => statusList
}
}
.getOrElse(List.empty)
for {
results <- ZIO.collectAll(
credentialStatus.map(status => CredentialVerification.verifyCredentialStatus(status)(uriResolver))
)
result = Validation.validateAll(results).flatMap(_ => Validation.unit)
} yield result
}

def verify(w3cPayload: W3cVerifiableCredentialPayload, options: CredentialVerification.CredentialVerificationOptions)(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.circe.*
import io.circe.syntax.*
import org.hyperledger.identus.castor.core.model.did.{DID, VerificationRelationship}
import org.hyperledger.identus.pollux.vc.jwt.CredentialPayload.Implicits.*
import org.hyperledger.identus.pollux.vc.jwt.StatusPurpose.Revocation
import org.hyperledger.identus.shared.http.*
import zio.*
import zio.prelude.Validation
Expand Down Expand Up @@ -62,7 +63,11 @@ object JWTVerificationTest extends ZIOSpecDefault {
|}
|""".stripMargin

private def createJwtCredential(issuer: IssuerWithKey, issuerAsObject: Boolean = false): JWT = {
private def createJwtCredential(
issuer: IssuerWithKey,
issuerAsObject: Boolean = false,
credentialStatus: Option[CredentialStatus | List[CredentialStatus]] = None
): JWT = {
val validFrom = Instant.parse("2010-01-05T00:00:00Z") // ISSUANCE DATE
val jwtCredentialNbf = Instant.parse("2010-01-01T00:00:00Z") // ISSUANCE DATE
val validUntil = Instant.parse("2010-01-09T00:00:00Z") // EXPIRATION DATE
Expand All @@ -75,7 +80,7 @@ object JWTVerificationTest extends ZIOSpecDefault {
`type` = Set("VerifiableCredential", "UniversityDegreeCredential"),
maybeCredentialSchema = None,
credentialSubject = Json.obj("id" -> Json.fromString("1")),
maybeCredentialStatus = None,
maybeCredentialStatus = credentialStatus,
maybeRefreshService = None,
maybeEvidence = None,
maybeTermsOfUse = None,
Expand Down Expand Up @@ -190,6 +195,51 @@ object JWTVerificationTest extends ZIOSpecDefault {
)
)
},
test("fail verification if proof is valid but credential is revoked at the give status list index given list") {
val revokedStatus: List[CredentialStatus] = List(
org.hyperledger.identus.pollux.vc.jwt.CredentialStatus(
id = "http://localhost:8085/credential-status/664382dc-9e6d-4d0c-99d1-85e2c74eb5e9#1",
statusPurpose = StatusPurpose.Revocation,
`type` = "StatusList2021Entry",
statusListCredential = "http://localhost:8085/credential-status/664382dc-9e6d-4d0c-99d1-85e2c74eb5e9",
statusListIndex = 1
),
org.hyperledger.identus.pollux.vc.jwt.CredentialStatus(
id = "http://localhost:8085/credential-status/664382dc-9e6d-4d0c-99d1-85e2c74eb5e9#2",
statusPurpose = StatusPurpose.Suspension,
`type` = "StatusList2021Entry",
statusListCredential = "http://localhost:8085/credential-status/664382dc-9e6d-4d0c-99d1-85e2c74eb5e9",
statusListIndex = 1
)
)

val urlResolver = new UriResolver {
override def resolve(uri: String): IO[GenericUriResolverError, String] = {
ZIO.succeed(statusListCredentialString)
}
}

val genericUriResolver = GenericUriResolver(
Map(
"data" -> DataUrlResolver(),
"http" -> urlResolver,
"https" -> urlResolver
)
)
val issuer = createUser("did:prism:issuer")
val jwtCredential = createJwtCredential(issuer, credentialStatus = Some(revokedStatus))

for {
validation <- JwtCredential.verifyRevocationStatusJwt(jwtCredential)(genericUriResolver)
} yield assertTrue(
validation.fold(
chunk =>
chunk.length == 2 && chunk.head.contentEquals("Credential is revoked") && chunk.tail.head
.contentEquals("Credential is revoked"),
_ => false
)
)
},
test("validate dates happy path") {
val issuer = createUser("did:prism:issuer")
val jwtCredential = createJwtCredential(issuer)
Expand Down Expand Up @@ -223,6 +273,29 @@ object JWTVerificationTest extends ZIOSpecDefault {
jwtWithObjectIssuerIssuer.equals(jwtIssuer)
)
},
test("validate credential status list") {
val issuer = createUser("did:prism:issuer")
val status = CredentialStatus(id = "id", `type` = "type", statusPurpose = Revocation, 1, "1")
val encodedJwtWithStatusList = createJwtCredential(
issuer,
false,
Some(List(status))
)
val econdedJwtWithStatusObject = createJwtCredential(issuer, true, Some(status))
for {
decodeJwtWithStatusList <- JwtCredential
.decodeJwt(encodedJwtWithStatusList)
decodeJwtWithStatusObject <- JwtCredential
.decodeJwt(econdedJwtWithStatusObject)
statusFromList = decodeJwtWithStatusList.vc.maybeCredentialStatus.map {
case list: List[CredentialStatus] => list.head
case _: CredentialStatus => throw new IllegalStateException("List expected")
}.get
statusFromObjet = decodeJwtWithStatusObject.vc.maybeCredentialStatus.get
} yield assertTrue(
statusFromList.equals(statusFromObjet)
)
},
test("validate dates should fail given after valid until") {
val issuer = createUser("did:prism:issuer")
val jwtCredential = createJwtCredential(issuer)
Expand Down

0 comments on commit ad946cf

Please sign in to comment.