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

input-output-hk / atala-prism-building-blocks / 8795121910

23 Apr 2024 04:45AM UTC coverage: 31.908% (+0.4%) from 31.528%
8795121910

Pull #975

CryptoKnightIOG
feat: VC Verification test coverage (#972)

Signed-off-by: Bassam Riman <bassam.riman@iohk.io>
Pull Request #975: feat: Vc Verification Api

104 of 281 new or added lines in 15 files covered. (37.01%)

379 existing lines in 106 files now uncovered.

4619 of 14476 relevant lines covered (31.91%)

0.32 hits per line

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

30.77
/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/PresentationServiceImpl.scala
1
package io.iohk.atala.pollux.core.service
2

3
import cats.*
4
import cats.implicits.*
5
import io.circe.*
6
import io.circe.parser.*
7
import io.circe.syntax.*
8
import io.iohk.atala.mercury.model.*
9
import io.iohk.atala.mercury.protocol.issuecredential.IssueCredentialIssuedFormat
10
import io.iohk.atala.mercury.protocol.presentproof.*
11
import io.iohk.atala.pollux.anoncreds.*
12
import io.iohk.atala.pollux.core.model.*
13
import io.iohk.atala.pollux.core.model.error.PresentationError
14
import io.iohk.atala.pollux.core.model.error.PresentationError.*
15
import io.iohk.atala.pollux.core.model.presentation.*
16
import io.iohk.atala.pollux.core.model.schema.`type`.anoncred.AnoncredSchemaSerDesV1
17
import io.iohk.atala.pollux.core.repository.{CredentialRepository, PresentationRepository}
18
import io.iohk.atala.pollux.core.service.serdes.*
19
import io.iohk.atala.pollux.vc.jwt.*
20
import io.iohk.atala.shared.models.WalletAccessContext
21
import io.iohk.atala.shared.utils.aspects.CustomMetricsAspect
22
import zio.*
23

24
import java.net.URI
25
import java.rmi.UnexpectedException
26
import java.time.Instant
27
import java.util as ju
28
import java.util.{UUID, Base64 as JBase64}
29
import scala.util.Try
30

31
private class PresentationServiceImpl(
32
    uriDereferencer: URIDereferencer,
33
    linkSecretService: LinkSecretService,
34
    presentationRepository: PresentationRepository,
35
    credentialRepository: CredentialRepository,
1✔
36
    maxRetries: Int = 5, // TODO move to config
37
) extends PresentationService {
38

39
  import PresentationRecord.*
40

1✔
41
  override def markPresentationGenerated(
42
      recordId: DidCommID,
43
      presentation: Presentation
44
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
1✔
45
    for {
×
46
      record <- getRecordWithState(recordId, ProtocolState.PresentationPending)
1✔
47
      count <- presentationRepository
×
48
        .updateWithPresentation(recordId, presentation, ProtocolState.PresentationGenerated)
×
49
        .mapError(RepositoryError.apply) @@ CustomMetricsAspect.endRecordingTime(
×
50
        s"${record.id}_present_proof_flow_prover_presentation_pending_to_generated_ms_gauge",
51
        "present_proof_flow_prover_presentation_pending_to_generated_ms_gauge"
×
52
      ) @@ CustomMetricsAspect.startRecordingTime(
×
53
        s"${record.id}_present_proof_flow_prover_presentation_generated_to_sent_ms_gauge"
54
      )
1✔
55
      _ <- count match
×
56
        case 1 => ZIO.succeed(())
×
57
        case _ => ZIO.fail(RecordIdNotFound(recordId))
×
58
      record <- presentationRepository
×
59
        .getPresentationRecord(recordId)
60
        .mapError(RepositoryError.apply)
61
        .flatMap {
×
62
          case None        => ZIO.fail(RecordIdNotFound(record.id))
×
63
          case Some(value) => ZIO.succeed(value)
64
        }
65
    } yield record
66
  }
67

1✔
68
  override def createJwtPresentationPayloadFromRecord(
69
      recordId: DidCommID,
70
      prover: Issuer,
71
      issuanceDate: Instant
72
  ): ZIO[WalletAccessContext, PresentationError, PresentationPayload] = {
73

1✔
74
    for {
×
75
      maybeRecord <- presentationRepository
×
76
        .getPresentationRecord(recordId)
UNCOV
77
        .mapError(RepositoryError.apply)
×
78
      record <- ZIO
79
        .fromOption(maybeRecord)
80
        .mapError(_ => RecordIdNotFound(recordId))
×
81
      credentialsToUse <- ZIO
82
        .fromOption(record.credentialsToUse)
×
83
        .mapError(_ => InvalidFlowStateError(s"No request found for this record: $recordId"))
×
84
      requestPresentation <- ZIO
85
        .fromOption(record.requestPresentationData)
×
86
        .mapError(_ => InvalidFlowStateError(s"RequestPresentation not found: $recordId"))
1✔
87
      issuedValidCredentials <- credentialRepository
×
88
        .getValidIssuedCredentials(credentialsToUse.map(DidCommID(_)))
89
        .mapError(RepositoryError.apply)
×
UNCOV
90
      signedCredentials = issuedValidCredentials.flatMap(_.issuedCredentialRaw)
×
91
      issuedCredentials <- ZIO.fromEither(
×
92
        Either.cond(
×
93
          signedCredentials.nonEmpty,
94
          signedCredentials,
95
          PresentationError.IssuedCredentialNotFoundError(
×
96
            new Throwable("No matching issued credentials found in prover db")
97
          )
98
        )
99
      )
100

1✔
101
      presentationPayload <- createJwtPresentationPayloadFromCredential(
102
        issuedCredentials,
103
        requestPresentation,
104
        prover
105
      )
106
    } yield presentationPayload
107
  }
108

1✔
109
  override def createAnoncredPresentationPayloadFromRecord(
110
      recordId: DidCommID,
111
      anoncredCredentialProof: AnoncredCredentialProofsV1,
112
      issuanceDate: Instant
113
  ): ZIO[WalletAccessContext, PresentationError, AnoncredPresentation] = {
114

1✔
115
    for {
×
116
      maybeRecord <- presentationRepository
×
117
        .getPresentationRecord(recordId)
UNCOV
118
        .mapError(RepositoryError.apply)
×
119
      record <- ZIO
120
        .fromOption(maybeRecord)
121
        .mapError(_ => RecordIdNotFound(recordId))
1✔
122
      requestPresentation <- ZIO
123
        .fromOption(record.requestPresentationData)
×
124
        .mapError(_ => InvalidFlowStateError(s"RequestPresentation not found: $recordId"))
1✔
125
      issuedValidCredentials <-
×
126
        credentialRepository
×
127
          .getValidAnoncredIssuedCredentials(
×
128
            anoncredCredentialProof.credentialProofs.map(credentialProof => DidCommID(credentialProof.credential))
129
          )
UNCOV
130
          .mapError(RepositoryError.apply)
×
131
      issuedCredentials <- ZIO.fromEither(
×
132
        Either.cond(
×
133
          issuedValidCredentials.nonEmpty,
134
          issuedValidCredentials,
135
          PresentationError.IssuedCredentialNotFoundError(
×
136
            new Throwable("No matching issued credentials found in prover db")
137
          )
138
        )
UNCOV
139
      )
×
140
      presentationPayload <- createAnoncredPresentationPayloadFromCredential(
141
        issuedCredentials,
×
142
        issuedValidCredentials.flatMap(_.schemaUri),
×
143
        issuedValidCredentials.flatMap(_.credentialDefinitionUri),
144
        requestPresentation,
145
        anoncredCredentialProof.credentialProofs
146
      )
147
    } yield presentationPayload
148
  }
149

1✔
150
  def createAnoncredPresentation(
151
      requestPresentation: RequestPresentation,
152
      recordId: DidCommID,
153
      anoncredCredentialProof: AnoncredCredentialProofsV1,
154
      issuanceDate: Instant
155
  ): ZIO[WalletAccessContext, PresentationError, Presentation] = {
1✔
156
    for {
157
      presentationPayload <-
×
158
        createAnoncredPresentationPayloadFromRecord(
159
          recordId,
160
          anoncredCredentialProof,
161
          issuanceDate
162
        )
×
163
      presentation <- ZIO.succeed(
×
164
        Presentation(
165
          body = Presentation.Body(
166
            goal_code = requestPresentation.body.goal_code,
167
            comment = requestPresentation.body.comment
168
          ),
×
169
          attachments = Seq(
170
            AttachmentDescriptor
×
171
              .buildBase64Attachment(
×
172
                payload = presentationPayload.data.getBytes(),
173
                mediaType = Some(PresentCredentialFormat.Anoncred.name),
174
                format = Some(PresentCredentialFormat.Anoncred.name),
175
              )
176
          ),
×
177
          thid = requestPresentation.thid.orElse(Some(requestPresentation.id)),
178
          from = requestPresentation.to,
179
          to = requestPresentation.from
180
        )
181
      )
182
    } yield presentation
183
  }
184

×
185
  override def extractIdFromCredential(credential: W3cCredentialPayload): Option[UUID] =
×
186
    credential.maybeId.map(_.split("/").last).map(UUID.fromString)
187

1✔
188
  override def getPresentationRecords(
189
      ignoreWithZeroRetries: Boolean
190
  ): ZIO[WalletAccessContext, PresentationError, Seq[PresentationRecord]] = {
1✔
191
    for {
×
192
      records <- presentationRepository
×
193
        .getPresentationRecords(ignoreWithZeroRetries)
194
        .mapError(RepositoryError.apply)
195
    } yield records
196
  }
197

1✔
198
  override def getPresentationRecord(
199
      recordId: DidCommID
200
  ): ZIO[WalletAccessContext, PresentationError, Option[PresentationRecord]] = {
1✔
201
    for {
×
202
      record <- presentationRepository
×
203
        .getPresentationRecord(recordId)
204
        .mapError(RepositoryError.apply)
205
    } yield record
206
  }
207

×
208
  override def getPresentationRecordByThreadId(
209
      thid: DidCommID
210
  ): ZIO[WalletAccessContext, PresentationError, Option[PresentationRecord]] =
×
211
    for {
×
212
      record <- presentationRepository
×
213
        .getPresentationRecordByThreadId(thid)
214
        .mapError(RepositoryError.apply)
215
    } yield record
216

1✔
217
  override def rejectRequestPresentation(
218
      recordId: DidCommID
219
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
1✔
220
    markRequestPresentationRejected(recordId)
221
  }
222

1✔
223
  def rejectPresentation(recordId: DidCommID): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
1✔
224
    markPresentationRejected(recordId)
225
  }
226

1✔
227
  override def createJwtPresentationRecord(
228
      pairwiseVerifierDID: DidId,
229
      pairwiseProverDID: DidId,
230
      thid: DidCommID,
231
      connectionId: Option[String],
232
      proofTypes: Seq[ProofType],
233
      maybeOptions: Option[io.iohk.atala.pollux.core.model.presentation.Options]
234
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
1✔
235
    createPresentationRecord(
236
      pairwiseVerifierDID,
237
      pairwiseProverDID,
238
      thid,
239
      connectionId,
240
      CredentialFormat.JWT,
241
      proofTypes,
×
242
      maybeOptions.map(options => Seq(toJWTAttachment(options))).getOrElse(Seq.empty)
243
    )
244
  }
245

1✔
246
  override def createAnoncredPresentationRecord(
247
      pairwiseVerifierDID: DidId,
248
      pairwiseProverDID: DidId,
249
      thid: DidCommID,
250
      connectionId: Option[String],
251
      presentationRequest: AnoncredPresentationRequestV1
252
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
1✔
253
    createPresentationRecord(
254
      pairwiseVerifierDID,
255
      pairwiseProverDID,
256
      thid,
257
      connectionId,
258
      CredentialFormat.AnonCreds,
1✔
259
      Seq.empty,
1✔
260
      Seq(toAnoncredAttachment(presentationRequest))
261
    )
262
  }
263

1✔
264
  private def createPresentationRecord(
265
      pairwiseVerifierDID: DidId,
266
      pairwiseProverDID: DidId,
267
      thid: DidCommID,
268
      connectionId: Option[String],
269
      format: CredentialFormat,
270
      proofTypes: Seq[ProofType],
271
      attachments: Seq[AttachmentDescriptor]
272
  ) = {
1✔
273
    for {
×
274
      request <- ZIO.succeed(
×
275
        createDidCommRequestPresentation(
276
          proofTypes,
277
          thid,
278
          pairwiseVerifierDID,
279
          pairwiseProverDID,
280
          attachments
281
        )
UNCOV
282
      )
×
283
      record <- ZIO.succeed(
284
        PresentationRecord(
×
285
          id = DidCommID(),
×
286
          createdAt = Instant.now,
287
          updatedAt = None,
288
          thid = thid,
289
          connectionId = connectionId,
290
          schemaId = None, // TODO REMOVE from DB
291
          role = PresentationRecord.Role.Verifier,
292
          subjectId = pairwiseProverDID,
293
          protocolState = PresentationRecord.ProtocolState.RequestPending,
294
          credentialFormat = format,
295
          requestPresentationData = Some(request),
296
          proposePresentationData = None,
297
          presentationData = None,
298
          credentialsToUse = None,
299
          anoncredCredentialsToUseJsonSchemaId = None,
300
          anoncredCredentialsToUse = None,
301
          metaRetries = maxRetries,
×
302
          metaNextRetry = Some(Instant.now()),
303
          metaLastFailure = None,
304
        )
305
      )
×
306
      _ <- presentationRepository
×
307
        .createPresentationRecord(record)
308
        .flatMap {
×
309
          case 1 => ZIO.succeed(())
×
310
          case n => ZIO.fail(UnexpectedException(s"Invalid row count result: $n"))
311
        }
×
312
        .mapError(RepositoryError.apply) @@ CustomMetricsAspect.startRecordingTime(
×
313
        s"${record.id}_present_proof_flow_verifier_req_pending_to_sent_ms_gauge"
314
      )
315
    } yield record
316
  }
317

1✔
318
  override def getPresentationRecordsByStates(
319
      ignoreWithZeroRetries: Boolean,
320
      limit: Int,
321
      states: PresentationRecord.ProtocolState*
322
  ): ZIO[WalletAccessContext, PresentationError, Seq[PresentationRecord]] = {
1✔
323
    for {
×
324
      records <- presentationRepository
×
325
        .getPresentationRecordsByStates(ignoreWithZeroRetries, limit, states: _*)
326
        .mapError(RepositoryError.apply)
327
    } yield records
328
  }
329

×
330
  override def getPresentationRecordsByStatesForAllWallets(
331
      ignoreWithZeroRetries: Boolean,
332
      limit: Int,
333
      states: PresentationRecord.ProtocolState*
334
  ): IO[PresentationError, Seq[PresentationRecord]] = {
×
335
    for {
×
336
      records <- presentationRepository
×
337
        .getPresentationRecordsByStatesForAllWallets(ignoreWithZeroRetries, limit, states: _*)
338
        .mapError(RepositoryError.apply)
339
    } yield records
340
  }
341

1✔
342
  override def receiveRequestPresentation(
343
      connectionId: Option[String],
344
      request: RequestPresentation
345
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
1✔
346
    for {
347
      format <- request.attachments match {
×
348
        case Seq() => ZIO.fail(PresentationError.MissingCredential)
×
349
        case Seq(head) =>
350
          val jsonF = PresentCredentialRequestFormat.JWT.name // stable identifier
351
          val anoncredF = PresentCredentialRequestFormat.Anoncred.name // stable identifier
352
          head.format match
×
353
            case None          => ZIO.fail(PresentationError.MissingCredentialFormat)
×
354
            case Some(`jsonF`) => ZIO.succeed(CredentialFormat.JWT)
×
355
            case Some(`anoncredF`) =>
356
              head.data match
×
357
                case Base64(data) =>
×
358
                  val decodedData = new String(JBase64.getUrlDecoder.decode(data))
×
359
                  AnoncredPresentationRequestV1.schemaSerDes
×
360
                    .validate(decodedData)
361
                    .map(_ => CredentialFormat.AnonCreds)
×
362
                    .mapError(error => InvalidAnoncredPresentationRequest(error.error))
×
363
                case _ => ZIO.fail(InvalidAnoncredPresentationRequest("Expecting Base64-encoded data"))
×
364
            case Some(unsupportedFormat) => ZIO.fail(PresentationError.UnsupportedCredentialFormat(unsupportedFormat))
×
365
        case _ => ZIO.fail(PresentationError.UnexpectedError("Presentation with multi attachments"))
366
      }
×
367
      record <- ZIO.succeed(
368
        PresentationRecord(
×
369
          id = DidCommID(),
×
370
          createdAt = Instant.now,
371
          updatedAt = None,
×
372
          thid = DidCommID(request.thid.getOrElse(request.id)),
373
          connectionId = connectionId,
374
          schemaId = None,
375
          role = Role.Prover,
376
          subjectId = request.to,
377
          protocolState = PresentationRecord.ProtocolState.RequestReceived,
378
          credentialFormat = format,
379
          requestPresentationData = Some(request),
380
          proposePresentationData = None,
381
          presentationData = None,
382
          credentialsToUse = None,
383
          anoncredCredentialsToUseJsonSchemaId = None,
384
          anoncredCredentialsToUse = None,
385
          metaRetries = maxRetries,
×
386
          metaNextRetry = Some(Instant.now()),
387
          metaLastFailure = None,
388
        )
389
      )
1✔
390
      _ <- presentationRepository
×
391
        .createPresentationRecord(record)
392
        .flatMap {
×
393
          case 1 => ZIO.succeed(())
×
394
          case n => ZIO.fail(UnexpectedException(s"Invalid row count result: $n"))
395
        }
396
        .mapError(RepositoryError.apply)
397
    } yield record
398
  }
399

400
  /** All credentials MUST be of the same format */
1✔
401
  private def createJwtPresentationPayloadFromCredential(
402
      issuedCredentials: Seq[String],
403
      requestPresentation: RequestPresentation,
404
      prover: Issuer
405
  ): IO[PresentationError, PresentationPayload] = {
406

407
    val verifiableCredentials: Either[
408
      PresentationError.PresentationDecodingError,
409
      Seq[JwtVerifiableCredentialPayload]
410
    ] =
1✔
411
      issuedCredentials.map { signedCredential =>
×
412
        decode[io.iohk.atala.mercury.model.Base64](signedCredential)
×
413
          .flatMap(x => Right(new String(java.util.Base64.getDecoder.decode(x.base64))))
×
414
          .flatMap(x => Right(JwtVerifiableCredentialPayload(JWT(x))))
×
415
          .left
×
416
          .map(err => PresentationDecodingError(new Throwable(s"JsonData decoding error: $err")))
417
      }.sequence
418

419
    val maybePresentationOptions
420
        : Either[PresentationError, Option[io.iohk.atala.pollux.core.model.presentation.Options]] =
×
421
      requestPresentation.attachments.headOption
×
422
        .map(attachment =>
×
423
          decode[io.iohk.atala.mercury.model.JsonData](attachment.data.asJson.noSpaces)
×
424
            .flatMap(data =>
×
425
              io.iohk.atala.pollux.core.model.presentation.PresentationAttachment.given_Decoder_PresentationAttachment
×
426
                .decodeJson(data.json.asJson)
×
427
                .map(_.options)
×
428
                .leftMap(err =>
×
429
                  PresentationDecodingError(new Throwable(s"PresentationAttachment decoding error: $err"))
430
                )
431
            )
×
432
            .leftMap(err => PresentationDecodingError(new Throwable(s"JsonData decoding error: $err")))
433
        )
×
434
        .getOrElse(Right(None))
435

1✔
436
    for {
×
UNCOV
437
      maybeOptions <- ZIO.fromEither(maybePresentationOptions)
×
438
      vcs <- ZIO.fromEither(verifiableCredentials)
1✔
439
      presentationPayload <-
×
440
        ZIO.succeed(
441
          maybeOptions
×
442
            .map { options =>
×
443
              W3cPresentationPayload(
×
444
                `@context` = Vector("https://www.w3.org/2018/presentations/v1"),
445
                maybeId = None,
×
446
                `type` = Vector("VerifiablePresentation"),
×
447
                verifiableCredential = vcs.toVector,
×
448
                holder = prover.did.value,
×
449
                verifier = Vector(options.domain),
450
                maybeIssuanceDate = None,
451
                maybeExpirationDate = None
×
452
              ).toJwtPresentationPayload.copy(maybeNonce = Some(options.challenge))
453
            }
×
454
            .getOrElse {
×
455
              W3cPresentationPayload(
×
456
                `@context` = Vector("https://www.w3.org/2018/presentations/v1"),
457
                maybeId = None,
×
458
                `type` = Vector("VerifiablePresentation"),
×
459
                verifiableCredential = vcs.toVector,
×
460
                holder = prover.did.value,
×
461
                verifier = Vector("https://example.verifier"), // TODO Fix this
462
                maybeIssuanceDate = None,
463
                maybeExpirationDate = None
×
464
              ).toJwtPresentationPayload
465
            }
466
        )
467
    } yield presentationPayload
468
  }
469

470
  private case class AnoncredCredentialProof(
471
      credential: String,
472
      requestedAttribute: Seq[String],
473
      requestedPredicate: Seq[String]
474
  )
475

1✔
476
  private def createAnoncredPresentationPayloadFromCredential(
477
      issuedCredentialRecords: Seq[ValidFullIssuedCredentialRecord],
478
      schemaIds: Seq[String],
479
      credentialDefinitionIds: Seq[String],
480
      requestPresentation: RequestPresentation,
481
      credentialProofs: List[AnoncredCredentialProofV1],
482
  ): ZIO[WalletAccessContext, PresentationError, AnoncredPresentation] = {
1✔
483
    for {
484
      schemaMap <-
×
485
        ZIO
×
486
          .collectAll(schemaIds.map { schemaUri =>
×
487
            resolveSchema(schemaUri)
488
          })
×
489
          .map(_.toMap)
×
490
      credentialDefinitionMap <-
×
491
        ZIO
×
492
          .collectAll(credentialDefinitionIds.map { credentialDefinitionUri =>
×
493
            resolveCredentialDefinition(credentialDefinitionUri)
494
          })
×
495
          .map(_.toMap)
×
496
      credentialProofsMap = credentialProofs.map(credentialProof => (credentialProof.credential, credentialProof)).toMap
1✔
497
      verifiableCredentials <-
×
498
        ZIO.collectAll(
499
          issuedCredentialRecords
×
500
            .flatMap(issuedCredentialRecord => {
×
501
              issuedCredentialRecord.issuedCredential
×
502
                .map(issuedCredential =>
503
                  issuedCredential.attachments
×
504
                    .filter(attachment => attachment.format.contains(IssueCredentialIssuedFormat.Anoncred.name))
×
505
                    .map(_.data)
×
506
                    .map {
×
507
                      case Base64(data) =>
×
508
                        Right(
509
                          AnoncredCredentialProof(
×
510
                            new String(JBase64.getUrlDecoder.decode(data)),
×
511
                            credentialProofsMap(issuedCredentialRecord.id.value).requestedAttribute,
×
512
                            credentialProofsMap(issuedCredentialRecord.id.value).requestedPredicate
513
                          )
514
                        )
×
515
                      case _ => Left(InvalidAnoncredPresentationRequest("Expecting Base64-encoded data"))
516
                    }
×
517
                    .map(ZIO.fromEither(_))
518
                )
×
519
                .toSeq
520
                .flatten
521
            })
522
        )
1✔
523
      presentationRequestAttachment <- ZIO.fromEither(
×
524
        requestPresentation.attachments.headOption.toRight(InvalidAnoncredPresentationRequest("Missing Presentation"))
525
      )
1✔
526
      presentationRequestData <-
527
        presentationRequestAttachment.data match
×
528
          case Base64(data) => ZIO.succeed(new String(JBase64.getUrlDecoder.decode(data)))
×
529
          case _            => ZIO.fail(InvalidAnoncredPresentationRequest("Expecting Base64-encoded data"))
1✔
530
      _ <-
×
531
        AnoncredPresentationRequestV1.schemaSerDes
532
          .deserialize(presentationRequestData)
×
533
          .mapError(error => InvalidAnoncredPresentationRequest(error.error))
1✔
534
      linkSecret <-
×
535
        linkSecretService
×
536
          .fetchOrCreate()
537
          .map(_.secret)
538
          .mapError(t => AnoncredPresentationCreationError(t.cause))
539
      credentialRequest =
×
540
        verifiableCredentials.map(verifiableCredential =>
541
          AnoncredCredentialRequests(
542
            AnoncredCredential(verifiableCredential.credential),
543
            verifiableCredential.requestedAttribute,
544
            verifiableCredential.requestedPredicate
545
          )
546
        )
1✔
547
      presentation <-
×
548
        ZIO
549
          .fromEither(
×
550
            AnoncredLib.createPresentation(
551
              AnoncredPresentationRequest(presentationRequestData),
552
              credentialRequest,
×
553
              Map.empty, // TO FIX
554
              linkSecret,
555
              schemaMap,
556
              credentialDefinitionMap
557
            )
558
          )
559
          .mapError((t: Throwable) => AnoncredPresentationCreationError(t))
560
    } yield presentation
561
  }
562

1✔
563
  private def resolveSchema(schemaUri: String): IO[UnexpectedError, (String, AnoncredSchemaDef)] = {
1✔
564
    for {
×
565
      uri <- ZIO.attempt(new URI(schemaUri)).mapError(e => UnexpectedError(e.getMessage))
×
566
      content <- uriDereferencer.dereference(uri).mapError(e => UnexpectedError(e.error))
1✔
567
      anoncredSchema <-
×
568
        AnoncredSchemaSerDesV1.schemaSerDes
569
          .deserialize(content)
×
570
          .mapError(error => UnexpectedError(s"AnonCreds Schema parsing error: $error"))
571
      anoncredLibSchema =
572
        AnoncredSchemaDef(
573
          schemaUri,
574
          anoncredSchema.version,
575
          anoncredSchema.attrNames,
576
          anoncredSchema.issuerId
577
        )
578
    } yield (schemaUri, anoncredLibSchema)
579
  }
580

1✔
581
  private def resolveCredentialDefinition(
582
      credentialDefinitionUri: String
583
  ): IO[UnexpectedError, (String, AnoncredCredentialDefinition)] = {
1✔
584
    for {
×
585
      uri <- ZIO.attempt(new URI(credentialDefinitionUri)).mapError(e => UnexpectedError(e.getMessage))
×
586
      content <- uriDereferencer.dereference(uri).mapError(e => UnexpectedError(e.error))
×
587
      _ <-
×
588
        PublicCredentialDefinitionSerDesV1.schemaSerDes
×
589
          .validate(content)
×
590
          .mapError(error => UnexpectedError(s"AnonCreds Schema parsing error: $error"))
591
      anoncredCredentialDefinition = AnoncredCredentialDefinition(content)
592
    } yield (credentialDefinitionUri, anoncredCredentialDefinition)
593
  }
594

1✔
595
  def acceptRequestPresentation(
596
      recordId: DidCommID,
597
      credentialsToUse: Seq[String]
598
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
599

1✔
600
    for {
×
UNCOV
601
      record <- getRecordWithState(recordId, ProtocolState.RequestReceived)
×
602
      issuedCredentials <- credentialRepository
×
603
        .getValidIssuedCredentials(credentialsToUse.map(DidCommID(_)))
UNCOV
604
        .mapError(RepositoryError.apply)
×
UNCOV
605
      validatedCredentialsFormat <- validateCredentialsFormat(record, issuedCredentials)
×
606
      _ <- validateCredentials(
×
607
        s"No matching issued credentials found in prover db from the given: $credentialsToUse",
608
        validatedCredentialsFormat
609
      )
1✔
610
      count <- presentationRepository
×
611
        .updatePresentationWithCredentialsToUse(recordId, Option(credentialsToUse), ProtocolState.PresentationPending)
×
612
        .mapError(RepositoryError.apply) @@ CustomMetricsAspect.startRecordingTime(
×
613
        s"${record.id}_present_proof_flow_prover_presentation_pending_to_generated_ms_gauge"
614
      )
1✔
615
      record <- fetchPresentationRecord(recordId, count)
616
    } yield record
617
  }
618

1✔
619
  private def fetchPresentationRecord(recordId: DidCommID, count: RuntimeFlags) = {
1✔
620
    for {
1✔
621
      _ <- count match
×
622
        case 1 => ZIO.succeed(())
×
623
        case _ => ZIO.fail(RecordIdNotFound(recordId))
1✔
624
      record <- presentationRepository
×
625
        .getPresentationRecord(recordId)
626
        .mapError(RepositoryError.apply)
627
        .flatMap {
×
628
          case None        => ZIO.fail(RecordIdNotFound(recordId))
×
629
          case Some(value) => ZIO.succeed(value)
630
        }
631
    } yield record
632
  }
633

1✔
634
  override def acceptAnoncredRequestPresentation(
635
      recordId: DidCommID,
636
      credentialsToUse: AnoncredCredentialProofsV1
637
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
638

1✔
639
    for {
×
640
      record <- getRecordWithState(recordId, ProtocolState.RequestReceived)
1✔
641
      issuedCredentials <-
×
642
        credentialRepository
×
643
          .getValidAnoncredIssuedCredentials(
×
644
            credentialsToUse.credentialProofs.map(credentialProof => DidCommID(credentialProof.credential))
645
          )
UNCOV
646
          .mapError(RepositoryError.apply)
×
647
      _ <- validateFullCredentialsFormat(
648
        record,
649
        issuedCredentials
UNCOV
650
      )
×
651
      anoncredCredentialProofsV1AsJson <- ZIO
652
        .fromEither(
×
653
          AnoncredCredentialProofsV1.schemaSerDes.serialize(credentialsToUse)
654
        )
655
        .mapError(error =>
656
          PresentationError.UnexpectedError(
×
657
            s"Unable to serialize credentialsToUse. credentialsToUse:$credentialsToUse, error:$error"
658
          )
UNCOV
659
        )
×
660
      count <- presentationRepository
×
661
        .updateAnoncredPresentationWithCredentialsToUse(
662
          recordId,
×
663
          Option(AnoncredCredentialProofsV1.version),
×
664
          Option(anoncredCredentialProofsV1AsJson),
665
          ProtocolState.PresentationPending
666
        )
×
667
        .mapError(RepositoryError.apply) @@ CustomMetricsAspect.startRecordingTime(
×
668
        s"${record.id}_present_proof_flow_prover_presentation_pending_to_generated_ms_gauge"
669
      )
×
670
      record <- fetchPresentationRecord(recordId, count)
671
    } yield record
672
  }
673

1✔
674
  private def validateCredentials(
675
      errorMessage: String,
676
      issuedCredentials: Seq[ValidIssuedCredentialRecord]
677
  ) = {
1✔
678
    val issuedCredentialRaw = issuedCredentials.flatMap(_.issuedCredentialRaw)
1✔
679
    for {
×
680
      _ <- ZIO.fromEither(
×
681
        Either.cond(
×
682
          issuedCredentialRaw.nonEmpty,
683
          issuedCredentialRaw,
684
          PresentationError.IssuedCredentialNotFoundError(
×
685
            new Throwable(errorMessage)
686
          )
687
        )
688
      )
689
    } yield ()
690
  }
691

1✔
692
  private def validateCredentialsFormat(
693
      record: PresentationRecord,
694
      issuedCredentials: Seq[ValidIssuedCredentialRecord]
695
  ) = {
1✔
UNCOV
696
    for {
×
697
      _ <- ZIO.cond(
×
698
        issuedCredentials.map(_.subjectId).toSet.size == 1,
699
        (),
700
        PresentationError.HolderBindingError(
×
701
          s"Creating a Verifiable Presentation for credential with different subject DID is not supported, found : ${issuedCredentials
×
702
              .map(_.subjectId)}"
703
        )
704
      )
1✔
705
      validatedCredentials <- ZIO.fromEither(
×
706
        Either.cond(
×
707
          issuedCredentials.forall(issuedValidCredential =>
708
            issuedValidCredential.credentialFormat == record.credentialFormat
709
          ),
710
          issuedCredentials,
711
          PresentationError.NotMatchingPresentationCredentialFormat(
×
712
            new IllegalArgumentException(
×
713
              s"No matching issued credentials format: expectedFormat=${record.credentialFormat}"
714
            )
715
          )
716
        )
717
      )
718
    } yield validatedCredentials
719
  }
