Skip to content

Commit

Permalink
feat(pollux): [ATL-2640] JWT Presentation Signature Verification Usin…
Browse files Browse the repository at this point in the history
…g DidResolver (#212)
  • Loading branch information
CryptoKnightIOG authored Dec 8, 2022
1 parent 460b0cc commit 258c6c0
Show file tree
Hide file tree
Showing 7 changed files with 631 additions and 331 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.iohk.atala.pollux.vc.jwt

import com.nimbusds.jose.jwk.ECKey
import io.circe
import io.circe.*
import io.circe.generic.auto.*
Expand Down Expand Up @@ -66,3 +67,13 @@ class ES256Signer(privateKey: PrivateKey) extends Signer {
return JWT(JwtCirce.encode(claim, privateKey, algorithm))
}
}

def toJWKFormat(holderJwk: ECKey): JsonWebKey = {
JsonWebKey(
kty = "EC",
crv = Some(holderJwk.getCurve.getName),
x = Some(holderJwk.getX.toJSONString),
y = Some(holderJwk.getY.toJSONString),
d = Some(holderJwk.getD.toJSONString)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package io.iohk.atala.pollux.vc.jwt
import com.nimbusds.jose.jwk.*
import com.nimbusds.jose.jwk.gen.*
import com.nimbusds.jose.util.Base64URL
import io.circe
import io.circe.generic.auto.*
import io.circe.parser.decode
import io.circe.syntax.*
import io.circe.{Decoder, Encoder, HCursor, Json}
import io.iohk.atala.pollux.vc.jwt.schema.{SchemaResolver, SchemaValidator}
import net.reactivecore.cjs.validator.Violation
import net.reactivecore.cjs.{DocumentValidator, Loader}
import pdi.jwt.*
import zio.prelude.*
import zio.{IO, NonEmptyChunk, Task, ZIO}

import java.security.spec.{ECParameterSpec, ECPublicKeySpec}
import java.security.{KeyPairGenerator, PublicKey}
import java.time.temporal.{Temporal, TemporalAmount, TemporalUnit}
import java.time.{Clock, Instant, ZonedDateTime}
import java.util
import scala.util.{Failure, Success, Try}

object JWTVerification {
def validateEncodedJwt[T](jwt: JWT)(
didResolver: DidResolver
)(decoder: String => IO[String, T])(issuerDidExtractor: T => String): IO[String, Boolean] = {
val decodeJWT = ZIO
.fromTry(JwtCirce.decodeRawAll(jwt.value, JwtOptions(false, false, false)))
.mapError(_.getMessage)

val extractAlgorithm =
for {
decodedJwtTask <- decodeJWT
(header, _, _) = decodedJwtTask
algorithm <- Validation
.fromOptionWith("An algorithm must be specified in the header")(JwtCirce.parseHeader(header).algorithm)
.toZIO
} yield algorithm

val loadDidDocument =
for {
decodedJwtTask <- decodeJWT
(_, claim, _) = decodedJwtTask
decodedClaim <- decoder(claim)
extractIssuerDid = issuerDidExtractor(decodedClaim)
resolvedDidDocument <- resolve(extractIssuerDid)(didResolver)
} yield resolvedDidDocument

for {
results <- loadDidDocument validatePar extractAlgorithm
(didDocument, algorithm) = results
verificationMethods <- extractVerificationMethods(didDocument, algorithm)
} yield validateEncodedJwt(jwt, verificationMethods)
}

def validateEncodedJwt(jwt: JWT, publicKey: PublicKey): Boolean =
JwtCirce.isValid(jwt.value, publicKey)

def validateEncodedJwt(jwt: JWT, verificationMethods: IndexedSeq[VerificationMethod]): Boolean = {
verificationMethods.exists(verificationMethod =>
toPublicKey(verificationMethod).exists(publicKey => validateEncodedJwt(jwt, publicKey))
)
}

private def resolve(issuerDid: String)(didResolver: DidResolver): IO[String, DIDDocument] = {
didResolver
.resolve(issuerDid)
.flatMap(
_ match
case (didResolutionSucceeded: DIDResolutionSucceeded) =>
ZIO.succeed(didResolutionSucceeded.didDocument)
case (didResolutionFailed: DIDResolutionFailed) => ZIO.fail(didResolutionFailed.error.toString)
)
}

private def extractVerificationMethods(
didDocument: DIDDocument,
jwtAlgorithm: JwtAlgorithm
): IO[String, IndexedSeq[VerificationMethod]] = {
Validation
.fromPredicateWith("No PublicKey to validate against found")(
didDocument.verificationMethod.filter(verification => verification.`type` == jwtAlgorithm.name)
)(_.nonEmpty)
.toZIO
}

// TODO Implement other key types
def toPublicKey(verificationMethod: VerificationMethod): Option[PublicKey] = {
for {
publicKeyJwk <- verificationMethod.publicKeyJwk
curve <- publicKeyJwk.crv
x <- publicKeyJwk.x.map(Base64URL.from)
y <- publicKeyJwk.y.map(Base64URL.from)
d <- publicKeyJwk.d.map(Base64URL.from)
} yield new ECKey.Builder(Curve.parse(curve), x, y).d(d).build().toPublicKey
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -541,121 +541,42 @@ object JwtCredential {
def validateEncodedJwt(jwt: JWT, publicKey: PublicKey): Boolean =
JwtCirce.isValid(jwt.value, publicKey)

def validateEncodedJWT(jwt: JWT, verificationMethods: IndexedSeq[VerificationMethod]): Boolean = {
verificationMethods.exists(verificationMethod =>
toPublicKey(verificationMethod).exists(publicKey => validateEncodedJwt(jwt, publicKey))
)
}
def validateEncodedJWT(
jwt: JWT
)(didResolver: DidResolver): IO[String, Boolean] = {
val decodeJWT = ZIO
.fromTry(JwtCirce.decodeRawAll(jwt.value, JwtOptions(false, false, false)))
.mapError(_.getMessage)

val extractAlgorithm =
for {
decodedJwtTask <- decodeJWT
(header, _, _) = decodedJwtTask
algorithm <- Validation
.fromOptionWith("An algorithm must be specified in the header")(JwtCirce.parseHeader(header).algorithm)
.toZIO
} yield algorithm

val loadDidDocument =
for {
decodedJwtTask <- decodeJWT
(_, claim, _) = decodedJwtTask
decodedClaim <- ZIO.fromEither(decode[JwtCredentialPayload](claim).left.map(_.toString))
extractIssuerDid = decodedClaim.iss
resolvedDidDocument <- resolve(extractIssuerDid)(didResolver)
} yield resolvedDidDocument

for {
results <- loadDidDocument validatePar extractAlgorithm
(didDocument, algorithm) = results
verificationMethods <- extractVerificationMethods(didDocument, algorithm)
} yield validateEncodedJWT(jwt, verificationMethods)
JWTVerification.validateEncodedJwt(jwt)(didResolver: DidResolver)(claim =>
ZIO.fromEither(decode[JwtCredentialPayload](claim).left.map(_.toString))
)(_.iss)
}

def validateEncodedJWT(
def validateJwtSchema(
jwt: JWT
)(didResolver: DidResolver)(schemaResolver: SchemaResolver)(
)(schemaResolver: SchemaResolver)(
schemaToValidator: Json => Either[String, SchemaValidator]
): IO[String, Boolean] = {
val decodeJWT = ZIO
.fromTry(JwtCirce.decodeRawAll(jwt.value, JwtOptions(false, false, false)))
.mapError(_.getMessage)

val extractAlgorithm =
for {
decodedJwtTask <- decodeJWT
(header, _, _) = decodedJwtTask
algorithm <- Validation
.fromOptionWith("An algorithm must be specified in the header")(JwtCirce.parseHeader(header).algorithm)
.toZIO
} yield algorithm

val decodeClaim =
for {
decodedJwtTask <- decodeJWT
(_, claim, _) = decodedJwtTask
decodedClaim <- ZIO.fromEither(decode[JwtCredentialPayload](claim).left.map(_.toString))
} yield decodedClaim

val loadDidDocument =
for {
decodedClaim <- decodeClaim
extractIssuerDid = decodedClaim.iss
resolvedDidDocument <- resolve(extractIssuerDid)(didResolver)
} yield resolvedDidDocument

val validateCredentialSubject =
for {
decodedClaim <- decodeClaim
validatedCredential <- CredentialPayloadValidation.validateSchema(decodedClaim)(schemaResolver)(
schemaToValidator
)
} yield validatedCredential

for {
results <- loadDidDocument validate extractAlgorithm validate validateCredentialSubject
(didDocument, algorithm, _) = results
verificationMethods <- extractVerificationMethods(didDocument, algorithm)
} yield validateEncodedJWT(jwt, verificationMethods)
}

// TODO Implement other key types
def toPublicKey(verificationMethod: VerificationMethod): Option[PublicKey] = {
for {
publicKeyJwk <- verificationMethod.publicKeyJwk
curve <- publicKeyJwk.crv
x <- publicKeyJwk.x.map(Base64URL.from)
y <- publicKeyJwk.y.map(Base64URL.from)
d <- publicKeyJwk.d.map(Base64URL.from)
} yield new ECKey.Builder(Curve.parse(curve), x, y).d(d).build().toPublicKey
}

private def resolve(issuerDid: String)(didResolver: DidResolver): IO[String, DIDDocument] = {
didResolver
.resolve(issuerDid)
.flatMap(
_ match
case (didResolutionSucceeded: DIDResolutionSucceeded) =>
ZIO.succeed(didResolutionSucceeded.didDocument)
case (didResolutionFailed: DIDResolutionFailed) => ZIO.fail(didResolutionFailed.error.toString)
decodedJwtTask <- decodeJWT
(_, claim, _) = decodedJwtTask
decodedClaim <- ZIO.fromEither(decode[JwtCredentialPayload](claim).left.map(_.toString))
validatedCredential <- CredentialPayloadValidation.validateSchema(decodedClaim)(schemaResolver)(
schemaToValidator
)
} yield true
}

private def extractVerificationMethods(
didDocument: DIDDocument,
jwtAlgorithm: JwtAlgorithm
): IO[String, IndexedSeq[VerificationMethod]] = {
Validation
.fromPredicateWith("No PublicKey to validate against found")(
didDocument.verificationMethod.filter(verification => verification.`type` == jwtAlgorithm.name)
)(_.nonEmpty)
.toZIO
def validateSchemaAndSignature(
jwt: JWT
)(didResolver: DidResolver)(schemaResolver: SchemaResolver)(
schemaToValidator: Json => Either[String, SchemaValidator]
): IO[String, Boolean] = {
for {
validatedJwtSchema <- validateJwtSchema(jwt)(schemaResolver)(schemaToValidator)
validateJwtSignature <- validateEncodedJWT(jwt)(didResolver)
} yield validatedJwtSchema && validateJwtSignature
}

def verifyDates(jwt: JWT, leeway: TemporalAmount)(implicit clock: Clock): Validation[String, Unit] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.circe.generic.auto.*
import io.circe.parser.decode
import io.circe.syntax.*
import pdi.jwt.{Jwt, JwtCirce, JwtOptions}
import zio.{IO, ZIO}
import zio.prelude.*

import java.security.{KeyPairGenerator, PublicKey}
Expand Down Expand Up @@ -296,7 +297,15 @@ object JwtPresentation {
}

def validateEncodedJwt(jwt: JWT, publicKey: PublicKey): Boolean =
JwtCirce.isValid(jwt.value, publicKey)
JWTVerification.validateEncodedJwt(jwt, publicKey)

def validateEncodedJWT(
jwt: JWT
)(didResolver: DidResolver): IO[String, Boolean] = {
JWTVerification.validateEncodedJwt(jwt)(didResolver: DidResolver)(claim =>
ZIO.fromEither(decode[JwtPresentationPayload](claim).left.map(_.toString))
)(_.iss)
}

def verifyDates(jwt: JWT, leeway: TemporalAmount)(implicit clock: Clock): Validation[String, Unit] = {
val now = clock.instant()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,6 @@ import scala.collection.immutable.Set

object JwtCredentialDIDDocumentValidationDemo extends ZIOAppDefault {
def run =

def toJWKFormat(holderJwk: ECKey): JsonWebKey = {
JsonWebKey(
kty = "EC",
crv = Some(holderJwk.getCurve.getName),
x = Some(holderJwk.getX.toJSONString),
y = Some(holderJwk.getY.toJSONString),
d = Some(holderJwk.getD.toJSONString)
)
}

def createUser(did: DID) = {
val keyGen = KeyPairGenerator.getInstance("EC")
keyGen.initialize(Curve.P_256.toECParameterSpec)
Expand Down Expand Up @@ -72,7 +61,7 @@ object JwtCredentialDIDDocumentValidationDemo extends ZIOAppDefault {

println("")
println("==================")
println("Create Issuer2")
println("Create Issuer3")
println("==================")
val (issuer3, issuer3Jwk) =
createUser(DID("did:issuer3:MDP8AsFhHzhwUvGNuYkX7T"))
Expand Down Expand Up @@ -219,7 +208,7 @@ object JwtCredentialDIDDocumentValidationDemo extends ZIOAppDefault {
println("Validate JWT Credential Using DID Document of the Issuer of the Credential")
println("==================")
val validator =
JwtCredential.validateEncodedJWT(encodedJwt)(DidResolverTest())(schemaResolved)(
JwtCredential.validateSchemaAndSignature(encodedJwt)(DidResolverTest())(schemaResolved)(
PlaceholderSchemaValidator.fromSchema
)

Expand Down
Loading

0 comments on commit 258c6c0

Please sign in to comment.