From f0a1f2c1aaafb636cabcb84c599d9deaa90fd373 Mon Sep 17 00:00:00 2001 From: Bassam Date: Tue, 23 Apr 2024 23:59:28 -0400 Subject: [PATCH] feat: Vc Verification Api (#975) Signed-off-by: Bassam Riman Signed-off-by: Benjamin Voiturier Co-authored-by: Benjamin Voiturier --- .../core/model/schema/CredentialSchema.scala | 19 +- .../core/service/CredentialServiceImpl.scala | 2 +- .../service/verification/VcVerification.scala | 31 + .../verification/VcVerificationService.scala | 18 + .../VcVerificationServiceError.scala | 9 + .../VcVerificationServiceImpl.scala | 260 ++++++++ .../VcVerificationServiceImplSpec.scala | 604 ++++++++++++++++++ .../VcVerificationServiceSpecHelper.scala | 50 ++ .../atala/pollux/vc/jwt/JWTVerification.scala | 87 ++- .../vc/jwt/VerifiableCredentialPayload.scala | 48 +- .../io/iohk/atala/agent/server/Main.scala | 4 + .../atala/agent/server/PrismAgentApp.scala | 14 +- .../controller/VcVerificationController.scala | 21 + .../VcVerificationControllerImpl.scala | 33 + .../controller/VcVerificationEndpoints.scala | 34 + .../VcVerificationServerEndpoints.scala | 50 ++ .../http/ParameterizableVcVerification.scala | 18 + .../controller/http/VcVerification.scala | 79 +++ .../http/VcVerificationParameter.scala | 42 ++ .../http/VcVerificationRequest.scala | 70 ++ .../http/VcVerificationResponse.scala | 113 ++++ .../http/VcVerificationResult.scala | 26 + .../iohk/atala/api/util/Tapir2StaticOAS.scala | 2 + .../VcVerificationControllerImplSpec.scala | 129 ++++ .../VcVerificationControllerTestTools.scala | 90 +++ 25 files changed, 1817 insertions(+), 36 deletions(-) create mode 100644 pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerification.scala create mode 100644 pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationService.scala create mode 100644 pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceError.scala create mode 100644 pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceImpl.scala create mode 100644 pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceImplSpec.scala create mode 100644 pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceSpecHelper.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationController.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationControllerImpl.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationEndpoints.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationServerEndpoints.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/ParameterizableVcVerification.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerification.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationParameter.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationRequest.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationResponse.scala create mode 100644 prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationResult.scala create mode 100644 prism-agent/service/server/src/test/scala/io/iohk/atala/verification/controller/VcVerificationControllerImplSpec.scala create mode 100644 prism-agent/service/server/src/test/scala/io/iohk/atala/verification/controller/VcVerificationControllerTestTools.scala diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/CredentialSchema.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/CredentialSchema.scala index 6c05b941cb..8c12ad125a 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/CredentialSchema.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/model/schema/CredentialSchema.scala @@ -8,7 +8,7 @@ import io.iohk.atala.pollux.core.model.schema.`type`.{ CredentialJsonSchemaType, CredentialSchemaType } -import io.iohk.atala.pollux.core.model.schema.validator.JsonSchemaValidatorImpl +import io.iohk.atala.pollux.core.model.schema.validator.{JsonSchemaValidator, JsonSchemaValidatorImpl} import io.iohk.atala.pollux.core.service.URIDereferencer import zio.* import zio.json.* @@ -116,11 +116,10 @@ object CredentialSchema { given JsonEncoder[CredentialSchema] = DeriveJsonEncoder.gen[CredentialSchema] given JsonDecoder[CredentialSchema] = DeriveJsonDecoder.gen[CredentialSchema] - def validateJWTClaims( + def validSchemaValidator( schemaId: String, - claims: String, uriDereferencer: URIDereferencer - ): IO[CredentialSchemaError, Unit] = { + ): IO[CredentialSchemaError, JsonSchemaValidator] = { for { uri <- ZIO.attempt(new URI(schemaId)).mapError(t => URISyntaxError(t.getMessage)) content <- uriDereferencer.dereference(uri).mapError(err => UnexpectedError(err.toString)) @@ -137,7 +136,17 @@ object CredentialSchema { .mapError(error => CredentialSchemaParsingError(s"Failed to parse schema content as Json or OEA: $error")) .flatMap(cs => JsonSchemaValidatorImpl.from(cs.schema).mapError(SchemaError.apply)) ) - _ <- schemaValidator.validate(claims).mapError(SchemaError.apply) + } yield schemaValidator + } + + def validateJWTCredentialSubject( + schemaId: String, + credentialSubject: String, + uriDereferencer: URIDereferencer + ): IO[CredentialSchemaError, Unit] = { + for { + schemaValidator <- validSchemaValidator(schemaId, uriDereferencer) + _ <- schemaValidator.validate(credentialSubject).mapError(SchemaError.apply) } yield () } diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceImpl.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceImpl.scala index 06b2af96df..9b4e5fe06e 100644 --- a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceImpl.scala +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceImpl.scala @@ -139,7 +139,7 @@ private class CredentialServiceImpl( _ <- maybeSchemaId match case Some(schemaId) => CredentialSchema - .validateJWTClaims(schemaId, claims.noSpaces, uriDereferencer) + .validateJWTCredentialSubject(schemaId, claims.noSpaces, uriDereferencer) .mapError(e => CredentialSchemaError(e)) case None => ZIO.unit diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerification.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerification.scala new file mode 100644 index 0000000000..670849bd89 --- /dev/null +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerification.scala @@ -0,0 +1,31 @@ +package io.iohk.atala.pollux.core.service.verification + +import java.time.OffsetDateTime + +sealed trait VcVerification + +object VcVerification { + case object SignatureVerification extends VcVerification + + case class IssuerIdentification(iss: String) extends VcVerification + + case class ExpirationCheck(dateTime: OffsetDateTime) extends VcVerification + + case class NotBeforeCheck(dateTime: OffsetDateTime) extends VcVerification + + case class AudienceCheck(aud: String) extends VcVerification + + case object SubjectVerification extends VcVerification + + case object IntegrityOfClaims extends VcVerification + + case object ComplianceWithStandards extends VcVerification + + case object RevocationCheck extends VcVerification + + case object AlgorithmVerification extends VcVerification + + case object SchemaCheck extends VcVerification + + case object SemanticCheckOfClaims extends VcVerification +} diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationService.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationService.scala new file mode 100644 index 0000000000..915bd6e10b --- /dev/null +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationService.scala @@ -0,0 +1,18 @@ +package io.iohk.atala.pollux.core.service.verification + +import zio.* + +trait VcVerificationService { + def verify(request: List[VcVerificationRequest]): IO[VcVerificationServiceError, List[VcVerificationResult]] +} + +final case class VcVerificationRequest( + credential: String, + verification: VcVerification, +) + +final case class VcVerificationResult( + credential: String, + verification: VcVerification, + success: Boolean +) diff --git a/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceError.scala b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceError.scala new file mode 100644 index 0000000000..500fdcb052 --- /dev/null +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceError.scala @@ -0,0 +1,9 @@ +package io.iohk.atala.pollux.core.service.verification + +sealed trait VcVerificationServiceError { + def error: String +} + +object VcVerificationServiceError { + final case class UnexpectedError(error: String) extends VcVerificationServiceError +} 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 new file mode 100644 index 0000000000..3a3665f9ab --- /dev/null +++ b/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceImpl.scala @@ -0,0 +1,260 @@ +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, JWTVerification, JwtCredential} +import zio.{IO, *} + +import java.time.OffsetDateTime + +class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDereferencer) + extends VcVerificationService { + override def verify( + vcVerificationRequests: List[VcVerificationRequest] + ): IO[VcVerificationServiceError, List[VcVerificationResult]] = { + ZIO.collectAll( + vcVerificationRequests.map(vcVerificationRequest => + verify(vcVerificationRequest.credential, vcVerificationRequest.verification) + ) + ) + } + + private def verify( + credential: String, + verification: VcVerification, + ): IO[VcVerificationServiceError, VcVerificationResult] = { + verification match { + case VcVerification.SchemaCheck => verifySchema(credential) + case VcVerification.SignatureVerification => verifySignature(credential) + case VcVerification.ExpirationCheck(dateTime) => verifyExpiration(credential, dateTime) + case VcVerification.NotBeforeCheck(dateTime) => verifyNotBefore(credential, dateTime) + case VcVerification.AlgorithmVerification => verifyAlgorithm(credential) + case VcVerification.IssuerIdentification(iss) => verifyIssuerIdentification(credential, iss) + case VcVerification.SubjectVerification => verifySubjectVerification(credential) + case VcVerification.SemanticCheckOfClaims => verifySemanticCheckOfClaims(credential) + case VcVerification.AudienceCheck(aud) => verifyAudienceCheck(credential, aud) + case _ => + ZIO.fail( + VcVerificationServiceError.UnexpectedError( + s"Unsupported Verification:$verification" + ) + ) + } + } + + private def verifySchema(credential: String): IO[VcVerificationServiceError, VcVerificationResult] = { + val result = + for { + decodedJwt <- + JwtCredential + .decodeJwt(JWT(credential)) + .mapError(error => VcVerificationServiceError.UnexpectedError(s"Unable decode JWT: $error")) + credentialSchema <- + ZIO + .fromOption(decodedJwt.maybeCredentialSchema) + .mapError(error => VcVerificationServiceError.UnexpectedError(s"Missing Credential Schema: $error")) + result <- CredentialSchema + .validSchemaValidator( + credentialSchema.id, + uriDereferencer + ) + .mapError(error => VcVerificationServiceError.UnexpectedError(s"Schema Validator Failed: $error")) + } yield result + + result + .as( + VcVerificationResult( + credential = credential, + verification = VcVerification.SchemaCheck, + success = true + ) + ) + .catchAll(_ => + ZIO.succeed( + VcVerificationResult( + credential = credential, + verification = VcVerification.SchemaCheck, + success = false + ) + ) + ) + } + + private def verifySubjectVerification(credential: String): IO[VcVerificationServiceError, VcVerificationResult] = { + val result = + for { + decodedJwt <- + JwtCredential + .decodeJwt(JWT(credential)) + .mapError(error => VcVerificationServiceError.UnexpectedError(s"Unable decode JWT: $error")) + credentialSchema <- + ZIO + .fromOption(decodedJwt.maybeCredentialSchema) + .mapError(error => VcVerificationServiceError.UnexpectedError(s"Missing Credential Schema: $error")) + result <- CredentialSchema + .validateJWTCredentialSubject( + credentialSchema.id, + decodedJwt.credentialSubject.noSpaces, + uriDereferencer + ) + .mapError(error => + VcVerificationServiceError.UnexpectedError(s"JWT Credential Subject Validation Failed: $error") + ) + } yield result + + result + .as( + VcVerificationResult( + credential = credential, + verification = VcVerification.SubjectVerification, + success = true + ) + ) + .catchAll(_ => + ZIO.succeed( + VcVerificationResult( + credential = credential, + verification = VcVerification.SubjectVerification, + success = false + ) + ) + ) + } + + private def verifySignature(credential: String): IO[VcVerificationServiceError, VcVerificationResult] = { + JwtCredential + .validateEncodedJWT(JWT(credential))(didResolver) + .mapError(error => VcVerificationServiceError.UnexpectedError(error)) + .map(validation => + VcVerificationResult( + credential = credential, + verification = VcVerification.SignatureVerification, + success = validation + .map(_ => true) + .getOrElse(false) + ) + ) + } + + private def verifyExpiration( + credential: String, + dateTime: OffsetDateTime + ): IO[VcVerificationServiceError, VcVerificationResult] = { + ZIO.succeed( + VcVerificationResult( + credential = credential, + verification = VcVerification.ExpirationCheck(dateTime), + success = JwtCredential + .validateExpiration(JWT(credential), dateTime) + .map(_ => true) + .getOrElse(false) + ) + ) + } + + private def verifyNotBefore( + credential: String, + dateTime: OffsetDateTime + ): IO[VcVerificationServiceError, VcVerificationResult] = { + ZIO.succeed( + VcVerificationResult( + credential = credential, + verification = VcVerification.NotBeforeCheck(dateTime), + success = JwtCredential + .validateNotBefore(JWT(credential), dateTime) + .map(_ => true) + .getOrElse(false) + ) + ) + } + + private def verifyAlgorithm(credential: String): IO[VcVerificationServiceError, VcVerificationResult] = { + ZIO.succeed( + VcVerificationResult( + credential = credential, + verification = VcVerification.AlgorithmVerification, + success = JWTVerification + .validateAlgorithm(JWT(credential)) + .map(_ => true) + .getOrElse(false) + ) + ) + } + + private def verifyIssuerIdentification( + credential: String, + iss: String + ): IO[VcVerificationServiceError, VcVerificationResult] = { + val result = + for { + decodedJwt <- + JwtCredential + .decodeJwt(JWT(credential)) + .mapError(error => VcVerificationServiceError.UnexpectedError(s"Unable decode JWT: $error")) + } yield decodedJwt.iss.contains(iss) + + result + .map(success => + VcVerificationResult( + credential = credential, + verification = VcVerification.IssuerIdentification(iss), + success = success + ) + ) + } + + private def verifySemanticCheckOfClaims(credential: String): IO[VcVerificationServiceError, VcVerificationResult] = { + val result = + for { + decodedJwt <- + JwtCredential + .decodeJwt(JWT(credential)) + .mapError(error => VcVerificationServiceError.UnexpectedError(s"Unable decode JWT: $error")) + } yield decodedJwt + + result + .as( + VcVerificationResult( + credential = credential, + verification = VcVerification.SubjectVerification, + success = true + ) + ) + .catchAll(_ => + ZIO.succeed( + VcVerificationResult( + credential = credential, + verification = VcVerification.SubjectVerification, + success = false + ) + ) + ) + } + + private def verifyAudienceCheck( + credential: String, + aud: String + ): IO[VcVerificationServiceError, VcVerificationResult] = { + val result = + for { + decodedJwt <- + JwtCredential + .decodeJwt(JWT(credential)) + .mapError(error => VcVerificationServiceError.UnexpectedError(s"Unable decode JWT: $error")) + } yield decodedJwt.aud.contains(aud) + + result + .map(success => + VcVerificationResult( + credential = credential, + verification = VcVerification.AudienceCheck(aud), + success = success + ) + ) + } +} + +object VcVerificationServiceImpl { + val layer: URLayer[DidResolver & URIDereferencer, VcVerificationService] = + ZLayer.fromFunction(VcVerificationServiceImpl(_, _)) +} diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceImplSpec.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceImplSpec.scala new file mode 100644 index 0000000000..c621819224 --- /dev/null +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceImplSpec.scala @@ -0,0 +1,604 @@ +package io.iohk.atala.pollux.core.service.verification + +import io.circe.* +import io.circe.syntax.* +import io.iohk.atala.agent.walletapi.service.MockManagedDIDService +import io.iohk.atala.castor.core.service.MockDIDService +import io.iohk.atala.pollux.core.service.ResourceURIDereferencerImpl +import io.iohk.atala.pollux.vc.jwt.* +import io.iohk.atala.pollux.vc.jwt.CredentialPayload.Implicits.* +import io.iohk.atala.shared.models.{WalletAccessContext, WalletId} +import zio.* +import zio.Config.OffsetDateTime +import zio.test.* + +import java.time.Instant + +object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationServiceSpecHelper { + + override def spec = { + suite("VcVerificationServiceImpl")( + test("verify aud given valid") { + for { + svc <- ZIO.service[VcVerificationService] + verifier = DID("did:prism:verifier") + jwtCredentialPayload = W3cCredentialPayload( + `@context` = + Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), + maybeId = Some("http://example.edu/credentials/3732"), + `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), + issuer = issuer.did, + issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), + maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeCredentialSchema = Some( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) + ), + credentialSubject = Json.obj( + "userName" -> Json.fromString("Bob"), + "age" -> Json.fromInt(42), + "email" -> Json.fromString("email") + ), + maybeCredentialStatus = Some( + CredentialStatus( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "StatusList2021Entry", + statusPurpose = StatusPurpose.Revocation, + statusListIndex = 0, + statusListCredential = "https://example.com/credentials/status/3" + ) + ), + maybeRefreshService = Some( + RefreshService( + id = "https://example.edu/refresh/3732", + `type` = "ManualRefreshService2018" + ) + ), + maybeEvidence = Option.empty, + maybeTermsOfUse = Option.empty, + aud = Set(verifier.value) + ).toJwtCredentialPayload + signedJwtCredential = issuer.signer.encode(jwtCredentialPayload.asJson) + result <- + svc.verify( + List( + VcVerificationRequest(signedJwtCredential.value, VcVerification.AudienceCheck(verifier.value)) + ) + ) + } yield { + assertTrue( + result.contains( + VcVerificationResult(signedJwtCredential.value, VcVerification.AudienceCheck(verifier.value), true) + ) + ) + } + }.provideSomeLayer( + MockDIDService.empty ++ + MockManagedDIDService.empty ++ + ResourceURIDereferencerImpl.layer >+> + someVcVerificationServiceLayer ++ + ZLayer.succeed(WalletAccessContext(WalletId.random)) + ), + test("verify aud given invalid") { + for { + svc <- ZIO.service[VcVerificationService] + issuerDid = DID(issuerDidData.id.toString) + verifier = DID("did:prism:verifier") + jwtCredentialPayload = W3cCredentialPayload( + `@context` = + Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), + maybeId = Some("http://example.edu/credentials/3732"), + `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), + issuer = issuer.did, + issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), + maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeCredentialSchema = Some( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) + ), + credentialSubject = Json.obj( + "userName" -> Json.fromString("Bob"), + "age" -> Json.fromInt(42), + "email" -> Json.fromString("email") + ), + maybeCredentialStatus = Some( + CredentialStatus( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "StatusList2021Entry", + statusPurpose = StatusPurpose.Revocation, + statusListIndex = 0, + statusListCredential = "https://example.com/credentials/status/3" + ) + ), + maybeRefreshService = Some( + RefreshService( + id = "https://example.edu/refresh/3732", + `type` = "ManualRefreshService2018" + ) + ), + maybeEvidence = Option.empty, + maybeTermsOfUse = Option.empty, + aud = Set(verifier.value) + ).toJwtCredentialPayload + signedJwtCredential = issuer.signer.encode(jwtCredentialPayload.asJson) + result <- + svc.verify( + List( + VcVerificationRequest(signedJwtCredential.value, VcVerification.AudienceCheck(issuer.did.value)) + ) + ) + } yield { + assertTrue( + result.contains( + VcVerificationResult(signedJwtCredential.value, VcVerification.AudienceCheck(issuer.did.value), false) + ) + ) + } + }.provideSomeLayer( + MockDIDService.empty ++ + MockManagedDIDService.empty ++ + ResourceURIDereferencerImpl.layer >+> + someVcVerificationServiceLayer ++ + ZLayer.succeed(WalletAccessContext(WalletId.random)) + ), + test("verify signature given valid") { + for { + svc <- ZIO.service[VcVerificationService] + verifier = DID("did:prism:verifier") + jwtCredentialPayload = W3cCredentialPayload( + `@context` = + Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), + maybeId = Some("http://example.edu/credentials/3732"), + `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), + issuer = issuer.did, + issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), + maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeCredentialSchema = Some( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) + ), + credentialSubject = Json.obj( + "userName" -> Json.fromString("Bob"), + "age" -> Json.fromInt(42), + "email" -> Json.fromString("email") + ), + maybeCredentialStatus = Some( + CredentialStatus( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "StatusList2021Entry", + statusPurpose = StatusPurpose.Revocation, + statusListIndex = 0, + statusListCredential = "https://example.com/credentials/status/3" + ) + ), + maybeRefreshService = Some( + RefreshService( + id = "https://example.edu/refresh/3732", + `type` = "ManualRefreshService2018" + ) + ), + maybeEvidence = Option.empty, + maybeTermsOfUse = Option.empty, + aud = Set(verifier.value) + ).toJwtCredentialPayload + signedJwtCredential = issuer.signer.encode(jwtCredentialPayload.asJson) + result <- + svc.verify( + List( + VcVerificationRequest(signedJwtCredential.value, VcVerification.SignatureVerification) + ) + ) + } yield { + assertTrue( + result.contains( + VcVerificationResult(signedJwtCredential.value, VcVerification.SignatureVerification, true) + ) + ) + } + }.provideSomeLayer( + issuerDidServiceExpectations.toLayer ++ + MockManagedDIDService.empty ++ + ResourceURIDereferencerImpl.layer >+> + someVcVerificationServiceLayer ++ + ZLayer.succeed(WalletAccessContext(WalletId.random)) + ), + test("verify issuer given valid") { + for { + svc <- ZIO.service[VcVerificationService] + verifier = DID("did:prism:verifier") + jwtCredentialPayload = W3cCredentialPayload( + `@context` = + Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), + maybeId = Some("http://example.edu/credentials/3732"), + `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), + issuer = issuer.did, + issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), + maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeCredentialSchema = Some( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) + ), + credentialSubject = Json.obj( + "userName" -> Json.fromString("Bob"), + "age" -> Json.fromInt(42), + "email" -> Json.fromString("email") + ), + maybeCredentialStatus = Some( + CredentialStatus( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "StatusList2021Entry", + statusPurpose = StatusPurpose.Revocation, + statusListIndex = 0, + statusListCredential = "https://example.com/credentials/status/3" + ) + ), + maybeRefreshService = Some( + RefreshService( + id = "https://example.edu/refresh/3732", + `type` = "ManualRefreshService2018" + ) + ), + maybeEvidence = Option.empty, + maybeTermsOfUse = Option.empty, + aud = Set(verifier.value) + ).toJwtCredentialPayload + signedJwtCredential = issuer.signer.encode(jwtCredentialPayload.asJson) + result <- + svc.verify( + List( + VcVerificationRequest(signedJwtCredential.value, VcVerification.IssuerIdentification(issuer.did.value)) + ) + ) + } yield { + assertTrue( + result.contains( + VcVerificationResult( + signedJwtCredential.value, + VcVerification.IssuerIdentification(issuer.did.value), + true + ) + ) + ) + } + }.provideSomeLayer( + MockDIDService.empty ++ + MockManagedDIDService.empty ++ + ResourceURIDereferencerImpl.layer >+> + someVcVerificationServiceLayer ++ + ZLayer.succeed(WalletAccessContext(WalletId.random)) + ), + test("verify issuer given invalid") { + for { + svc <- ZIO.service[VcVerificationService] + issuerDid = DID(issuerDidData.id.toString) + verifier = DID("did:prism:verifier") + jwtCredentialPayload = W3cCredentialPayload( + `@context` = + Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), + maybeId = Some("http://example.edu/credentials/3732"), + `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), + issuer = issuer.did, + issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), + maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeCredentialSchema = Some( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) + ), + credentialSubject = Json.obj( + "userName" -> Json.fromString("Bob"), + "age" -> Json.fromInt(42), + "email" -> Json.fromString("email") + ), + maybeCredentialStatus = Some( + CredentialStatus( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "StatusList2021Entry", + statusPurpose = StatusPurpose.Revocation, + statusListIndex = 0, + statusListCredential = "https://example.com/credentials/status/3" + ) + ), + maybeRefreshService = Some( + RefreshService( + id = "https://example.edu/refresh/3732", + `type` = "ManualRefreshService2018" + ) + ), + maybeEvidence = Option.empty, + maybeTermsOfUse = Option.empty, + aud = Set(verifier.value) + ).toJwtCredentialPayload + signedJwtCredential = issuer.signer.encode(jwtCredentialPayload.asJson) + result <- + svc.verify( + List( + VcVerificationRequest(signedJwtCredential.value, VcVerification.IssuerIdentification(verifier.value)) + ) + ) + } yield { + assertTrue( + result.contains( + VcVerificationResult( + signedJwtCredential.value, + VcVerification.IssuerIdentification(verifier.value), + false + ) + ) + ) + } + }.provideSomeLayer( + MockDIDService.empty ++ + MockManagedDIDService.empty ++ + ResourceURIDereferencerImpl.layer >+> + someVcVerificationServiceLayer ++ + ZLayer.succeed(WalletAccessContext(WalletId.random)) + ), + test("verify nbf given valid") { + for { + svc <- ZIO.service[VcVerificationService] + verifier = DID("did:prism:verifier") + currentTime = OffsetDateTime.parse("2010-01-01T00:00:00Z").toOption.get + jwtCredentialPayload = W3cCredentialPayload( + `@context` = + Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), + maybeId = Some("http://example.edu/credentials/3732"), + `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), + issuer = issuer.did, + issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), + maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeCredentialSchema = Some( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) + ), + credentialSubject = Json.obj( + "userName" -> Json.fromString("Bob"), + "age" -> Json.fromInt(42), + "email" -> Json.fromString("email") + ), + maybeCredentialStatus = Some( + CredentialStatus( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "StatusList2021Entry", + statusPurpose = StatusPurpose.Revocation, + statusListIndex = 0, + statusListCredential = "https://example.com/credentials/status/3" + ) + ), + maybeRefreshService = Some( + RefreshService( + id = "https://example.edu/refresh/3732", + `type` = "ManualRefreshService2018" + ) + ), + maybeEvidence = Option.empty, + maybeTermsOfUse = Option.empty, + aud = Set(verifier.value) + ).toJwtCredentialPayload + signedJwtCredential = issuer.signer.encode(jwtCredentialPayload.asJson) + result <- + svc.verify( + List( + VcVerificationRequest(signedJwtCredential.value, VcVerification.NotBeforeCheck(currentTime)) + ) + ) + } yield { + assertTrue( + result.contains( + VcVerificationResult(signedJwtCredential.value, VcVerification.NotBeforeCheck(currentTime), true) + ) + ) + } + }.provideSomeLayer( + MockDIDService.empty ++ + MockManagedDIDService.empty ++ + ResourceURIDereferencerImpl.layer >+> + someVcVerificationServiceLayer ++ + ZLayer.succeed(WalletAccessContext(WalletId.random)) + ), + test("verify nbf given invalid") { + for { + svc <- ZIO.service[VcVerificationService] + verifier = DID("did:prism:verifier") + currentTime = OffsetDateTime.parse("2010-01-01T00:00:00Z").toOption.get.minusDays(2) + jwtCredentialPayload = W3cCredentialPayload( + `@context` = + Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), + maybeId = Some("http://example.edu/credentials/3732"), + `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), + issuer = issuer.did, + issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), + maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeCredentialSchema = Some( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) + ), + credentialSubject = Json.obj( + "userName" -> Json.fromString("Bob"), + "age" -> Json.fromInt(42), + "email" -> Json.fromString("email") + ), + maybeCredentialStatus = Some( + CredentialStatus( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "StatusList2021Entry", + statusPurpose = StatusPurpose.Revocation, + statusListIndex = 0, + statusListCredential = "https://example.com/credentials/status/3" + ) + ), + maybeRefreshService = Some( + RefreshService( + id = "https://example.edu/refresh/3732", + `type` = "ManualRefreshService2018" + ) + ), + maybeEvidence = Option.empty, + maybeTermsOfUse = Option.empty, + aud = Set(verifier.value) + ).toJwtCredentialPayload + signedJwtCredential = issuer.signer.encode(jwtCredentialPayload.asJson) + result <- + svc.verify( + List( + VcVerificationRequest(signedJwtCredential.value, VcVerification.NotBeforeCheck(currentTime)) + ) + ) + } yield { + assertTrue( + result.contains( + VcVerificationResult(signedJwtCredential.value, VcVerification.NotBeforeCheck(currentTime), false) + ) + ) + } + }.provideSomeLayer( + MockDIDService.empty ++ + MockManagedDIDService.empty ++ + ResourceURIDereferencerImpl.layer >+> + someVcVerificationServiceLayer ++ + ZLayer.succeed(WalletAccessContext(WalletId.random)) + ), + test("verify exp given valid") { + for { + svc <- ZIO.service[VcVerificationService] + verifier = DID("did:prism:verifier") + currentTime = OffsetDateTime.parse("2010-01-01T00:00:00Z").toOption.get + jwtCredentialPayload = W3cCredentialPayload( + `@context` = + Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), + maybeId = Some("http://example.edu/credentials/3732"), + `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), + issuer = issuer.did, + issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), + maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeCredentialSchema = Some( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) + ), + credentialSubject = Json.obj( + "userName" -> Json.fromString("Bob"), + "age" -> Json.fromInt(42), + "email" -> Json.fromString("email") + ), + maybeCredentialStatus = Some( + CredentialStatus( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "StatusList2021Entry", + statusPurpose = StatusPurpose.Revocation, + statusListIndex = 0, + statusListCredential = "https://example.com/credentials/status/3" + ) + ), + maybeRefreshService = Some( + RefreshService( + id = "https://example.edu/refresh/3732", + `type` = "ManualRefreshService2018" + ) + ), + maybeEvidence = Option.empty, + maybeTermsOfUse = Option.empty, + aud = Set(verifier.value) + ).toJwtCredentialPayload + signedJwtCredential = issuer.signer.encode(jwtCredentialPayload.asJson) + result <- + svc.verify( + List( + VcVerificationRequest(signedJwtCredential.value, VcVerification.ExpirationCheck(currentTime)) + ) + ) + } yield { + assertTrue( + result.contains( + VcVerificationResult(signedJwtCredential.value, VcVerification.ExpirationCheck(currentTime), true) + ) + ) + } + }.provideSomeLayer( + MockDIDService.empty ++ + MockManagedDIDService.empty ++ + ResourceURIDereferencerImpl.layer >+> + someVcVerificationServiceLayer ++ + ZLayer.succeed(WalletAccessContext(WalletId.random)) + ), + test("verify exp given invalid") { + for { + svc <- ZIO.service[VcVerificationService] + verifier = DID("did:prism:verifier") + currentTime = OffsetDateTime.parse("2010-01-12T00:00:00Z").toOption.get + jwtCredentialPayload = W3cCredentialPayload( + `@context` = + Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), + maybeId = Some("http://example.edu/credentials/3732"), + `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), + issuer = issuer.did, + issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), + maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeCredentialSchema = Some( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) + ), + credentialSubject = Json.obj( + "userName" -> Json.fromString("Bob"), + "age" -> Json.fromInt(42), + "email" -> Json.fromString("email") + ), + maybeCredentialStatus = Some( + CredentialStatus( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "StatusList2021Entry", + statusPurpose = StatusPurpose.Revocation, + statusListIndex = 0, + statusListCredential = "https://example.com/credentials/status/3" + ) + ), + maybeRefreshService = Some( + RefreshService( + id = "https://example.edu/refresh/3732", + `type` = "ManualRefreshService2018" + ) + ), + maybeEvidence = Option.empty, + maybeTermsOfUse = Option.empty, + aud = Set(verifier.value) + ).toJwtCredentialPayload + signedJwtCredential = issuer.signer.encode(jwtCredentialPayload.asJson) + result <- + svc.verify( + List( + VcVerificationRequest(signedJwtCredential.value, VcVerification.ExpirationCheck(currentTime)) + ) + ) + } yield { + assertTrue( + result.contains( + VcVerificationResult(signedJwtCredential.value, VcVerification.ExpirationCheck(currentTime), false) + ) + ) + } + }.provideSomeLayer( + MockDIDService.empty ++ + MockManagedDIDService.empty ++ + ResourceURIDereferencerImpl.layer >+> + someVcVerificationServiceLayer ++ + ZLayer.succeed(WalletAccessContext(WalletId.random)) + ) + ) + } +} diff --git a/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceSpecHelper.scala b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceSpecHelper.scala new file mode 100644 index 0000000000..a4778a9072 --- /dev/null +++ b/pollux/lib/core/src/test/scala/io/iohk/atala/pollux/core/service/verification/VcVerificationServiceSpecHelper.scala @@ -0,0 +1,50 @@ +package io.iohk.atala.pollux.core.service.verification + +import io.iohk.atala.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService} +import io.iohk.atala.castor.core.model.did.VerificationRelationship +import io.iohk.atala.castor.core.service.{DIDService, MockDIDService} +import io.iohk.atala.pollux.core.service.{ResourceURIDereferencerImpl, URIDereferencer} +import io.iohk.atala.pollux.vc.jwt.* +import io.iohk.atala.shared.models.WalletId.* +import io.iohk.atala.shared.models.{WalletAccessContext, WalletId} +import zio.* +import zio.mock.Expectation + +trait VcVerificationServiceSpecHelper { + protected val defaultWalletLayer: ULayer[WalletAccessContext] = ZLayer.succeed(WalletAccessContext(WalletId.default)) + + protected val (issuerOp, issuerKp, issuerDidMetadata, issuerDidData) = + MockDIDService.createDID(VerificationRelationship.AssertionMethod) + + protected val issuer = + Issuer( + did = io.iohk.atala.pollux.vc.jwt.DID(issuerDidData.id.did.toString), + signer = ES256KSigner(issuerKp.privateKey.toJavaPrivateKey), + publicKey = issuerKp.publicKey.toJavaPublicKey + ) + + protected val issuerDidServiceExpectations: Expectation[DIDService] = + MockDIDService.resolveDIDExpectation(issuerDidMetadata, issuerDidData) + + protected val issuerManagedDIDServiceExpectations: Expectation[ManagedDIDService] = + MockManagedDIDService.getManagedDIDStateExpectation(issuerOp) + ++ MockManagedDIDService.javaKeyPairWithDIDExpectation(issuerKp) + + protected val issuerDidResolverLayer: ZLayer[Any, Nothing, PrismDidResolver] = (issuerDidServiceExpectations ++ + issuerManagedDIDServiceExpectations).toLayer >>> ZLayer.fromFunction(PrismDidResolver(_)) + + protected val emptyDidResolverLayer: ZLayer[Any, Nothing, PrismDidResolver] = MockDIDService.empty ++ + MockManagedDIDService.empty >>> ZLayer.fromFunction(PrismDidResolver(_)) + + protected val vcVerificationServiceLayer: ZLayer[Any, Nothing, VcVerificationService with WalletAccessContext] = + emptyDidResolverLayer ++ ResourceURIDereferencerImpl.layer >>> + VcVerificationServiceImpl.layer ++ defaultWalletLayer + + protected val someVcVerificationServiceLayer + : URLayer[DIDService & ManagedDIDService & URIDereferencer, VcVerificationService] = + ZLayer.makeSome[DIDService & ManagedDIDService & URIDereferencer, VcVerificationService]( + ZLayer.fromFunction(PrismDidResolver(_)), + VcVerificationServiceImpl.layer + ) + +} 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..4b2a21d43c 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("Algorithm Not Supported")( + 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 c37183b110..aba96e8f76 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 @@ -1,21 +1,21 @@ package io.iohk.atala.pollux.vc.jwt import io.circe +import io.circe.* import io.circe.generic.auto.* import io.circe.parser.decode import io.circe.syntax.* -import io.circe.{CursorOp, Decoder, DecodingFailure, Encoder, HCursor, Json} import io.iohk.atala.castor.core.model.did.VerificationRelationship import io.iohk.atala.pollux.vc.jwt.revocation.BitString import io.iohk.atala.pollux.vc.jwt.schema.{SchemaResolver, SchemaValidator} import io.iohk.atala.shared.http.UriResolver import pdi.jwt.* -import zio.prelude.* import zio.* +import zio.prelude.* import java.security.PublicKey import java.time.temporal.TemporalAmount -import java.time.{Clock, Instant} +import java.time.{Clock, Instant, OffsetDateTime, ZoneId} import scala.util.Try opaque type DID = String @@ -716,6 +716,20 @@ object JwtCredential { .flatMap(decode[JwtCredentialPayload](_).toTry) } + def decodeJwt(jwt: JWT): IO[String, JwtCredentialPayload] = { + val decodeJWT = + ZIO.fromTry(JwtCirce.decodeRawAll(jwt.value, JwtOptions(false, false, false))).mapError(_.getMessage) + + val validatedDecodedClaim: IO[String, JwtCredentialPayload] = + for { + decodedJwtTask <- decodeJWT + (_, claim, _) = decodedJwtTask + decodedClaim <- ZIO.fromEither(decode[JwtCredentialPayload](claim).left.map(_.toString)) + } yield decodedClaim + + validatedDecodedClaim + } + def validateEncodedJwt(jwt: JWT, publicKey: PublicKey): Boolean = JwtCirce.isValid(jwt.value, publicKey, JwtOptions(expiration = false, notBefore = false)) @@ -728,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)( @@ -750,6 +772,26 @@ object JwtCredential { )(_.replicateZIODiscard(1)) } + def validateExpiration(jwt: JWT, dateTime: OffsetDateTime): Validation[String, Unit] = { + Validation + .fromTry( + JwtCirce(Clock.fixed(dateTime.toInstant, ZoneId.of(dateTime.getOffset.getId))) + .decodeRawAll(jwt.value, JwtOptions(false, true, false)) + ) + .flatMap(_ => Validation.unit) + .mapError(_.getMessage) + } + + def validateNotBefore(jwt: JWT, dateTime: OffsetDateTime): Validation[String, Unit] = { + Validation + .fromTry( + JwtCirce(Clock.fixed(dateTime.toInstant, ZoneId.of(dateTime.getOffset.getId))) + .decodeRawAll(jwt.value, JwtOptions(false, false, true)) + ) + .flatMap(_ => Validation.unit) + .mapError(_.getMessage) + } + def validateSchemaAndSignature( jwt: JWT )(didResolver: DidResolver)(schemaResolver: SchemaResolver)( diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 5661ded955..b12d7fbdf0 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -30,6 +30,7 @@ import io.iohk.atala.iam.wallet.http.controller.WalletManagementControllerImpl import io.iohk.atala.issue.controller.IssueControllerImpl import io.iohk.atala.mercury.* import io.iohk.atala.pollux.core.service.* +import io.iohk.atala.pollux.core.service.verification.VcVerificationServiceImpl import io.iohk.atala.pollux.credentialdefinition.controller.CredentialDefinitionControllerImpl import io.iohk.atala.pollux.credentialschema.controller.{ CredentialSchemaController, @@ -48,6 +49,7 @@ import io.iohk.atala.pollux.sql.repository.{ import io.iohk.atala.presentproof.controller.PresentProofControllerImpl import io.iohk.atala.resolvers.DIDResolver import io.iohk.atala.system.controller.SystemControllerImpl +import io.iohk.atala.verification.controller.VcVerificationControllerImpl import io.micrometer.prometheus.{PrometheusConfig, PrometheusMeterRegistry} import zio.* import zio.metrics.connectors.micrometer @@ -145,6 +147,7 @@ object MainApp extends ZIOAppDefault { IssueControllerImpl.layer, CredentialStatusControllerImpl.layer, PresentProofControllerImpl.layer, + VcVerificationControllerImpl.layer, VerificationPolicyControllerImpl.layer, EntityControllerImpl.layer, WalletManagementControllerImpl.layer, @@ -167,6 +170,7 @@ object MainApp extends ZIOAppDefault { LinkSecretServiceImpl.layer >>> PresentationServiceImpl.layer >>> PresentationServiceNotifier.layer, VerificationPolicyServiceImpl.layer, WalletManagementServiceImpl.layer, + VcVerificationServiceImpl.layer, // authentication AppModule.builtInAuthenticatorLayer, AppModule.keycloakAuthenticatorLayer, diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala index 253dc6a645..a8950f0c1b 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/PrismAgentApp.scala @@ -3,13 +3,7 @@ package io.iohk.atala.agent.server import io.iohk.atala.agent.notification.WebhookPublisher import io.iohk.atala.agent.server.config.AppConfig import io.iohk.atala.agent.server.http.{ZHttp4sBlazeServer, ZHttpEndpoints} -import io.iohk.atala.agent.server.jobs.{ - ConnectBackgroundJobs, - DIDStateSyncBackgroundJobs, - IssueBackgroundJobs, - PresentBackgroundJobs, - StatusListJobs -} +import io.iohk.atala.agent.server.jobs.* import io.iohk.atala.agent.walletapi.model.{Entity, Wallet, WalletSeed} import io.iohk.atala.agent.walletapi.service.{EntityService, ManagedDIDService, WalletManagementService} import io.iohk.atala.agent.walletapi.storage.DIDNonSecretStorage @@ -31,10 +25,10 @@ import io.iohk.atala.pollux.credentialschema.{SchemaRegistryServerEndpoints, Ver import io.iohk.atala.pollux.vc.jwt.DidResolver as JwtDidResolver import io.iohk.atala.presentproof.controller.PresentProofServerEndpoints import io.iohk.atala.resolvers.DIDResolver -import io.iohk.atala.shared.models.WalletAdministrationContext -import io.iohk.atala.shared.models.{HexString, WalletAccessContext, WalletId} +import io.iohk.atala.shared.models.{HexString, WalletAccessContext, WalletAdministrationContext, WalletId} import io.iohk.atala.shared.utils.DurationOps.toMetricsSeconds import io.iohk.atala.system.controller.SystemServerEndpoints +import io.iohk.atala.verification.controller.VcVerificationServerEndpoints import zio.* import zio.metrics.* @@ -133,6 +127,7 @@ object AgentHttpServer { allDIDEndpoints <- DIDServerEndpoints.all allDIDRegistrarEndpoints <- DIDRegistrarServerEndpoints.all allPresentProofEndpoints <- PresentProofServerEndpoints.all + allVcVerificationEndpoints <- VcVerificationServerEndpoints.all allSystemEndpoints <- SystemServerEndpoints.all allEntityEndpoints <- EntityServerEndpoints.all allWalletManagementEndpoints <- WalletManagementServerEndpoints.all @@ -146,6 +141,7 @@ object AgentHttpServer { allIssueEndpoints ++ allStatusListEndpoints ++ allPresentProofEndpoints ++ + allVcVerificationEndpoints ++ allSystemEndpoints ++ allEntityEndpoints ++ allWalletManagementEndpoints ++ diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationController.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationController.scala new file mode 100644 index 0000000000..b08e735a77 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationController.scala @@ -0,0 +1,21 @@ +package io.iohk.atala.verification.controller + +import io.iohk.atala.api.http.{ErrorResponse, RequestContext} +import io.iohk.atala.pollux.core.service.verification.VcVerificationServiceError +import io.iohk.atala.verification.controller +import zio.* + +trait VcVerificationController { + + def verify(request: List[controller.http.VcVerificationRequest])(implicit + rc: RequestContext + ): IO[ErrorResponse, List[controller.http.VcVerificationResponse]] +} + +object VcVerificationController { + def toHttpError(error: VcVerificationServiceError): ErrorResponse = + error match + case VcVerificationServiceError.UnexpectedError(error) => + ErrorResponse.badRequest(detail = Some(s"VC Verification Failed: $error")) + +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationControllerImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationControllerImpl.scala new file mode 100644 index 0000000000..200cb4e421 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationControllerImpl.scala @@ -0,0 +1,33 @@ +package io.iohk.atala.verification.controller + +import io.iohk.atala.api.http.{ErrorResponse, RequestContext} +import io.iohk.atala.pollux.core.service.verification.VcVerificationService +import io.iohk.atala.verification.controller +import zio.* + +class VcVerificationControllerImpl(vcVerificationService: VcVerificationService) extends VcVerificationController { + + override def verify( + requests: List[controller.http.VcVerificationRequest] + )(implicit rc: RequestContext): IO[ErrorResponse, List[controller.http.VcVerificationResponse]] = { + ZIO.collectAll( + requests.map(request => { + for { + serviceRequests <- controller.http.VcVerificationRequest.toService(request) + results <- + vcVerificationService + .verify(serviceRequests) + .mapError(error => VcVerificationController.toHttpError(error)) + } yield controller.http.VcVerificationResponse( + request.credential, + results.map(result => controller.http.VcVerificationResult.toService(result)) + ) + }) + ) + } +} + +object VcVerificationControllerImpl { + val layer: URLayer[VcVerificationService, VcVerificationController] = + ZLayer.fromFunction(VcVerificationControllerImpl(_)) +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationEndpoints.scala new file mode 100644 index 0000000000..f81c951fec --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationEndpoints.scala @@ -0,0 +1,34 @@ +package io.iohk.atala.verification.controller + +import io.iohk.atala.api.http.EndpointOutputs.* +import io.iohk.atala.api.http.{ErrorResponse, RequestContext} +import io.iohk.atala.iam.authentication.apikey.ApiKeyCredentials +import io.iohk.atala.iam.authentication.apikey.ApiKeyEndpointSecurityLogic.apiKeyHeader +import io.iohk.atala.iam.authentication.oidc.JwtCredentials +import io.iohk.atala.iam.authentication.oidc.JwtSecurityLogic.jwtAuthHeader +import sttp.model.StatusCode +import sttp.tapir.* +import sttp.tapir.json.zio.jsonBody + +object VcVerificationEndpoints { + val verify: Endpoint[ + (ApiKeyCredentials, JwtCredentials), + (RequestContext, List[http.VcVerificationRequest]), + ErrorResponse, + List[http.VcVerificationResponse], + Any + ] = + endpoint.get + .tag("Verifiable Credentials Verification") + .name("verify") + .summary("As a Verifier, verify a set of credentials") + .description("As a Verifier, verify a set of credentials") + .securityIn(apiKeyHeader) + .securityIn(jwtAuthHeader) + .in("verification" / "credential") + .in(extractFromRequest[RequestContext](RequestContext.apply)) + .in(jsonBody[List[http.VcVerificationRequest]].description("List of VC to verify")) + .out(statusCode(StatusCode.Ok).description("List of VC verification outcome")) + .out(jsonBody[List[http.VcVerificationResponse]]) + .errorOut(basicFailuresAndForbidden) +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationServerEndpoints.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationServerEndpoints.scala new file mode 100644 index 0000000000..2cc01ce215 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/VcVerificationServerEndpoints.scala @@ -0,0 +1,50 @@ +package io.iohk.atala.verification.controller + +import io.iohk.atala.LogUtils.* +import io.iohk.atala.agent.walletapi.model.BaseEntity +import io.iohk.atala.api.http.RequestContext +import io.iohk.atala.iam.authentication.{Authenticator, Authorizer, DefaultAuthenticator, SecurityLogic} +import io.iohk.atala.shared.models.WalletAccessContext +import io.iohk.atala.verification.controller +import io.iohk.atala.verification.controller.VcVerificationEndpoints.verify +import sttp.tapir.ztapir.* +import zio.* + +class VcVerificationServerEndpoints( + vcVerificationController: VcVerificationController, + authenticator: Authenticator[BaseEntity], + authorizer: Authorizer[BaseEntity] +) { + + val verifyEndpoint: ZServerEndpoint[Any, Any] = + verify + .zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer)) + .serverLogic { wac => + { case (ctx: RequestContext, request: List[controller.http.VcVerificationRequest]) => + vcVerificationController + .verify(request)(ctx) + .provideSomeLayer(ZLayer.succeed(wac)) + .logTrace(ctx) + } + } + + val all: List[ZServerEndpoint[Any, Any]] = List( + verifyEndpoint + ) + +} + +object VcVerificationServerEndpoints { + def all: URIO[VcVerificationController & DefaultAuthenticator, List[ZServerEndpoint[Any, Any]]] = { + for { + authenticator <- ZIO.service[DefaultAuthenticator] + vcVerificationController <- ZIO.service[VcVerificationController] + vcVerificationProofEndpoints = + new VcVerificationServerEndpoints( + vcVerificationController, + authenticator, + authenticator + ) + } yield vcVerificationProofEndpoints.all + } +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/ParameterizableVcVerification.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/ParameterizableVcVerification.scala new file mode 100644 index 0000000000..8994cedf77 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/ParameterizableVcVerification.scala @@ -0,0 +1,18 @@ +package io.iohk.atala.verification.controller.http + +import sttp.tapir.Schema +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} + +final case class ParameterizableVcVerification( + verification: VcVerification, + parameter: Option[VcVerificationParameter] +) +object ParameterizableVcVerification { + given encoder: JsonEncoder[ParameterizableVcVerification] = + DeriveJsonEncoder.gen[ParameterizableVcVerification] + + given decoder: JsonDecoder[ParameterizableVcVerification] = + DeriveJsonDecoder.gen[ParameterizableVcVerification] + + given schema: Schema[ParameterizableVcVerification] = Schema.derived +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerification.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerification.scala new file mode 100644 index 0000000000..345e582714 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerification.scala @@ -0,0 +1,79 @@ +package io.iohk.atala.verification.controller.http + +import io.iohk.atala.api.http.ErrorResponse +import io.iohk.atala.pollux.core.service +import io.iohk.atala.pollux.core.service.verification.VcVerification as ServiceVcVerification +import sttp.tapir.Schema +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} +import zio.{IO, *} + +enum VcVerification { + case SignatureVerification + case IssuerIdentification + case ExpirationCheck + case NotBeforeCheck + case AudienceCheck + case SubjectVerification + case IntegrityOfClaims + case ComplianceWithStandards + case RevocationCheck + case AlgorithmVerification + case SchemaCheck + case SemanticCheckOfClaims +} + +object VcVerification { + given encoder: JsonEncoder[VcVerification] = + DeriveJsonEncoder.gen[VcVerification] + + given decoder: JsonDecoder[VcVerification] = + DeriveJsonDecoder.gen[VcVerification] + + given schema: Schema[VcVerification] = Schema.derivedEnumeration.defaultStringBased + + def convert( + verification: VcVerification, + maybeParameter: Option[VcVerificationParameter] + ): IO[ErrorResponse, ServiceVcVerification] = { + (verification, maybeParameter) match { + case (SignatureVerification, None) => ZIO.succeed(ServiceVcVerification.SignatureVerification) + case (IssuerIdentification, Some(DidParameter(iss))) => + ZIO.succeed(ServiceVcVerification.IssuerIdentification(iss)) + case (ExpirationCheck, Some(DateTimeParameter(dateTime))) => + ZIO.succeed(ServiceVcVerification.ExpirationCheck(dateTime)) + case (NotBeforeCheck, Some(DateTimeParameter(dateTime))) => + ZIO.succeed(ServiceVcVerification.NotBeforeCheck(dateTime)) + case (AudienceCheck, Some(DidParameter(aud))) => ZIO.succeed(ServiceVcVerification.AudienceCheck(aud)) + case (SubjectVerification, None) => ZIO.succeed(ServiceVcVerification.SubjectVerification) + case (IntegrityOfClaims, None) => ZIO.succeed(ServiceVcVerification.IntegrityOfClaims) + case (ComplianceWithStandards, None) => ZIO.succeed(ServiceVcVerification.ComplianceWithStandards) + case (RevocationCheck, None) => ZIO.succeed(ServiceVcVerification.RevocationCheck) + case (AlgorithmVerification, None) => ZIO.succeed(ServiceVcVerification.AlgorithmVerification) + case (SchemaCheck, None) => ZIO.succeed(ServiceVcVerification.SchemaCheck) + case (SemanticCheckOfClaims, None) => ZIO.succeed(ServiceVcVerification.SemanticCheckOfClaims) + case _ => + ZIO.fail( + ErrorResponse.badRequest(detail = + Some(s"Unsupported Verification:$verification and Parameters:$maybeParameter") + ) + ) + } + } + + def toService(verification: ServiceVcVerification): VcVerification = { + verification match { + case ServiceVcVerification.SignatureVerification => SignatureVerification + case ServiceVcVerification.IssuerIdentification(_) => IssuerIdentification + case ServiceVcVerification.ExpirationCheck(_) => ExpirationCheck + case ServiceVcVerification.NotBeforeCheck(_) => NotBeforeCheck + case ServiceVcVerification.AudienceCheck(_) => AudienceCheck + case ServiceVcVerification.SubjectVerification => SubjectVerification + case ServiceVcVerification.IntegrityOfClaims => IntegrityOfClaims + case ServiceVcVerification.ComplianceWithStandards => ComplianceWithStandards + case ServiceVcVerification.RevocationCheck => RevocationCheck + case ServiceVcVerification.AlgorithmVerification => AlgorithmVerification + case ServiceVcVerification.SchemaCheck => SchemaCheck + case ServiceVcVerification.SemanticCheckOfClaims => SemanticCheckOfClaims + } + } +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationParameter.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationParameter.scala new file mode 100644 index 0000000000..66ca951fcb --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationParameter.scala @@ -0,0 +1,42 @@ +package io.iohk.atala.verification.controller.http + +import sttp.tapir.Schema +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} + +import java.time.OffsetDateTime + +sealed trait VcVerificationParameter + +object VcVerificationParameter { + given encoder: JsonEncoder[VcVerificationParameter] = + DeriveJsonEncoder.gen[VcVerificationParameter] + + given decoder: JsonDecoder[VcVerificationParameter] = + DeriveJsonDecoder.gen[VcVerificationParameter] + + given schema: Schema[VcVerificationParameter] = Schema.derived +} + +case class DidParameter(aud: String) extends VcVerificationParameter + +object DidParameter { + given encoder: JsonEncoder[DidParameter] = + DeriveJsonEncoder.gen[DidParameter] + + given decoder: JsonDecoder[DidParameter] = + DeriveJsonDecoder.gen[DidParameter] + + given schema: Schema[DidParameter] = Schema.derived +} + +case class DateTimeParameter(dateTime: OffsetDateTime) extends VcVerificationParameter + +object DateTimeParameter { + given encoder: JsonEncoder[DateTimeParameter] = + DeriveJsonEncoder.gen[DateTimeParameter] + + given decoder: JsonDecoder[DateTimeParameter] = + DeriveJsonDecoder.gen[DateTimeParameter] + + given schema: Schema[DateTimeParameter] = Schema.derived +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationRequest.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationRequest.scala new file mode 100644 index 0000000000..dd7602b3d0 --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationRequest.scala @@ -0,0 +1,70 @@ +package io.iohk.atala.verification.controller.http + +import io.iohk.atala.api.http.{Annotation, ErrorResponse} +import io.iohk.atala.pollux.core.service.verification.VcVerificationRequest as ServiceVcVerificationRequest +import sttp.tapir.Schema +import sttp.tapir.Schema.annotations.{description, encodedExample} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} +import zio.{IO, *} + +final case class VcVerificationRequest( + @description(VcVerificationRequest.annotations.credential.description) + @encodedExample(VcVerificationRequest.annotations.credential.example) + credential: String, + @description(VcVerificationRequest.annotations.vcVerification.description) + @encodedExample(VcVerificationRequest.annotations.vcVerification.example) + verifications: List[ParameterizableVcVerification] +) + +object VcVerificationRequest { + object annotations { + + object credential + extends Annotation[String]( + description = "Encoded Verifiable Credential to verify", + example = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + ) + + object vcVerification + extends Annotation[List[VcVerification]]( + description = "The list of Verifications to verify. All verifications run if Verifications left empty", + example = List( + VcVerification.SignatureVerification, + VcVerification.IssuerIdentification, + VcVerification.ExpirationCheck, + VcVerification.NotBeforeCheck, + VcVerification.AudienceCheck, + VcVerification.SubjectVerification, + VcVerification.IntegrityOfClaims, + VcVerification.ComplianceWithStandards, + VcVerification.RevocationCheck, + VcVerification.AlgorithmVerification, + VcVerification.SchemaCheck, + VcVerification.SemanticCheckOfClaims, + ) + ) + } + + given credentialVerificationRequestEncoder: JsonEncoder[VcVerificationRequest] = + DeriveJsonEncoder.gen[VcVerificationRequest] + + given credentialVerificationRequestDecoder: JsonDecoder[VcVerificationRequest] = + DeriveJsonDecoder.gen[VcVerificationRequest] + + given credentialVerificationRequestSchema: Schema[VcVerificationRequest] = Schema.derived + + def toService(request: VcVerificationRequest): IO[ErrorResponse, List[ServiceVcVerificationRequest]] = { + ZIO.collectAll( + request.verifications.map(verification => + for { + serviceVerification <- VcVerification.convert( + verification.verification, + verification.parameter + ) + + } yield ServiceVcVerificationRequest(credential = request.credential, verification = serviceVerification) + ) + ) + } +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationResponse.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationResponse.scala new file mode 100644 index 0000000000..70e4ee35bc --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationResponse.scala @@ -0,0 +1,113 @@ +package io.iohk.atala.verification.controller.http + +import io.iohk.atala.api.http.Annotation +import sttp.tapir.Schema +import sttp.tapir.Schema.annotations.{description, encodedExample} +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} + +final case class VcVerificationResponse( + @description(VcVerificationResponse.annotations.credential.description) + @encodedExample(VcVerificationResponse.annotations.credential.example) + credential: String, + @description(VcVerificationResponse.annotations.checks.description) + @encodedExample(VcVerificationResponse.annotations.checks.example) + result: List[VcVerificationResult], +) + +object VcVerificationResponse { + + object annotations { + + object credential + extends Annotation[String]( + description = "Encoded Verifiable Credential to verify", + example = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + ) + + object checks + extends Annotation[List[VcVerification]]( + description = "The list executed Verifications", + example = List( + VcVerification.SignatureVerification, + VcVerification.IssuerIdentification, + VcVerification.ExpirationCheck, + VcVerification.NotBeforeCheck, + VcVerification.AudienceCheck, + VcVerification.SubjectVerification, + VcVerification.IntegrityOfClaims, + VcVerification.ComplianceWithStandards, + VcVerification.RevocationCheck, + VcVerification.AlgorithmVerification, + VcVerification.SchemaCheck, + VcVerification.SemanticCheckOfClaims, + ) + ) + + object successfulChecks + extends Annotation[List[VcVerification]]( + description = "The list of successful Verifications", + example = List( + VcVerification.SignatureVerification, + VcVerification.IssuerIdentification, + VcVerification.ExpirationCheck, + VcVerification.NotBeforeCheck, + VcVerification.AudienceCheck, + VcVerification.SubjectVerification, + VcVerification.IntegrityOfClaims, + VcVerification.ComplianceWithStandards, + VcVerification.RevocationCheck, + VcVerification.AlgorithmVerification, + VcVerification.SchemaCheck, + VcVerification.SemanticCheckOfClaims, + ) + ) + + object failedChecks + extends Annotation[List[VcVerification]]( + description = "The list of failed Verifications.", + example = List( + VcVerification.SignatureVerification, + VcVerification.IssuerIdentification, + VcVerification.ExpirationCheck, + VcVerification.NotBeforeCheck, + VcVerification.AudienceCheck, + VcVerification.SubjectVerification, + VcVerification.IntegrityOfClaims, + VcVerification.ComplianceWithStandards, + VcVerification.RevocationCheck, + VcVerification.AlgorithmVerification, + VcVerification.SchemaCheck, + VcVerification.SemanticCheckOfClaims, + ) + ) + + object failedAsWarningChecks + extends Annotation[List[VcVerification]]( + description = "The list of failed Verifications as warning", + example = List( + VcVerification.SignatureVerification, + VcVerification.IssuerIdentification, + VcVerification.ExpirationCheck, + VcVerification.NotBeforeCheck, + VcVerification.AudienceCheck, + VcVerification.SubjectVerification, + VcVerification.IntegrityOfClaims, + VcVerification.ComplianceWithStandards, + VcVerification.RevocationCheck, + VcVerification.AlgorithmVerification, + VcVerification.SchemaCheck, + VcVerification.SemanticCheckOfClaims, + ) + ) + } + + given credentialVerificationRequestEncoder: JsonEncoder[VcVerificationResponse] = + DeriveJsonEncoder.gen[VcVerificationResponse] + + given credentialVerificationRequestDecoder: JsonDecoder[VcVerificationResponse] = + DeriveJsonDecoder.gen[VcVerificationResponse] + + given credentialVerificationRequestSchema: Schema[VcVerificationResponse] = Schema.derived + +} diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationResult.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationResult.scala new file mode 100644 index 0000000000..99158682da --- /dev/null +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/verification/controller/http/VcVerificationResult.scala @@ -0,0 +1,26 @@ +package io.iohk.atala.verification.controller.http + +import io.iohk.atala.pollux.core.service.verification.VcVerificationResult as ServiceVcVerificationResult +import sttp.tapir.Schema +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} + +final case class VcVerificationResult( + verification: VcVerification, + success: Boolean +) +object VcVerificationResult { + given encoder: JsonEncoder[VcVerificationResult] = + DeriveJsonEncoder.gen[VcVerificationResult] + + given decoder: JsonDecoder[VcVerificationResult] = + DeriveJsonDecoder.gen[VcVerificationResult] + + given schema: Schema[VcVerificationResult] = Schema.derived + + def toService(result: ServiceVcVerificationResult): VcVerificationResult = { + VcVerificationResult( + verification = VcVerification.toService(result.verification), + success = result.success + ) + } +} diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala index 73593dfd12..68b510f62e 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/api/util/Tapir2StaticOAS.scala @@ -14,6 +14,7 @@ import io.iohk.atala.pollux.credentialdefinition.controller.CredentialDefinition import io.iohk.atala.pollux.credentialschema.controller.{CredentialSchemaController, VerificationPolicyController} import io.iohk.atala.presentproof.controller.PresentProofController import io.iohk.atala.system.controller.SystemController +import io.iohk.atala.verification.controller.VcVerificationController import org.scalatestplus.mockito.MockitoSugar.* import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter import zio.{Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer} @@ -44,6 +45,7 @@ object Tapir2StaticOAS extends ZIOAppDefault { ZLayer.succeed(mock[VerificationPolicyController]) ++ ZLayer.succeed(mock[DIDRegistrarController]) ++ ZLayer.succeed(mock[PresentProofController]) ++ + ZLayer.succeed(mock[VcVerificationController]) ++ ZLayer.succeed(mock[IssueController]) ++ ZLayer.succeed(mock[DIDController]) ++ ZLayer.succeed(mock[SystemController]) ++ diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/verification/controller/VcVerificationControllerImplSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/verification/controller/VcVerificationControllerImplSpec.scala new file mode 100644 index 0000000000..4e67ad59bf --- /dev/null +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/verification/controller/VcVerificationControllerImplSpec.scala @@ -0,0 +1,129 @@ +package io.iohk.atala.verification.controller + +import io.circe.* +import io.circe.syntax.* +import io.iohk.atala.agent.walletapi.model.BaseEntity +import io.iohk.atala.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService} +import io.iohk.atala.castor.core.service.MockDIDService +import io.iohk.atala.iam.authentication.AuthenticatorWithAuthZ +import io.iohk.atala.pollux.vc.jwt.* +import io.iohk.atala.pollux.vc.jwt.CredentialPayload.Implicits.* +import io.iohk.atala.verification.controller.http.* +import sttp.client3.ziojson.* +import sttp.client3.{DeserializationException, Response, UriContext, basicRequest} +import sttp.model.StatusCode +import zio.* +import zio.Config.OffsetDateTime +import zio.json.EncoderOps +import zio.test.* +import zio.test.Assertion.* + +import java.time.Instant + +object VcVerificationControllerImplSpec extends ZIOSpecDefault with VcVerificationControllerTestTools { + + def spec = httpErrorResponses.provideSomeLayerShared( + MockDIDService.empty ++ MockManagedDIDService.empty >>> testEnvironmentLayer + ) + + private val httpErrorResponses = suite("IssueControllerImp http failure cases")( + test("provide incorrect recordId to endpoint") { + for { + vcVerificationController <- ZIO.service[VcVerificationController] + verifier = DID("did:prism:verifier") + currentTime = OffsetDateTime.parse("2010-01-01T00:00:00Z").toOption.get + jwtCredentialPayload = W3cCredentialPayload( + `@context` = Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), + maybeId = Some("http://example.edu/credentials/3732"), + `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), + issuer = issuer.did, + issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), + maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeCredentialSchema = Some( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) + ), + credentialSubject = Json.obj( + "userName" -> Json.fromString("Bob"), + "age" -> Json.fromInt(42), + "email" -> Json.fromString("email") + ), + maybeCredentialStatus = Some( + CredentialStatus( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "StatusList2021Entry", + statusPurpose = StatusPurpose.Revocation, + statusListIndex = 0, + statusListCredential = "https://example.com/credentials/status/3" + ) + ), + maybeRefreshService = Some( + RefreshService( + id = "https://example.edu/refresh/3732", + `type` = "ManualRefreshService2018" + ) + ), + maybeEvidence = Option.empty, + maybeTermsOfUse = Option.empty, + aud = Set(verifier.value) + ).toJwtCredentialPayload + signedJwtCredential = issuer.signer.encode(jwtCredentialPayload.asJson) + authenticator <- ZIO.service[AuthenticatorWithAuthZ[BaseEntity]] + backend = httpBackend(vcVerificationController, authenticator) + response: Response[Either[DeserializationException[String], List[VcVerificationResponse]]] <- + basicRequest + .get(uri"${vcVerificationUriBase}") + .body( + List( + VcVerificationRequest( + signedJwtCredential.value, + List( + ParameterizableVcVerification(VcVerification.SignatureVerification, None), + ParameterizableVcVerification(VcVerification.NotBeforeCheck, Some(DateTimeParameter(currentTime))), + ParameterizableVcVerification(VcVerification.ExpirationCheck, Some(DateTimeParameter(currentTime))) + ) + ), + VcVerificationRequest( + signedJwtCredential.value, + List( + ParameterizableVcVerification(VcVerification.AudienceCheck, Some(DidParameter(verifier.value))), + ParameterizableVcVerification( + VcVerification.IssuerIdentification, + Some(DidParameter(issuer.did.value)) + ) + ) + ) + ).toJsonPretty + ) + .response(asJsonAlways[List[VcVerificationResponse]]) + .send(backend) + statusCodeIs200 = assert(response.code)(equalTo(StatusCode.Ok)) + body <- ZIO.fromEither(response.body) + bodyIsOk = assert(body)( + equalTo( + List( + VcVerificationResponse( + signedJwtCredential.value, + List( + VcVerificationResult(VcVerification.SignatureVerification, false), + VcVerificationResult(VcVerification.NotBeforeCheck, true), + VcVerificationResult(VcVerification.ExpirationCheck, true) + ) + ), + VcVerificationResponse( + signedJwtCredential.value, + List( + VcVerificationResult(VcVerification.AudienceCheck, true), + VcVerificationResult(VcVerification.IssuerIdentification, true) + ) + ) + ) + ) + ) + } yield statusCodeIs200 && bodyIsOk + } + ) + +} diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/verification/controller/VcVerificationControllerTestTools.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/verification/controller/VcVerificationControllerTestTools.scala new file mode 100644 index 0000000000..30bce18ce2 --- /dev/null +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/verification/controller/VcVerificationControllerTestTools.scala @@ -0,0 +1,90 @@ +package io.iohk.atala.verification.controller + +import io.iohk.atala.agent.server.http.CustomServerInterceptors +import io.iohk.atala.agent.walletapi.model.BaseEntity +import io.iohk.atala.agent.walletapi.service.ManagedDIDService +import io.iohk.atala.castor.core.model.did.VerificationRelationship +import io.iohk.atala.castor.core.service.MockDIDService +import io.iohk.atala.iam.authentication.{AuthenticatorWithAuthZ, DefaultEntityAuthenticator} +import io.iohk.atala.pollux.core.service.* +import io.iohk.atala.pollux.core.service.verification.{VcVerificationService, VcVerificationServiceImpl} +import io.iohk.atala.pollux.vc.jwt.* +import io.iohk.atala.shared.models.WalletId.* +import io.iohk.atala.shared.models.{WalletAccessContext, WalletId} +import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport +import sttp.client3.UriContext +import sttp.client3.testing.SttpBackendStub +import sttp.monad.MonadError +import sttp.tapir.server.interceptor.CustomiseInterceptors +import sttp.tapir.server.stub.TapirStubInterpreter +import sttp.tapir.ztapir.RIOMonadError +import zio.* +import zio.test.* + +trait VcVerificationControllerTestTools extends PostgresTestContainerSupport { + self: ZIOSpecDefault => + + protected val (issuerOp, issuerKp, issuerDidMetadata, issuerDidData) = + MockDIDService.createDID(VerificationRelationship.AssertionMethod) + + protected val issuer = + Issuer( + did = io.iohk.atala.pollux.vc.jwt.DID(issuerDidData.id.did.toString), + signer = ES256KSigner(issuerKp.privateKey.toJavaPrivateKey), + publicKey = issuerKp.publicKey.toJavaPublicKey + ) + + val didResolverLayer = ZLayer.fromZIO(ZIO.succeed(makeResolver(Map.empty))) + + private[this] def makeResolver(lookup: Map[String, DIDDocument]): DidResolver = (didUrl: String) => { + lookup + .get(didUrl) + .fold( + ZIO.succeed(DIDResolutionFailed(NotFound(s"DIDDocument not found for $didUrl"))) + )((didDocument: DIDDocument) => { + ZIO.succeed( + DIDResolutionSucceeded( + didDocument, + DIDDocumentMetadata() + ) + ) + }) + } + + protected val defaultWalletLayer = ZLayer.succeed(WalletAccessContext(WalletId.default)) + + lazy val testEnvironmentLayer = + zio.test.testEnvironment ++ ZLayer.makeSome[ + ManagedDIDService, + VcVerificationController & VcVerificationService & AuthenticatorWithAuthZ[BaseEntity] + ]( + didResolverLayer, + ResourceURIDereferencerImpl.layer, + VcVerificationControllerImpl.layer, + VcVerificationServiceImpl.layer, + DefaultEntityAuthenticator.layer + ) + + val vcVerificationUriBase = uri"http://test.com/verification/credential" + + def bootstrapOptions[F[_]](monadError: MonadError[F]): CustomiseInterceptors[F, Any] = { + new CustomiseInterceptors[F, Any](_ => ()) + .exceptionHandler(CustomServerInterceptors.exceptionHandler) + .rejectHandler(CustomServerInterceptors.rejectHandler) + .decodeFailureHandler(CustomServerInterceptors.decodeFailureHandler) + } + + def httpBackend(controller: VcVerificationController, authenticator: AuthenticatorWithAuthZ[BaseEntity]) = { + val vcVerificationEndpoints = VcVerificationServerEndpoints(controller, authenticator, authenticator) + val backend = + TapirStubInterpreter( + bootstrapOptions(new RIOMonadError[Any]), + SttpBackendStub(new RIOMonadError[Any]) + ) + .whenServerEndpoint(vcVerificationEndpoints.verifyEndpoint) + .thenRunLogic() + .backend() + backend + } + +}