720

1✔
721
  private def validateFullCredentialsFormat(
722
      record: PresentationRecord,
723
      issuedCredentials: Seq[ValidFullIssuedCredentialRecord]
1✔
724
  ) = validateCredentialsFormat(
725
    record,
1✔
726
    issuedCredentials.map(cred => ValidIssuedCredentialRecord(cred.id, None, cred.credentialFormat, cred.subjectId))
727
  )
728

1✔
729
  override def acceptPresentation(
730
      recordId: DidCommID
731
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
1✔
732
    for {
×
733
      maybeRecord <- presentationRepository
×
734
        .getPresentationRecord(recordId)
UNCOV
735
        .mapError(RepositoryError.apply)
×
736
      record <- ZIO
737
        .fromOption(maybeRecord)
738
        .mapError(_ => RecordIdNotFound(recordId))
1✔
739
      _ <- ZIO
740
        .fromOption(record.presentationData)
×
741
        .mapError(_ => InvalidFlowStateError(s"No request found for this record: $recordId"))
1✔
742
      recordUpdated <- markPresentationAccepted(record.id)
743
    } yield recordUpdated
744
  }
745

1✔
746
  override def receivePresentation(
747
      presentation: Presentation
748
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
1✔
749
    for {
×
750
      record <- getRecordFromThreadId(presentation.thid)
1✔
751
      _ <- presentationRepository
×
752
        .updateWithPresentation(record.id, presentation, ProtocolState.PresentationReceived)
753
        .flatMap {
×
754
          case 1 => ZIO.succeed(())
×
755
          case n => ZIO.fail(UnexpectedException(s"Invalid row count result: $n"))
756
        }
×
757
        .mapError(RepositoryError.apply) @@ CustomMetricsAspect.startRecordingTime(
×
758
        s"${record.id}_present_proof_flow_verifier_presentation_received_to_verification_success_or_failure_ms_gauge"
759
      )
×
760
      record <- presentationRepository
×
761
        .getPresentationRecord(record.id)
762
        .mapError(RepositoryError.apply)
763
        .flatMap {
×
764
          case None        => ZIO.fail(RecordIdNotFound(record.id))
×
765
          case Some(value) => ZIO.succeed(value)
766
        }
767
    } yield record
768
  }
