Skip to content

Commit

Permalink
feat(pollux): check verification status on presentation verification (#…
Browse files Browse the repository at this point in the history
…917)

Signed-off-by: Shota Jolbordi <[email protected]>
  • Loading branch information
shotexa committed Mar 18, 2024
1 parent 33ccf7a commit 8b97089
Show file tree
Hide file tree
Showing 20 changed files with 411 additions and 52 deletions.
28 changes: 16 additions & 12 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ lazy val V = new {
val zioMetricsConnector = "2.1.0"
val zioMock = "1.0.0-RC11"
val mockito = "3.2.16.0"
val monocle = "3.1.0"
val monocle = "3.1.0"

// https://mvnrepository.com/artifact/io.circe/circe-core
val circe = "0.14.6"
Expand Down Expand Up @@ -107,6 +107,7 @@ lazy val D = new {
val tapirPrometheusMetrics: ModuleID = "com.softwaremill.sttp.tapir" %% "tapir-prometheus-metrics" % V.tapir
val micrometer: ModuleID = "io.micrometer" % "micrometer-registry-prometheus" % V.micrometer
val micrometerPrometheusRegistry = "io.micrometer" % "micrometer-core" % V.micrometer
val scalaUri = "io.lemonlabs" %% "scala-uri" % V.scalaUri

val zioConfig: ModuleID = "dev.zio" %% "zio-config" % V.zioConfig
val zioConfigMagnolia: ModuleID = "dev.zio" %% "zio-config-magnolia" % V.zioConfig
Expand Down Expand Up @@ -154,7 +155,7 @@ lazy val D = new {
val zioTestMagnolia: ModuleID = "dev.zio" %% "zio-test-magnolia" % V.zio % Test
val zioMock: ModuleID = "dev.zio" %% "zio-mock" % V.zioMock
val mockito: ModuleID = "org.scalatestplus" %% "mockito-4-11" % V.mockito % Test
val monocle: ModuleID = "dev.optics" %% "monocle-core" % V.monocle % Test
val monocle: ModuleID = "dev.optics" %% "monocle-core" % V.monocle % Test
val monocleMacro: ModuleID = "dev.optics" %% "monocle-macro" % V.monocle % Test

// LIST of Dependencies
Expand All @@ -168,12 +169,17 @@ lazy val D_Shared = new {
D.typesafeConfig,
D.scalaPbGrpc,
D.zio,
D.zioHttp,
D.scalaUri,
// FIXME: split shared DB stuff as subproject?
D.doobieHikari,
D.doobiePostgres,
D.zioCatsInterop,
D.jsonCanonicalization,
D.scodecBits
D.scodecBits,
D.circeCore,
D.circeGeneric,
D.circeParser,
)
}

Expand Down Expand Up @@ -210,8 +216,6 @@ lazy val D_Connect = new {

lazy val D_Castor = new {

val scalaUri = "io.lemonlabs" %% "scala-uri" % V.scalaUri

// We have to exclude bouncycastle since for some reason bitcoinj depends on bouncycastle jdk15to18
// (i.e. JDK 1.5 to 1.8), but we are using JDK 11
val prismCrypto = "io.iohk.atala" % "prism-crypto-jvm" % V.prismSdk excludeAll
Expand All @@ -228,11 +232,7 @@ lazy val D_Castor = new {
D.zioMock,
D.zioTestSbt,
D.zioTestMagnolia,
D.circeCore,
D.circeGeneric,
D.circeParser,
prismIdentity,
scalaUri
)

// Project Dependencies
Expand Down Expand Up @@ -316,9 +316,8 @@ lazy val D_Pollux_VC_JWT = new {

// Dependency Modules
val zioDependencies: Seq[ModuleID] = Seq(zio, zioPrelude, zioTest, zioTestSbt, zioTestMagnolia)
val circeDependencies: Seq[ModuleID] = Seq(D.circeCore, D.circeGeneric, D.circeParser)
val baseDependencies: Seq[ModuleID] =
circeDependencies ++ zioDependencies :+ D.jwtCirce :+ circeJsonSchema :+ networkntJsonSchemaValidator :+ D.nimbusJwt :+ scalaTest
zioDependencies :+ D.jwtCirce :+ circeJsonSchema :+ networkntJsonSchemaValidator :+ D.nimbusJwt :+ scalaTest

// Project Dependencies
lazy val polluxVcJwtDependencies: Seq[ModuleID] = baseDependencies
Expand Down Expand Up @@ -412,7 +411,12 @@ lazy val D_PrismAgent = new {
lazy val iamDependencies: Seq[ModuleID] = Seq(keycloakAuthz, D.jwtCirce)

lazy val serverDependencies: Seq[ModuleID] =
baseDependencies ++ tapirDependencies ++ postgresDependencies ++ Seq(D.zioMock, D.mockito, D.monocle, D.monocleMacro)
baseDependencies ++ tapirDependencies ++ postgresDependencies ++ Seq(
D.zioMock,
D.mockito,
D.monocle,
D.monocleMacro
)
}

publish / skip := true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ object PresentationError {
final case class HolderBindingError(msg: String) extends PresentationError
object MissingCredential extends PresentationError
object MissingCredentialFormat extends PresentationError

final case class UnsupportedCredentialFormat(vcFormat: String) extends PresentationError
}
Original file line number Diff line number Diff line change
Expand Up @@ -1246,7 +1246,7 @@ private class CredentialServiceImpl(
verifyDates = false,
leeway = Duration.Zero
)
)(didResolver)(clock)
)(didResolver, (_: String) => ZIO.succeed(""))(clock)
.mapError(errors => CredentialRequestValidationError(s"JWT presentation verification failed: $errors"))

result <- verificationResult match
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,22 @@ trait Signer {

}

class ES256Signer(privateKey: PrivateKey) extends Signer with EddsaJcs2022ProofGenerator {
class ES256Signer(privateKey: PrivateKey) extends Signer {
val algorithm: JwtECDSAAlgorithm = JwtAlgorithm.ES256
private val provider = BouncyCastleProviderSingleton.getInstance
Security.addProvider(provider)

override def encode(claim: Json): JWT = JWT(JwtCirce.encode(claim, privateKey, algorithm))

override def generateProofForJson(payload: Json, pk: PublicKey): Task[Proof] = {
generateProof(payload, privateKey, pk)
EddsaJcs2022ProofGenerator.generateProof(payload, privateKey, pk)
}

}

// works with java 7, 8, 11 & bouncycastle provider
// https://connect2id.com/products/nimbus-jose-jwt/jca-algorithm-support#alg-support-table
class ES256KSigner(privateKey: PrivateKey) extends Signer with EddsaJcs2022ProofGenerator {
class ES256KSigner(privateKey: PrivateKey) extends Signer {
lazy val signer: ECDSASigner = {
val ecdsaSigner = ECDSASigner(privateKey, Curve.SECP256K1)
val bouncyCastleProvider = BouncyCastleProviderSingleton.getInstance
Expand All @@ -61,7 +61,7 @@ class ES256KSigner(privateKey: PrivateKey) extends Signer with EddsaJcs2022Proof
}

override def generateProofForJson(payload: Json, pk: PublicKey): Task[Proof] = {
generateProof(payload, privateKey, pk)
EddsaJcs2022ProofGenerator.generateProof(payload, privateKey, pk)
}

override def encode(claim: Json): JWT = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
package io.iohk.atala.pollux.vc.jwt

import io.circe.*
import io.iohk.atala.shared.utils.Base64Utils
import scodec.bits.ByteVector

case class MultiBaseString(header: MultiBaseString.Header, data: String) {
def toMultiBaseString: String = s"${header.value}$data"

def getBytes: Either[String, Array[Byte]] = header match {
case MultiBaseString.Header.Base64Url => Right(Base64Utils.decodeURL(data))
case MultiBaseString.Header.Base58Btc =>
ByteVector.fromBase58(data).map(_.toArray).toRight(s"Invalid base58 string: $data")
}
}

object MultiBaseString {
Expand All @@ -12,6 +20,14 @@ object MultiBaseString {
case Base58Btc extends Header('z')
}

def fromString(str: String): Either[String, MultiBaseString] = {
val header = Header.fromValue(str.head)
header match {
case Some(value) => Right(MultiBaseString(value, str.tail))
case None => Left(s"$str - is not a multi base string")
}
}

object Header {
def fromValue(value: Char): Option[Header] = value match {
case 'u' => Some(Header.Base64Url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package io.iohk.atala.pollux.vc.jwt
import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton
import io.circe.*
import io.circe.syntax.*

import cats.implicits.*
import java.time.{Instant, ZoneOffset}
import zio.*
import io.iohk.atala.shared.utils.Json as JsonUtils
import io.iohk.atala.shared.utils.Base64Utils
import scodec.bits.ByteVector
import scala.util.Try
import java.security.*
import java.security.spec.X509EncodedKeySpec

sealed trait Proof {
val id: Option[String] = None
Expand All @@ -24,13 +26,31 @@ sealed trait Proof {
val nonce: Option[String] = None
}

trait EddsaJcs2022ProofGenerator {
object Proof {
given decodeProof: Decoder[Proof] = new Decoder[Proof] {
final def apply(c: HCursor): Decoder.Result[Proof] = {
val decoders: List[Decoder[Proof]] = List(
Decoder[EddsaJcs2022Proof].widen
// Note: Add another proof types here when available
)

decoders.foldLeft(
Left[DecodingFailure, Proof](DecodingFailure("Cannot decode as Proof", c.history)): Decoder.Result[Proof]
) { (acc, decoder) =>
acc.orElse(decoder.tryDecode(c))
}
}
}
}

object EddsaJcs2022ProofGenerator {
private val provider = BouncyCastleProviderSingleton.getInstance
def generateProof(payload: Json, sk: PrivateKey, pk: PublicKey): Task[EddsaJcs2022Proof] = {
for {
canonicalizedJsonString <- ZIO.fromEither(JsonUtils.canonicalizeToJcs(payload.spaces2))
canonicalizedJson <- ZIO.fromEither(parser.parse(canonicalizedJsonString))
signature = sign(sk, canonicalizedJson.noSpaces.getBytes)
dataToSign = canonicalizedJson.noSpaces.getBytes
signature = sign(sk, dataToSign)
base58BtsEncodedSignature = MultiBaseString(
header = MultiBaseString.Header.Base58Btc,
data = ByteVector.view(signature).toBase58
Expand All @@ -50,13 +70,48 @@ trait EddsaJcs2022ProofGenerator {
)
}

def verifyProof(payload: Json, proofValue: String, pk: MultiKey): Task[Boolean] = {

val res = for {
canonicalizedJsonString <- ZIO
.fromEither(JsonUtils.canonicalizeToJcs(payload.spaces2))
.mapError(_.getMessage)
canonicalizedJson <- ZIO
.fromEither(parser.parse(canonicalizedJsonString))
.mapError(_.getMessage)
dataToVerify = canonicalizedJson.noSpaces.getBytes
signature <- ZIO.fromEither(MultiBaseString.fromString(proofValue).flatMap(_.getBytes))
publicKeyBytes <- ZIO.fromEither(
pk.publicKeyMultibase.toRight("No public key provided inside MultiKey").flatMap(_.getBytes)
)
javaPublicKey <- ZIO.fromEither(recoverPublicKey(publicKeyBytes))
isValid = verify(javaPublicKey, signature, dataToVerify)

} yield isValid

res.mapError(e => Throwable(e))
}

private def sign(privateKey: PrivateKey, data: Array[Byte]): Array[Byte] = {

val signer = Signature.getInstance("SHA256withECDSA", provider)
signer.initSign(privateKey)
signer.update(data)
signer.sign()
}

private def recoverPublicKey(pkBytes: Array[Byte]): Either[String, PublicKey] = {
val keyFactory = KeyFactory.getInstance("EC", provider)
val x509KeySpec = X509EncodedKeySpec(pkBytes)
Try(keyFactory.generatePublic(x509KeySpec)).toEither.left.map(_.getMessage)
}

private def verify(publicKey: PublicKey, signature: Array[Byte], data: Array[Byte]): Boolean = {
val verifier = Signature.getInstance("SHA256withECDSA", provider)
verifier.initVerify(publicKey)
verifier.update(data)
verifier.verify(signature)
}
}
case class EddsaJcs2022Proof(proofValue: String, verificationMethod: String, maybeCreated: Option[Instant])
extends Proof {
Expand Down
Loading

0 comments on commit 8b97089

Please sign in to comment.