From e840e091ed8bbdd283a013156a6ea64bf2fbfc1b Mon Sep 17 00:00:00 2001 From: Bassam Riman Date: Tue, 27 Aug 2024 11:32:07 -0400 Subject: [PATCH] ATL-7659: API Supports ValidFrom And ValidTo Signed-off-by: Bassam Riman --- .../vc/jwt/VerifiableCredentialPayload.scala | 165 +++++++++--------- 1 file changed, 81 insertions(+), 84 deletions(-) diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala index 2c1f39d8c9..9a670fbb6a 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala @@ -33,8 +33,6 @@ object DID { case class Issuer(did: DID, signer: Signer, publicKey: PublicKey) -case class ApolloIssuer(did: DID, signer: Signer, publicKey: ApolloPublicKey) - sealed trait VerifiableCredentialPayload case class W3cVerifiableCredentialPayload(payload: W3cCredentialPayload, proof: JwtProof) @@ -43,8 +41,6 @@ case class W3cVerifiableCredentialPayload(payload: W3cCredentialPayload, proof: case class JwtVerifiableCredentialPayload(jwt: JWT) extends VerifiableCredentialPayload -case class AnoncredVerifiableCredentialPayload(json: String) extends VerifiableCredentialPayload //FIXME json type - enum StatusPurpose(val str: String) { case Revocation extends StatusPurpose("Revocation") case Suspension extends StatusPurpose("Suspension") @@ -143,42 +139,6 @@ sealed trait CredentialPayload { ) } -object CredentialPayloadValidation { - - val DEFAULT_VC_TYPE = "VerifiableCredential" - val DEFAULT_CONTEXT = "https://www.w3.org/2018/credentials/v1" - - def validateIssuerDid(maybeIssuerDid: Option[String]): Validation[String, String] = { - Validation - .fromOptionWith("Issuer Did is empty")(maybeIssuerDid) - } - - def validateIssuanceDate(maybeIssuanceDate: Option[Instant]): Validation[String, Instant] = { - Validation - .fromOptionWith("Issuance Date is empty")(maybeIssuanceDate) - } - - def validateContext(`@context`: Set[String]): Validation[String, NonEmptySet[String]] = { - Validation - .fromOptionWith("@context is empty")(`@context`.toNonEmptySet) - .flatMap(`nonEmpty@context` => - Validation.fromPredicateWith("Missing default context from @context")(`nonEmpty@context`)( - _.contains(DEFAULT_CONTEXT) - ) - ) - } - - def validateVcType(`type`: Set[String]): Validation[String, NonEmptySet[String]] = { - Validation - .fromOptionWith("Type is empty")(`type`.toNonEmptySet) - .flatMap(nonEmptyType => - Validation.fromPredicateWith("Missing default type from `type`")(nonEmptyType)( - _.contains(DEFAULT_VC_TYPE) - ) - ) - } -} - case class JwtVc( `@context`: Set[String], `type`: Set[String], @@ -285,6 +245,8 @@ object CredentialPayload { ("issuer", w3cCredentialPayload.issuer.asJson), ("issuanceDate", w3cCredentialPayload.issuanceDate.asJson), ("expirationDate", w3cCredentialPayload.maybeExpirationDate.asJson), + ("validFrom", w3cCredentialPayload.maybeValidFrom.asJson), + ("validUntil", w3cCredentialPayload.maybeValidUntil.asJson), ("credentialSchema", w3cCredentialPayload.maybeCredentialSchema.asJson), ("credentialSubject", w3cCredentialPayload.credentialSubject), ("credentialStatus", w3cCredentialPayload.maybeCredentialStatus.asJson), @@ -519,52 +481,67 @@ object CredentialVerification { import CredentialPayload.Implicits.* - def validateCredential( - verifiableCredentialPayload: VerifiableCredentialPayload - )(didResolver: DidResolver): IO[String, Validation[String, Unit]] = { - verifiableCredentialPayload match { - case (w3cVerifiableCredentialPayload: W3cVerifiableCredentialPayload) => - W3CCredential.validateW3C(w3cVerifiableCredentialPayload)(didResolver) - case (jwtVerifiableCredentialPayload: JwtVerifiableCredentialPayload) => - JwtCredential.validateEncodedJWT(jwtVerifiableCredentialPayload.jwt)(didResolver) - } + def validateValidFromNotAfterValidUntil( + maybeValidFrom: Option[Instant], + maybeValidUntil: Option[Instant], + validFromName: String, + validUntilName: String + ): Validation[String, Unit] = { + (maybeValidFrom, maybeValidUntil) + .mapN((validFrom, validUntil) => + if (validFrom.isAfter(validUntil)) + Validation.fail( + s"Credential cannot expire before being in effect. $validFromName=$validFrom $validUntilName=$validUntil" + ) + else Validation.unit + ) + .getOrElse(Validation.unit) } - def verifyDates(issuanceDate: Instant, maybeExpirationDate: Option[Instant], leeway: TemporalAmount)(implicit - clock: Clock + private def validateValidFrom( + maybeValidFrom: Option[Instant], + now: Instant, + leeway: TemporalAmount, + validFromName: String, ): Validation[String, Unit] = { - val now = clock.instant() - - def validateNbfNotAfterExp(nbf: Instant, maybeExp: Option[Instant]): Validation[String, Unit] = { - maybeExp - .map(exp => - if (nbf.isAfter(exp)) - Validation.fail(s"Credential cannot expire before being in effect. nbf=$nbf exp=$exp") - else Validation.unit - ) - .getOrElse(Validation.unit) - } - - def validateNbf(nbf: Instant): Validation[String, Unit] = { - if (now.isBefore(nbf.minus(leeway))) - Validation.fail(s"Credential is not yet in effect. now=$now nbf=$nbf leeway=$leeway") - else Validation.unit - } + maybeValidFrom + .map(validFrom => + if (now.isBefore(validFrom.minus(leeway))) + Validation.fail(s"Credential is not yet in effect. now=$now $validFromName=$validFrom leeway=$leeway") + else Validation.unit + ) + .getOrElse(Validation.unit) + } - def validateExp(maybeExp: Option[Instant]): Validation[String, Unit] = { - maybeExp - .map(exp => - if (now.isAfter(exp.plus(leeway))) - Validation.fail(s"Credential has expired. now=$now exp=$exp leeway=$leeway") - else Validation.unit - ) - .getOrElse(Validation.unit) - } + private def validateValidUntil( + maybeValidUntil: Option[Instant], + now: Instant, + leeway: TemporalAmount, + validUntilName: String, + ): Validation[String, Unit] = { + maybeValidUntil + .map(validUntil => + if (now.isAfter(validUntil.plus(leeway))) + Validation.fail(s"Credential has expired. now=$now $validUntilName=$validUntil leeway=$leeway") + else Validation.unit + ) + .getOrElse(Validation.unit) + } + def verifyDates( + maybeValidFrom: Option[Instant], + maybeValidUntil: Option[Instant], + leeway: TemporalAmount, + validFromName: String, + validUntilName: String + )(implicit + clock: Clock + ): Validation[String, Unit] = { + val now = clock.instant() Validation.validateWith( - validateNbfNotAfterExp(issuanceDate, maybeExpirationDate), - validateNbf(issuanceDate), - validateExp(maybeExpirationDate) + validateValidFromNotAfterValidUntil(maybeValidFrom, maybeValidUntil, validFromName, validUntilName), + validateValidFrom(maybeValidFrom, now, leeway, validFromName), + validateValidUntil(maybeValidUntil, now, leeway, validUntilName) )((l, _, _) => l) } @@ -791,7 +768,12 @@ object JwtCredential { jwtCredentialPayload <- Validation.fromEither(decode[JwtCredentialPayload](decodedJWT)).mapError(_.getMessage) nbf = jwtCredentialPayload.nbf maybeExp = jwtCredentialPayload.maybeExp - result <- CredentialVerification.verifyDates(nbf, maybeExp, leeway)(clock) + maybeValidFrom = jwtCredentialPayload.vc.maybeValidFrom + maybeValidUntil = jwtCredentialPayload.vc.maybeValidUntil + result <- Validation.validateWith( + CredentialVerification.verifyDates(maybeValidFrom, maybeValidUntil, leeway, "validFrom", "validUntil")(clock), + CredentialVerification.verifyDates(Some(nbf), maybeExp, leeway, "nbf", "exp")(clock) + )((l, _) => l) } yield result } @@ -878,9 +860,24 @@ object W3CCredential { def verifyDates(w3cPayload: W3cVerifiableCredentialPayload, leeway: TemporalAmount)(implicit clock: Clock ): Validation[String, Unit] = { - CredentialVerification.verifyDates(w3cPayload.payload.issuanceDate, w3cPayload.payload.maybeExpirationDate, leeway)( - clock - ) + Validation.validateWith( + CredentialVerification.verifyDates( + w3cPayload.payload.maybeValidFrom, + w3cPayload.payload.maybeValidUntil, + leeway, + "validFrom", + "validUntil" + )(clock), + CredentialVerification.verifyDates( + Some(w3cPayload.payload.issuanceDate), + w3cPayload.payload.maybeExpirationDate, + leeway, + "issuanceDate", + "expirationDate" + )( + clock + ) + )((l, _) => l) } private def verifyRevocationStatusW3c(