769

1✔
770
  override def acceptProposePresentation(
771
      recordId: DidCommID
772
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
1✔
773
    for {
×
774
      maybeRecord <- presentationRepository
×
775
        .getPresentationRecord(recordId)
776
        .mapError(RepositoryError.apply)
1✔
777
      record <- ZIO
778
        .fromOption(maybeRecord)
UNCOV
779
        .mapError(_ => RecordIdNotFound(recordId))
×
780
      request <- ZIO
781
        .fromOption(record.proposePresentationData)
×
782
        .mapError(_ => InvalidFlowStateError(s"No request found for this record: $recordId"))
783
      // TODO: Generate the JWT credential and use it to create the Presentation object
×
784
      requestPresentation = createDidCommRequestPresentationFromProposal(request)
1✔
785
      count <- presentationRepository
×
786
        .updateWithRequestPresentation(recordId, requestPresentation, ProtocolState.PresentationPending)
787
        .mapError(RepositoryError.apply)
1✔
788
      _ <- count match
×
789
        case 1 => ZIO.succeed(())
×
790
        case _ => ZIO.fail(RecordIdNotFound(recordId))
1✔
791
      record <- presentationRepository
×
792
        .getPresentationRecord(record.id)
793
        .mapError(RepositoryError.apply)
794
        .flatMap {
×
795
          case None        => ZIO.fail(RecordIdNotFound(record.id))
×
796
          case Some(value) => ZIO.succeed(value)
797
        }
798
    } yield record
799
  }
800

1✔
801
  override def receiveProposePresentation(
802
      proposePresentation: ProposePresentation
803
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
1✔
804
    for {
×
UNCOV
805
      record <- getRecordFromThreadId(proposePresentation.thid)
×
806
      _ <- presentationRepository
×
807
        .updateWithProposePresentation(record.id, proposePresentation, ProtocolState.ProposalReceived)
808
        .flatMap {
×
809
          case 1 => ZIO.succeed(())
×
810
          case n => ZIO.fail(UnexpectedException(s"Invalid row count result: $n"))
811
        }
812
        .mapError(RepositoryError.apply)
×
813
      record <- presentationRepository
×
814
        .getPresentationRecord(record.id)
815
        .mapError(RepositoryError.apply)
816
        .flatMap {
×
817
          case None        => ZIO.fail(RecordIdNotFound(record.id))
×
818
          case Some(value) => ZIO.succeed(value)
819
        }
820
    } yield record
821
  }
822

1✔
823
  private[this] def getRecordWithState(
824
      recordId: DidCommID,
825
      state: ProtocolState
826
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
1✔
827
    for {
×
828
      maybeRecord <- presentationRepository
×
829
        .getPresentationRecord(recordId)
830
        .mapError(RepositoryError.apply)
×
831
      record <- ZIO
832
        .fromOption(maybeRecord)
833
        .mapError(_ => RecordIdNotFound(recordId))
1✔
834
      _ <- record.protocolState match {
×
835
        case s if s == state => ZIO.unit
×
836
        case state           => ZIO.fail(InvalidFlowStateError(s"Invalid protocol state for operation: $state"))
837
      }
838
    } yield record
839
  }
840

1✔
841
  override def markRequestPresentationSent(
842
      recordId: DidCommID
843
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] =
1✔
844
    updatePresentationRecordProtocolState(
845
      recordId,
846
      PresentationRecord.ProtocolState.RequestPending,
847
      PresentationRecord.ProtocolState.RequestSent
848
    )
849

1✔
850
  override def markProposePresentationSent(
851
      recordId: DidCommID
852
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] =
1✔
853
    updatePresentationRecordProtocolState(
854
      recordId,
855
      PresentationRecord.ProtocolState.ProposalPending,
856
      PresentationRecord.ProtocolState.ProposalSent
857
    )
1✔
858
  override def markPresentationVerified(
859
      recordId: DidCommID
860
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] =
1✔
861
    updatePresentationRecordProtocolState(
862
      recordId,
863
      PresentationRecord.ProtocolState.PresentationReceived,
864
      PresentationRecord.ProtocolState.PresentationVerified
865
    )
866

1✔
867
  override def markPresentationAccepted(
868
      recordId: DidCommID
869
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] =
1✔
870
    updatePresentationRecordProtocolState(
871
      recordId,
872
      PresentationRecord.ProtocolState.PresentationVerified,
873
      PresentationRecord.ProtocolState.PresentationAccepted
874
    )
875

1✔
876
  override def markPresentationSent(
877
      recordId: DidCommID
878
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] =
1✔
879
    updatePresentationRecordProtocolState(
880
      recordId,
881
      PresentationRecord.ProtocolState.PresentationGenerated,
882
      PresentationRecord.ProtocolState.PresentationSent
883
    )
884

1✔
885
  override def markPresentationRejected(
886
      recordId: DidCommID
887
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] =
1✔
888
    updatePresentationRecordProtocolState(
889
      recordId,
890
      PresentationRecord.ProtocolState.PresentationVerified,
891
      PresentationRecord.ProtocolState.PresentationRejected
892
    )
893

1✔
894
  override def markRequestPresentationRejected(
895
      recordId: DidCommID
896
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] =
1✔
897
    updatePresentationRecordProtocolState(
898
      recordId,
899
      PresentationRecord.ProtocolState.RequestReceived,
900
      PresentationRecord.ProtocolState.RequestRejected
901
    )
902

×
903
  override def markPresentationVerificationFailed(
904
      recordId: DidCommID
905
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] =
×
906
    updatePresentationRecordProtocolState(
907
      recordId,
908
      PresentationRecord.ProtocolState.PresentationReceived,
909
      PresentationRecord.ProtocolState.PresentationVerificationFailed
910
    )
911

1✔
912
  override def verifyAnoncredPresentation(
913
      presentation: Presentation,
914
      requestPresentation: RequestPresentation,
915
      recordId: DidCommID
916
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
1✔
917
    for {
×
918
      serializedPresentation <- presentation.attachments.head.data match {
×
919
        case Base64(data) => ZIO.succeed(AnoncredPresentation(new String(JBase64.getUrlDecoder.decode(data))))
×
920
        case _            => ZIO.fail(InvalidAnoncredPresentation("Expecting Base64-encoded data"))
921
      }
×
922
      deserializedPresentation <-
×
923
        AnoncredPresentationV1.schemaSerDes
924
          .deserialize(serializedPresentation.data)
×
925
          .mapError(error => PresentationError.UnexpectedError(error.error))
×
926
      schemaIds = deserializedPresentation.identifiers.map(_.schema_id)
1✔
927
      schemaMap <-
×
928
        ZIO
×
929
          .collectAll(schemaIds.map { schemaId =>
×
930
            resolveSchema(schemaId)
931
          })
×
932
          .map(_.toMap)
×
933
      credentialDefinitionIds = deserializedPresentation.identifiers.map(_.cred_def_id)
1✔
934
      credentialDefinitionMap <-
×
935
        ZIO
×
936
          .collectAll(credentialDefinitionIds.map { credentialDefinitionId =>
×
937
            resolveCredentialDefinition(credentialDefinitionId)
938
          })
×
939
          .map(_.toMap)
×
940
      serializedPresentationRequest <- requestPresentation.attachments.head.data match {
×
941
        case Base64(data) => ZIO.succeed(AnoncredPresentationRequest(new String(JBase64.getUrlDecoder.decode(data))))
×
942
        case _            => ZIO.fail(InvalidAnoncredPresentationRequest("Expecting Base64-encoded data"))
943
      }
1✔
944
      isValid <-
×
945
        ZIO
946
          .fromTry(
×
947
            Try(
×
948
              AnoncredLib.verifyPresentation(
949
                serializedPresentation,
950
                serializedPresentationRequest,
951
                schemaMap,
952
                credentialDefinitionMap
953
              )
954
            )
955
          )
956
          .mapError((t: Throwable) => AnoncredPresentationVerificationError(t))
957
          .flatMapError(e =>
×
958
            for {
×
959
              _ <- markPresentationVerificationFailed(recordId)
960
            } yield ()
×
961
            ZIO.succeed(e)
962
          )
1✔
963
      result <-
×
964
        if isValid then markPresentationVerified(recordId)
×
965
        else markPresentationVerificationFailed(recordId)
966
    } yield result
967
  }
