• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hyperledger / identus-cloud-agent / 10807284826

11 Sep 2024 07:46AM UTC coverage: 48.663% (+0.1%) from 48.554%
10807284826

Pull #1332

patlo-iog
test: make test work again

Signed-off-by: Pat Losoponkul <pat.losoponkul@iohk.io>
Pull Request #1332: feat: presentation_submission validation logic [WIP]

92 of 134 new or added lines in 5 files covered. (68.66%)

248 existing lines in 65 files now uncovered.

7478 of 15367 relevant lines covered (48.66%)

0.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.32
/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerification.scala
1
package org.hyperledger.identus.pollux.vc.jwt
2

3
import com.nimbusds.jose.crypto.{ECDSAVerifier, Ed25519Verifier}
4
import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton
5
import com.nimbusds.jose.jwk.*
6
import com.nimbusds.jose.util.Base64URL
7
import com.nimbusds.jose.JWSVerifier
8
import com.nimbusds.jwt.SignedJWT
9
import io.circe
10
import io.circe.generic.auto.*
11
import org.hyperledger.identus.castor.core.model.did.VerificationRelationship
12
import org.hyperledger.identus.shared.crypto.Ed25519PublicKey
13
import pdi.jwt.*
14
import zio.*
15
import zio.prelude.*
16

17
import java.security.interfaces.{ECPublicKey, EdECPublicKey}
18
import java.security.PublicKey
19
import scala.util.{Failure, Success, Try}
20

