From 93c8d1e80b37066c9212f3e39455345beda27a5a Mon Sep 17 00:00:00 2001 From: Bassam Riman Date: Wed, 10 Apr 2024 01:40:16 -0400 Subject: [PATCH] ATL-6787-6779: Algorithm Issuer Verification Signed-off-by: Bassam Riman --- .../VcVerificationServiceImpl.scala | 36 +++++++- .../atala/pollux/vc/jwt/JWTVerification.scala | 87 +++++++++++++++---- .../vc/jwt/VerifiableCredentialPayload.scala | 8 ++ 3 files changed, 109 insertions(+), 22 deletions(-) diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceImpl.scala index 1b3437b140..0614701c22 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceImpl.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceImpl.scala @@ -2,7 +2,7 @@ package io.iohk.atala.pollux.core.service.verification import io.iohk.atala.pollux.core.model.schema.CredentialSchema import io.iohk.atala.pollux.core.service.URIDereferencer -import io.iohk.atala.pollux.vc.jwt.{DidResolver, JWT, JwtCredential} +import io.iohk.atala.pollux.vc.jwt.{DidResolver, JWT, JWTVerification, JwtCredential} import zio.{IO, *} class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDereferencer) @@ -48,6 +48,8 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe case VcVerification.SignatureVerification => verifySignature(credential) case VcVerification.ExpirationCheck => verifyExpiration(credential) case VcVerification.NotBeforeCheck => verifyNotBefore(credential) + case VcVerification.AlgorithmVerification => verifyAlgorithm(credential) + case VcVerification.IssuerIdentification => verifyIssuerIdentification(credential) case _ => ZIO.fail(VcVerificationServiceError.UnexpectedError(s"Unsupported Verification $verification")) } } @@ -97,7 +99,7 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe .mapError(error => VcVerificationServiceError.UnexpectedError(error)) .map(validation => VcVerificationOutcome( - verification = VcVerification.SchemaCheck, + verification = VcVerification.SignatureVerification, success = validation .map(_ => true) .getOrElse(false) @@ -108,7 +110,7 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe private def verifyExpiration(credential: String): IO[VcVerificationServiceError, VcVerificationOutcome] = { ZIO.succeed( VcVerificationOutcome( - verification = VcVerification.SchemaCheck, + verification = VcVerification.ExpirationCheck, success = JwtCredential .validateExpiration(JWT(credential)) .map(_ => true) @@ -120,7 +122,7 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe private def verifyNotBefore(credential: String): IO[VcVerificationServiceError, VcVerificationOutcome] = { ZIO.succeed( VcVerificationOutcome( - verification = VcVerification.SchemaCheck, + verification = VcVerification.NotBeforeCheck, success = JwtCredential .validateNotBefore(JWT(credential)) .map(_ => true) @@ -128,6 +130,32 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe ) ) } + + private def verifyAlgorithm(credential: String): IO[VcVerificationServiceError, VcVerificationOutcome] = { + ZIO.succeed( + VcVerificationOutcome( + verification = VcVerification.AlgorithmVerification, + success = JWTVerification + .validateAlgorithm(JWT(credential)) + .map(_ => true) + .getOrElse(false) + ) + ) + } + + private def verifyIssuerIdentification(credential: String): IO[VcVerificationServiceError, VcVerificationOutcome] = { + JwtCredential + .validateIssuerJWT(JWT(credential))(didResolver) + .mapError(error => VcVerificationServiceError.UnexpectedError(error)) + .map(validation => + VcVerificationOutcome( + verification = VcVerification.IssuerIdentification, + success = validation + .map(_ => true) + .getOrElse(false) + ) + ) + } } object VcVerificationServiceImpl { diff --git a/pollux/lib/vc-jwt/src/main/scala/io/iohk/atala/pollux/vc/jwt/JWTVerification.scala b/pollux/lib/vc-jwt/src/main/scala/io/iohk/atala/pollux/vc/jwt/JWTVerification.scala index 873be46f23..7b774dbefe 100644 --- a/pollux/lib/vc-jwt/src/main/scala/io/iohk/atala/pollux/vc/jwt/JWTVerification.scala +++ b/pollux/lib/vc-jwt/src/main/scala/io/iohk/atala/pollux/vc/jwt/JWTVerification.scala @@ -1,19 +1,20 @@ package io.iohk.atala.pollux.vc.jwt + +import com.nimbusds.jose.JWSVerifier import com.nimbusds.jose.crypto.ECDSAVerifier +import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton import com.nimbusds.jose.jwk.* import com.nimbusds.jose.util.Base64URL -import com.nimbusds.jose.JWSVerifier -import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton import com.nimbusds.jwt.SignedJWT import io.circe import io.circe.generic.auto.* import io.iohk.atala.castor.core.model.did.VerificationRelationship import pdi.jwt.* -import zio.prelude.* import zio.* +import zio.prelude.* -import java.security.interfaces.ECPublicKey import java.security.PublicKey +import java.security.interfaces.ECPublicKey import scala.util.{Failure, Success, Try} object JWTVerification { @@ -24,25 +25,49 @@ object JWTVerification { "ES256" -> Set("ES256") // TODO: Only use valid type (added just for compatibility in the Demo code) ) - def validateEncodedJwt[T](jwt: JWT, proofPurpose: Option[VerificationRelationship] = None)( - didResolver: DidResolver - )(decoder: String => Validation[String, T])(issuerDidExtractor: T => String): IO[String, Validation[String, Unit]] = { - val decodeJWT = Validation - .fromTry(JwtCirce.decodeRawAll(jwt.value, JwtOptions(false, false, false))) - .mapError(_.getMessage) + def validateAlgorithm(jwt: JWT): Validation[String, Unit] = { + val decodedJWT = + Validation + .fromTry(JwtCirce.decodeRawAll(jwt.value, JwtOptions(false, false, false))) + .mapError(_.getMessage) + for { + decodedJwtTask <- decodedJWT + (header, _, _) = decodedJwtTask + algorithm <- Validation + .fromOptionWith("An algorithm must be specified in the header")(JwtCirce.parseHeader(header).algorithm) + result <- + Validation + .fromPredicateWith("No PublicKey to validate against found")( + SUPPORT_PUBLIC_KEY_TYPES.getOrElse(algorithm.name, Set.empty) + )(_.nonEmpty) + .flatMap(_ => Validation.unit) - val extractAlgorithm: Validation[String, JwtAlgorithm] = + } yield result + } + + def validateIssuer[T](jwt: JWT)(didResolver: DidResolver)( + decoder: String => Validation[String, T] + )(issuerDidExtractor: T => String): IO[String, Validation[String, DIDDocument]] = { + val decodedJWT = + Validation + .fromTry(JwtCirce.decodeRawAll(jwt.value, JwtOptions(false, false, false))) + .mapError(_.getMessage) + + val claim: Validation[String, String] = for { - decodedJwtTask <- decodeJWT - (header, _, _) = decodedJwtTask - algorithm <- Validation - .fromOptionWith("An algorithm must be specified in the header")(JwtCirce.parseHeader(header).algorithm) - } yield algorithm + decodedJwtTask <- decodedJWT + (_, claim, _) = decodedJwtTask + } yield claim + validateIssuerFromClaim(claim)(didResolver)(decoder)(issuerDidExtractor) + } + + def validateIssuerFromClaim[T](validatedClaim: Validation[String, String])(didResolver: DidResolver)( + decoder: String => Validation[String, T] + )(issuerDidExtractor: T => String): IO[String, Validation[String, DIDDocument]] = { val validatedIssuerDid: Validation[String, String] = for { - decodedJwtTask <- decodeJWT - (_, claim, _) = decodedJwtTask + claim <- validatedClaim decodedClaim <- decoder(claim) extractIssuerDid = issuerDidExtractor(decodedClaim) } yield extractIssuerDid @@ -55,6 +80,32 @@ object JWTVerification { )(identity) .map(b => b.flatten) + loadDidDocument + } + + def validateEncodedJwt[T](jwt: JWT, proofPurpose: Option[VerificationRelationship] = None)( + didResolver: DidResolver + )(decoder: String => Validation[String, T])(issuerDidExtractor: T => String): IO[String, Validation[String, Unit]] = { + val decodedJWT = Validation + .fromTry(JwtCirce.decodeRawAll(jwt.value, JwtOptions(false, false, false))) + .mapError(_.getMessage) + + val extractAlgorithm: Validation[String, JwtAlgorithm] = + for { + decodedJwtTask <- decodedJWT + (header, _, _) = decodedJwtTask + algorithm <- Validation + .fromOptionWith("An algorithm must be specified in the header")(JwtCirce.parseHeader(header).algorithm) + } yield algorithm + + val claim: Validation[String, String] = + for { + decodedJwtTask <- decodedJWT + (_, claim, _) = decodedJwtTask + } yield claim + + val loadDidDocument = validateIssuerFromClaim(claim)(didResolver)(decoder)(issuerDidExtractor) + loadDidDocument .map(validatedDidDocument => { for { diff --git a/pollux/lib/vc-jwt/src/main/scala/io/iohk/atala/pollux/vc/jwt/VerifiableCredentialPayload.scala b/pollux/lib/vc-jwt/src/main/scala/io/iohk/atala/pollux/vc/jwt/VerifiableCredentialPayload.scala index 451dd61709..fc2530f0e3 100644 --- a/pollux/lib/vc-jwt/src/main/scala/io/iohk/atala/pollux/vc/jwt/VerifiableCredentialPayload.scala +++ b/pollux/lib/vc-jwt/src/main/scala/io/iohk/atala/pollux/vc/jwt/VerifiableCredentialPayload.scala @@ -742,6 +742,14 @@ object JwtCredential { )(_.iss) } + def validateIssuerJWT( + jwt: JWT, + )(didResolver: DidResolver): IO[String, Validation[String, DIDDocument]] = { + JWTVerification.validateIssuer(jwt)(didResolver: DidResolver)(claim => + Validation.fromEither(decode[JwtCredentialPayload](claim).left.map(_.toString)) + )(_.iss) + } + def validateJwtSchema( jwt: JWT )(schemaResolver: SchemaResolver)(