968

×
969
  def reportProcessingFailure(
970
      recordId: DidCommID,
971
      failReason: Option[String]
972
  ): ZIO[WalletAccessContext, PresentationError, Unit] =
×
973
    presentationRepository
×
974
      .updateAfterFail(recordId, failReason)
975
      .mapError(RepositoryError.apply)
976
      .flatMap {
×
977
        case 1 => ZIO.unit
×
978
        case n => ZIO.fail(UnexpectedError(s"Invalid number of records updated: $n"))
979
      }
980

1✔
981
  private[this] def getRecordFromThreadId(
982
      thid: Option[String]
983
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
1✔
984
    for {
×
985
      thidID <- ZIO
986
        .fromOption(thid)
×
987
        .map(DidCommID(_))
988
        .mapError(_ => UnexpectedError("No `thid` found in Presentation request"))
1✔
989
      maybeRecord <- presentationRepository
×
990
        .getPresentationRecordByThreadId(thidID)
991
        .mapError(RepositoryError.apply)
1✔
992
      record <- ZIO
993
        .fromOption(maybeRecord)
994
        .mapError(_ => ThreadIdNotFound(thidID))
995
    } yield record
996
  }
997

1✔
998
  private[this] def toJWTAttachment(options: Options): AttachmentDescriptor = {
1✔
999
    AttachmentDescriptor.buildJsonAttachment(
1✔
1000
      payload = PresentationAttachment.build(Some(options)),
1001
      format = Some(PresentCredentialRequestFormat.JWT.name)
1002
    )
1003
  }
1004

1✔
1005
  private[this] def toAnoncredAttachment(
1006
      presentationRequest: AnoncredPresentationRequestV1
1007
  ): AttachmentDescriptor = {
1✔
1008
    AttachmentDescriptor.buildBase64Attachment(
1009
      mediaType = Some("application/json"),
1010
      format = Some(PresentCredentialRequestFormat.Anoncred.name),
1✔
1011
      payload = AnoncredPresentationRequestV1.schemaSerDes.serializeToJsonString(presentationRequest).getBytes()
1012
    )
1013
  }
1014

1✔
1015
  private[this] def createDidCommRequestPresentation(
1016
      proofTypes: Seq[ProofType],
1017
      thid: DidCommID,
1018
      pairwiseVerifierDID: DidId,
1019
      pairwiseProverDID: DidId,
1020
      attachments: Seq[AttachmentDescriptor]
1021
  ): RequestPresentation = {
1✔
1022
    RequestPresentation(
1✔
1023
      body = RequestPresentation.Body(
1024
        goal_code = Some("Request Proof Presentation"),
1025
        proof_types = proofTypes
1026
      ),
1027
      attachments = attachments,
1028
      from = pairwiseVerifierDID,
1029
      to = pairwiseProverDID,
1030
      thid = Some(thid.toString)
1031
    )
1032
  }
1033

1✔
1034
  private[this] def createDidCommRequestPresentationFromProposal(
1035
      proposePresentation: ProposePresentation
1036
  ): RequestPresentation = {
1037
    // TODO to review what is needed
1✔
1038
    val body = RequestPresentation.Body(goal_code = proposePresentation.body.goal_code)
1039

1✔
1040
    RequestPresentation(
1041
      body = body,
1042
      attachments = proposePresentation.attachments,
1043
      from = proposePresentation.to,
1044
      to = proposePresentation.from,
1045
      thid = proposePresentation.thid
1046
    )
1047
  }
1048

1✔
1049
  private[this] def updatePresentationRecordProtocolState(
1050
      id: DidCommID,
1051
      from: PresentationRecord.ProtocolState,
1052
      to: PresentationRecord.ProtocolState
1053
  ): ZIO[WalletAccessContext, PresentationError, PresentationRecord] = {
1✔
1054
    for {
1✔
1055
      _ <- presentationRepository
×
1056
        .updatePresentationRecordProtocolState(id, from, to)
1057
        .flatMap {
×
1058
          case 1 => ZIO.succeed(())
×
1059
          case n => ZIO.fail(UnexpectedException(s"Invalid row count result: $n"))
1060
        }
1061
        .mapError(RepositoryError.apply)
1✔
1062
      record <- fetchPresentationRecord(id, 1)
1063
    } yield record
1064
  }
1065

1066
}
1067

1068
object PresentationServiceImpl {
1069
  val layer: URLayer[
1070
    URIDereferencer & LinkSecretService & PresentationRepository & CredentialRepository,
1071
    PresentationService
1072
  ] =
1✔
1073
    ZLayer.fromFunction(PresentationServiceImpl(_, _, _, _))
1074
}
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