21
object JWTVerification {
22
  // JWT algo <-> publicKey type mapping reference
23
  // https://github.com/decentralized-identity/did-jwt/blob/8b3655097a1382934cabdf774d580e6731a636b1/src/JWT.ts#L146
24
  val SUPPORT_PUBLIC_KEY_TYPES: Map[String, Set[String]] = Map(
1✔
25
    "ES256K" -> Set("EcdsaSecp256k1VerificationKey2019", "JsonWebKey2020"),
1✔
26
    "EdDSA" -> Set("Ed25519VerificationKey2020", "JsonWebKey2020"),
1✔
27
    // Add support for other key types here
28
  )
29

30
  def validateAlgorithm(jwt: JWT): Validation[String, Unit] = {
×
31
    val decodedJWT =
32
      Validation
33
        .fromTry(JwtCirce.decodeRawAll(jwt.value, JwtOptions(false, false, false)))
×
34
        .mapError(_.getMessage)
×
35
    for {
×
36
      decodedJwtTask <- decodedJWT
×
37
      (header, _, _) = decodedJwtTask
×
38
      algorithm <- Validation
×
39
        .fromOptionWith("An algorithm must be specified in the header")(JwtCirce.parseHeader(header).algorithm)
×
40
      result <-
×
41
        Validation
42
          .fromPredicateWith("Algorithm Not Supported")(
43
            SUPPORT_PUBLIC_KEY_TYPES.getOrElse(algorithm.name, Set.empty)
×
44
          )(_.nonEmpty)
×
45
          .flatMap(_ => Validation.unit)
×
46

47
    } yield result
48
  }
49

50
  def validateIssuer[T](jwt: JWT)(didResolver: DidResolver)(
×
51
      decoder: String => Validation[String, T]
52
  )(issuerDidExtractor: T => String): IO[String, Validation[String, DIDDocument]] = {
53
    val decodedJWT =
54
      Validation
55
        .fromTry(JwtCirce.decodeRawAll(jwt.value, JwtOptions(false, false, false)))
×
56
        .mapError(_.getMessage)
×
57

58
    val claim: Validation[String, String] =
59
      for {
×
60
        decodedJwtTask <- decodedJWT
×
61
        (_, claim, _) = decodedJwtTask
×
62
      } yield claim
×
63

64
    validateIssuerFromClaim(claim)(didResolver)(decoder)(issuerDidExtractor)
×
65
  }
66

67
  def validateIssuerFromClaim[T](validatedClaim: Validation[String, String])(didResolver: DidResolver)(
1✔
68
      decoder: String => Validation[String, T]
69
  )(issuerDidExtractor: T => String): IO[String, Validation[String, DIDDocument]] = {
70
    val validatedIssuerDid: Validation[String, String] =
71
      for {
1✔
72
        claim <- validatedClaim
73
        decodedClaim <- decoder(claim)
1✔
74
        extractIssuerDid = issuerDidExtractor(decodedClaim)
1✔
75
      } yield extractIssuerDid
1✔
76

77
    val loadDidDocument =
78
      ValidationUtils
1✔
79
        .foreach(
80
          validatedIssuerDid
81
            .map(validIssuerDid => resolve(validIssuerDid)(didResolver))
1✔
82
        )(identity)
1✔
83
        .map(b => b.flatten)
1✔
84

85
    loadDidDocument
86
  }
87

88
  def validateIssuerFromKeyId(
×
89
      extractedDID: Validation[String, String]
90
  )(didResolver: DidResolver): IO[String, Validation[String, DIDDocument]] = {
91
    val loadDidDocument =
92
      ValidationUtils
×
93
        .foreach(extractedDID.map(validIssuerDid => resolve(validIssuerDid)(didResolver)))(identity)
×
94
        .map(b => b.flatten)
×
95

96
    loadDidDocument
97
  }
98

UNCOV
99
  def validateEncodedJwt[T](jwt: JWT, proofPurpose: Option[VerificationRelationship] = None)(
×
100
      didResolver: DidResolver
101
  )(decoder: String => Validation[String, T])(issuerDidExtractor: T => String): IO[String, Validation[String, Unit]] = {
102
    val decodedJWT = Validation
103
      .fromTry(JwtCirce.decodeRawAll(jwt.value, JwtOptions(false, false, false)))
1✔
104
      .mapError(_.getMessage)
1✔
105

106
    val extractAlgorithm: Validation[String, JwtAlgorithm] =
107
      for {
1✔
108
        decodedJwtTask <- decodedJWT
1✔
109
        (header, _, _) = decodedJwtTask
1✔
110
        algorithm <- Validation
1✔
111
          .fromOptionWith("An algorithm must be specified in the header")(JwtCirce.parseHeader(header).algorithm)
1✔
112
      } yield algorithm
113

114
    val claim: Validation[String, String] =
115
      for {
1✔
116
        decodedJwtTask <- decodedJWT
1✔
117
        (_, claim, _) = decodedJwtTask
1✔
118
      } yield claim
1✔
119

120
    val loadDidDocument = validateIssuerFromClaim(claim)(didResolver)(decoder)(issuerDidExtractor)
1✔
121

122
    loadDidDocument
1✔
123
      .map(validatedDidDocument => {
124
        for {
1✔
125
          results <- Validation.validateWith(validatedDidDocument, extractAlgorithm)((didDocument, algorithm) =>
1✔
126
            (didDocument, algorithm)
127
          )
128
          (didDocument, algorithm) = results
1✔
129
          verificationMethods <- extractVerificationMethods(didDocument, algorithm, proofPurpose)
1✔
130
          validatedJwt <- validateEncodedJwt(jwt, verificationMethods)
1✔
131
        } yield validatedJwt
132
      })
133
  }
134

135
  def toECDSAVerifier(publicKey: PublicKey): JWSVerifier = {
1✔
136
    val verifier: JWSVerifier = publicKey match {
137
      case key: ECPublicKey => ECDSAVerifier(key)
1✔
138
      case key: EdECPublicKey =>
139
        val octetKeyPair = Ed25519PublicKey.toPublicKeyOctetKeyPair(key)
×
140
        Ed25519Verifier(octetKeyPair)
×
141
      case key => throw Exception(s"unsupported public-key type: ${key.getClass.getCanonicalName}")
×
142
    }
143
    verifier.getJCAContext.setProvider(BouncyCastleProviderSingleton.getInstance)
1✔
144
    verifier
145
  }
146

147
  def validateEncodedJwt(jwt: JWT, publicKey: PublicKey): Validation[String, Unit] = {
1✔
148
    Try {
1✔
149
      val parsedJwt = SignedJWT.parse(jwt.value)
1✔
150
      // TODO Implement other key types
151
      val verifier = toECDSAVerifier(publicKey)
1✔
152
      parsedJwt.verify(verifier)
1✔
153
    } match {
154
      case Failure(exception) => Validation.fail(s"Jwt[$jwt] verification pre-process failed: ${exception.getMessage}")
×
155
      case Success(isValid) =>
1✔
156
        if isValid then Validation.unit else Validation.fail(s"Jwt[$jwt] not singed by $publicKey")
1✔
157
    }
158
  }
159

160
  def validateEncodedJwt(jwt: JWT, verificationMethods: IndexedSeq[VerificationMethod]): Validation[String, Unit] = {
1✔
161
    verificationMethods
162
      .map(verificationMethod => {
1✔
163
        for {
1✔
164
          publicKey <- extractPublicKey(verificationMethod)
1✔
165
          signatureValidation <- validateEncodedJwt(jwt, publicKey)
1✔
166
        } yield signatureValidation
167
      })
UNCOV
168
      .reduce((v1, v2) => v1.orElse(v2))
×
169
  }
170

171
  def resolve(issuerDid: String)(didResolver: DidResolver): IO[String, Validation[String, DIDDocument]] = {
1✔
172
    didResolver
1✔
173
      .resolve(issuerDid)
1✔
174
      .map(
175
        _ match
176
          case (didResolutionSucceeded: DIDResolutionSucceeded) =>
1✔
177
            Validation.succeed(didResolutionSucceeded.didDocument)
1✔
178
          case (didResolutionFailed: DIDResolutionFailed) => Validation.fail(didResolutionFailed.error.toString)
1✔
179
      )
180
  }
181

182
  private def extractVerificationMethods(
1✔
183
      didDocument: DIDDocument,
184
      jwtAlgorithm: JwtAlgorithm,
185
      proofPurpose: Option[VerificationRelationship]
186
  ): Validation[String, IndexedSeq[VerificationMethod]] = {
187
    val publicKeysToCheck: Vector[VerificationMethodOrRef] = proofPurpose.fold(didDocument.verificationMethod) {
1✔
188
      case VerificationRelationship.Authentication       => didDocument.authentication
1✔
189
      case VerificationRelationship.AssertionMethod      => didDocument.assertionMethod
1✔
190
      case VerificationRelationship.KeyAgreement         => didDocument.keyAgreement
×
191
      case VerificationRelationship.CapabilityInvocation => didDocument.capabilityInvocation
×
192
      case VerificationRelationship.CapabilityDelegation => didDocument.capabilityDelegation
×
193
    }
194
    // FIXME
195
    // To be fully compliant, key extraction MUST follow the referenced URI which
196
    // might not be in the same DID document. For now, this only performs lookup within
197
    // the same DID document which is what Prism DID currently support.
198

199
    val dereferencedKeysToCheck = dereferenceVerificationMethods(didDocument, publicKeysToCheck)
1✔
200

201
    Validation
202
      .fromPredicateWith("No PublicKey to validate against found")(
203
        dereferencedKeysToCheck.filter { verification =>
1✔
204
          val supportPublicKeys = SUPPORT_PUBLIC_KEY_TYPES.getOrElse(jwtAlgorithm.name, Set.empty)
1✔
205
          supportPublicKeys.contains(verification.`type`)
1✔
206
        }
207
      )(_.nonEmpty)
1✔
208
  }
209

210
  def dereferenceVerificationMethods(
1✔
211
      didDocument: DIDDocument,
212
      methods: Vector[VerificationMethodOrRef]
213
  ): Vector[VerificationMethod] = {
214
    val (referenced, embedded) = methods.partitionMap[String, VerificationMethod] {
1✔
215
      case uri: String            => Left(uri)
1✔
216
      case pk: VerificationMethod => Right(pk)
1✔
217
    }
218
    val keySet = referenced.toSet
1✔
219
    embedded ++ didDocument.verificationMethod.filter(pk => keySet.contains(pk.id))
1✔
220
  }
221

222
  // TODO Implement other key types
223
  def extractPublicKey(verificationMethod: VerificationMethod): Validation[String, PublicKey] = {
1✔
224
    val maybePublicKey =
225
      for {
1✔
226
        publicKeyJwk <- verificationMethod.publicKeyJwk
227
        curve <- publicKeyJwk.crv
1✔
228

229
        key <- curve match
1✔
230
          case "Ed25519" =>
×
231
            publicKeyJwk.x.map(Base64URL.from).map { base64 =>
×
232
              Ed25519PublicKey.toJavaEd25519PublicKey(base64.decode())
×
233
            }
234
          case "secp256k1" =>
1✔
235
            for {
1✔
236
              x <- publicKeyJwk.x.map(Base64URL.from)
1✔
237
              y <- publicKeyJwk.y.map(Base64URL.from)
1✔
238
            } yield new ECKey.Builder(Curve.parse(curve), x, y).build().toPublicKey
1✔
239

240
      } yield key
241
    Validation.fromOptionWith("Unable to parse Public Key")(maybePublicKey)
1✔
242
  }
243

244
  def extractJwtHeader(jwt: JWT): Validation[String, JwtHeader] = {
×
245
    import io.circe.parser._
246
    import io.circe.Json
247

248
    def parseHeaderUnsafe(json: Json): Either[String, JwtHeader] =
×
249
      Try(JwtCirce.parseHeader(json.noSpaces)).toEither.left.map(_.getMessage)
×
250

251
    def decodeJwtHeader(header: String): Validation[String, JwtHeader] = {
×
252
      val eitherResult = for {
×
253
        json <- parse(header).left.map(_.getMessage)
×
254
        jwtHeader <- parseHeaderUnsafe(json)
×
255
      } yield jwtHeader
256
      Validation.fromEither(eitherResult)
×
257
    }
258
    for {
×
259
      decodedJwt <- Validation
×
260
        .fromTry(JwtCirce.decodeRawAll(jwt.value, JwtOptions(false, false, false)))
×
261
        .mapError(_.getMessage)
×
262
      (header, _, _) = decodedJwt
×
263
      jwtHeader <- decodeJwtHeader(header)
×
264
    } yield jwtHeader
265
  }
266
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc