Skip to content

Commit

Permalink
feat: Vc Verification Api (#975)
Browse files Browse the repository at this point in the history
Signed-off-by: Bassam Riman <[email protected]>
Signed-off-by: Benjamin Voiturier <[email protected]>
Co-authored-by: Benjamin Voiturier <[email protected]>
  • Loading branch information
CryptoKnightIOG and bvoiturier authored Apr 24, 2024
1 parent 1268178 commit f0a1f2c
Show file tree
Hide file tree
Showing 25 changed files with 1,817 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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))
Expand All @@ -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 ()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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(_, _))
}
Loading

0 comments on commit f0a1f2c

Please sign in to comment.