Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pollux): [ATL-2640] JWT Presentation Signature Verification Using DidResolver #212

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a duplication of 34-35 and 43-44 lines where the document is decoded from JWT.
does it make sense to separate these stages: decoding, raw-parsing, validation, verification, etc? ...instead of doing everything in one method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I will be doing refactoring as I go.

(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