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

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

04 Mar 2024 07:15PM UTC coverage: 31.187% (-0.1%) from 31.311%
8145761569

Pull #917

shotexa
Fix minor bugs

Signed-off-by: Shota Jolbordi <shota.jolbordi@iohk.io>
Pull Request #917: feat(pollux): check verification status on presentation verification

34 of 139 new or added lines in 10 files covered. (24.46%)

408 existing lines in 114 files now uncovered.

4228 of 13557 relevant lines covered (31.19%)

0.31 hits per line

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

28.63
/pollux/lib/core/src/main/scala/io/iohk/atala/pollux/core/service/CredentialServiceImpl.scala
1
package io.iohk.atala.pollux.core.service
2

3
import io.circe.Json
4
import io.circe.syntax.*
5
import io.iohk.atala.agent.walletapi.model.{ManagedDIDState, PublicationState}
6
import io.iohk.atala.agent.walletapi.service.ManagedDIDService
7
import io.iohk.atala.agent.walletapi.storage.GenericSecretStorage
8
import io.iohk.atala.castor.core.model.did.{CanonicalPrismDID, PrismDID, VerificationRelationship}
9
import io.iohk.atala.castor.core.service.DIDService
10
import io.iohk.atala.mercury.model.*
11
import io.iohk.atala.mercury.protocol.issuecredential.*
12
import io.iohk.atala.pollux.*
13
import io.iohk.atala.pollux.anoncreds.{AnoncredLib, CreateCredentialDefinition, CredentialOffer}
14
import io.iohk.atala.pollux.core.model.*
15
import io.iohk.atala.pollux.core.model.CredentialFormat.AnonCreds
16
import io.iohk.atala.pollux.core.model.IssueCredentialRecord.ProtocolState.OfferReceived
17
import io.iohk.atala.pollux.core.model.error.CredentialServiceError
18
import io.iohk.atala.pollux.core.model.error.CredentialServiceError.*
19
import io.iohk.atala.pollux.core.model.presentation.*
20
import io.iohk.atala.pollux.core.model.schema.CredentialSchema
21
import io.iohk.atala.pollux.core.model.secret.CredentialDefinitionSecret
22
import io.iohk.atala.pollux.core.repository.{CredentialRepository, CredentialStatusListRepository}
23
import io.iohk.atala.pollux.vc.jwt.{ES256KSigner, Issuer as JwtIssuer, *}
24
import io.iohk.atala.shared.models.WalletAccessContext
25
import io.iohk.atala.shared.utils.aspects.CustomMetricsAspect
26
import zio.*
27
import zio.prelude.ZValidation
28

29
import java.net.URI
30
import java.rmi.UnexpectedException
31
import java.time.{Instant, ZoneId}
32
import java.util.UUID
33
import scala.language.implicitConversions
34

35
object CredentialServiceImpl {
36
  val layer: URLayer[
37
    CredentialRepository & CredentialStatusListRepository & DidResolver & URIDereferencer & GenericSecretStorage &
38
      CredentialDefinitionService & LinkSecretService & DIDService & ManagedDIDService,
39
    CredentialService
40
  ] = {
1✔
41
    ZLayer.fromZIO {
1✔
42
      for {
×
43
        credentialRepo <- ZIO.service[CredentialRepository]
×
UNCOV
44
        credentialStatusListRepo <- ZIO.service[CredentialStatusListRepository]
×
45
        didResolver <- ZIO.service[DidResolver]
×
46
        uriDereferencer <- ZIO.service[URIDereferencer]
×
47
        genericSecretStorage <- ZIO.service[GenericSecretStorage]
1✔
48
        credDefenitionService <- ZIO.service[CredentialDefinitionService]
×
49
        linkSecretService <- ZIO.service[LinkSecretService]
1✔
50
        didService <- ZIO.service[DIDService]
1✔
51
        manageDidService <- ZIO.service[ManagedDIDService]
1✔
52
        issueCredentialSem <- Semaphore.make(1)
1✔
53
      } yield CredentialServiceImpl(
54
        credentialRepo,
55
        credentialStatusListRepo,
56
        didResolver,
57
        uriDereferencer,
58
        genericSecretStorage,
59
        credDefenitionService,
60
        linkSecretService,
61
        didService,
62
        manageDidService,
63
        5,
64
        issueCredentialSem
65
      )
66
    }
67
  }
68

69
  //  private val VC_JSON_SCHEMA_URI = "https://w3c-ccg.github.io/vc-json-schemas/schema/2.0/schema.json"
70
  private val VC_JSON_SCHEMA_TYPE = "CredentialSchema2022"
71
}
72

73
private class CredentialServiceImpl(
74
    credentialRepository: CredentialRepository,
75
    credentialStatusListRepository: CredentialStatusListRepository,
76
    didResolver: DidResolver,
77
    uriDereferencer: URIDereferencer,
78
    genericSecretStorage: GenericSecretStorage,
79
    credentialDefinitionService: CredentialDefinitionService,
80
    linkSecretService: LinkSecretService,
81
    didService: DIDService,
82
    managedDIDService: ManagedDIDService,
×
83
    maxRetries: Int = 5, // TODO move to config
84
    issueCredentialSem: Semaphore
85
) extends CredentialService {
86

87
  import CredentialServiceImpl.*
88
  import IssueCredentialRecord.*
89

1✔
90
  override def getIssueCredentialRecords(
91
      ignoreWithZeroRetries: Boolean,
92
      offset: Option[Int],
93
      limit: Option[Int]
94
  ): ZIO[WalletAccessContext, CredentialServiceError, (Seq[IssueCredentialRecord], Int)] = {
1✔
95
    for {
×
96
      records <- credentialRepository
×
97
        .getIssueCredentialRecords(ignoreWithZeroRetries = ignoreWithZeroRetries, offset = offset, limit = limit)
98
        .mapError(RepositoryError.apply)
99
    } yield records
100
  }
101

×
102
  override def getIssueCredentialRecordByThreadId(
103
      thid: DidCommID,
104
      ignoreWithZeroRetries: Boolean
105
  ): ZIO[WalletAccessContext, CredentialServiceError, Option[IssueCredentialRecord]] =
×
106
    for {
×
107
      record <- credentialRepository
×
108
        .getIssueCredentialRecordByThreadId(thid, ignoreWithZeroRetries)
109
        .mapError(RepositoryError.apply)
110
    } yield record
111

1✔
112
  override def getIssueCredentialRecord(
113
      recordId: DidCommID
114
  ): ZIO[WalletAccessContext, CredentialServiceError, Option[IssueCredentialRecord]] = {
1✔
115
    for {
×
116
      record <- credentialRepository
×
117
        .getIssueCredentialRecord(recordId)
118
        .mapError(RepositoryError.apply)
119
    } yield record
120
  }
121

1✔
122
  override def createJWTIssueCredentialRecord(
123
      pairwiseIssuerDID: DidId,
124
      pairwiseHolderDID: DidId,
125
      thid: DidCommID,
126
      maybeSchemaId: Option[String],
127
      claims: Json,
128
      validityPeriod: Option[Double],
129
      automaticIssuance: Option[Boolean],
130
      issuingDID: CanonicalPrismDID
131
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1✔
132
    for {
1✔
133
      _ <- maybeSchemaId match
×
134
        case Some(schemaId) =>
×
135
          CredentialSchema
×
136
            .validateJWTClaims(schemaId, claims.noSpaces, uriDereferencer)
137
            .mapError(e => CredentialSchemaError(e))
×
138
        case None =>
139
          ZIO.unit
1✔
140
      attributes <- CredentialService.convertJsonClaimsToAttributes(claims)
×
141
      offer <- createJWTDidCommOfferCredential(
142
        pairwiseIssuerDID = pairwiseIssuerDID,
143
        pairwiseHolderDID = pairwiseHolderDID,
144
        maybeSchemaId = maybeSchemaId,
145
        claims = attributes,
146
        thid = thid,
×
147
        UUID.randomUUID().toString,
148
        "domain"
UNCOV
149
      )
×
150
      record <- ZIO.succeed(
151
        IssueCredentialRecord(
×
152
          id = DidCommID(),
×
153
          createdAt = Instant.now,
154
          updatedAt = None,
155
          thid = thid,
156
          schemaId = maybeSchemaId,
157
          credentialDefinitionId = None,
158
          credentialFormat = CredentialFormat.JWT,
159
          role = IssueCredentialRecord.Role.Issuer,
160
          subjectId = None,
161
          validityPeriod = validityPeriod,
162
          automaticIssuance = automaticIssuance,
163
          protocolState = IssueCredentialRecord.ProtocolState.OfferPending,
164
          offerCredentialData = Some(offer),
165
          requestCredentialData = None,
166
          anonCredsRequestMetadata = None,
167
          issueCredentialData = None,
168
          issuedCredentialRaw = None,
169
          issuingDID = Some(issuingDID),
170
          metaRetries = maxRetries,
×
171
          metaNextRetry = Some(Instant.now()),
172
          metaLastFailure = None,
173
        )
174
      )
×
175
      count <- credentialRepository
×
176
        .createIssueCredentialRecord(record)
177
        .flatMap {
×
178
          case 1 => ZIO.succeed(())
×
179
          case n => ZIO.fail(UnexpectedException(s"Invalid row count result: $n"))
180
        }
181
        .mapError(RepositoryError.apply) @@ CustomMetricsAspect
×
182
        .startRecordingTime(s"${record.id}_issuer_offer_pending_to_sent_ms_gauge")
183
    } yield record
184
  }
185

1✔
186
  override def createAnonCredsIssueCredentialRecord(
187
      pairwiseIssuerDID: DidId,
188
      pairwiseHolderDID: DidId,
189
      thid: DidCommID,
190
      credentialDefinitionGUID: UUID,
191
      claims: Json,
192
      validityPeriod: Option[Double],
193
      automaticIssuance: Option[Boolean],
194
      credentialDefinitionId: String
195
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1✔
196
    for {
×
197
      credentialDefinition <- credentialDefinitionService
×
198
        .getByGUID(credentialDefinitionGUID)
199
        .mapError(e => CredentialServiceError.UnexpectedError(e.toString))
1✔
200
      _ <- CredentialSchema
×
201
        .validateAnonCredsClaims(credentialDefinition.schemaId, claims.noSpaces, uriDereferencer)
UNCOV
202
        .mapError(e => CredentialSchemaError(e))
×
203
      attributes <- CredentialService.convertJsonClaimsToAttributes(claims)
1✔
204
      offer <- createAnonCredsDidCommOfferCredential(
205
        pairwiseIssuerDID = pairwiseIssuerDID,
206
        pairwiseHolderDID = pairwiseHolderDID,
207
        schemaId = credentialDefinition.schemaId,
208
        credentialDefinitionGUID = credentialDefinitionGUID,
209
        claims = attributes,
210
        thid = thid,
211
        credentialDefinitionId
212
      )
×
213
      record <- ZIO.succeed(
214
        IssueCredentialRecord(
×
215
          id = DidCommID(),
×
216
          createdAt = Instant.now,
217
          updatedAt = None,
218
          thid = thid,
219
          schemaId = Some(credentialDefinition.schemaId),
220
          credentialDefinitionId = Some(credentialDefinitionGUID),
221
          credentialFormat = CredentialFormat.AnonCreds,
222
          role = IssueCredentialRecord.Role.Issuer,
223
          subjectId = None,
224
          validityPeriod = validityPeriod,
225
          automaticIssuance = automaticIssuance,
226
          protocolState = IssueCredentialRecord.ProtocolState.OfferPending,
227
          offerCredentialData = Some(offer),
228
          requestCredentialData = None,
229
          anonCredsRequestMetadata = None,
230
          issueCredentialData = None,
231
          issuedCredentialRaw = None,
232
          issuingDID = None,
233
          metaRetries = maxRetries,
×
234
          metaNextRetry = Some(Instant.now()),
235
          metaLastFailure = None,
236
        )
237
      )
×
238
      count <- credentialRepository
×
239
        .createIssueCredentialRecord(record)
240
        .flatMap {
×
241
          case 1 => ZIO.succeed(())
×
242
          case n => ZIO.fail(UnexpectedException(s"Invalid row count result: $n"))
243
        }
244
        .mapError(RepositoryError.apply) @@ CustomMetricsAspect
×
245
        .startRecordingTime(s"${record.id}_issuer_offer_pending_to_sent_ms_gauge")
246
    } yield record
247
  }
248

1✔
249
  override def getIssueCredentialRecordsByStates(
250
      ignoreWithZeroRetries: Boolean,
251
      limit: Int,
252
      states: IssueCredentialRecord.ProtocolState*
253
  ): ZIO[WalletAccessContext, CredentialServiceError, Seq[IssueCredentialRecord]] = {
1✔
254
    for {
×
255
      records <- credentialRepository
×
256
        .getIssueCredentialRecordsByStates(ignoreWithZeroRetries, limit, states: _*)
257
        .mapError(RepositoryError.apply)
258
    } yield records
259
  }
260

×
261
  override def getIssueCredentialRecordsByStatesForAllWallets(
262
      ignoreWithZeroRetries: Boolean,
263
      limit: Int,
264
      states: IssueCredentialRecord.ProtocolState*
265
  ): IO[CredentialServiceError, Seq[IssueCredentialRecord]] = {
×
266
    for {
×
267
      records <- credentialRepository
×
268
        .getIssueCredentialRecordsByStatesForAllWallets(ignoreWithZeroRetries, limit, states: _*)
269
        .mapError(RepositoryError.apply)
270
    } yield records
271
  }
272

1✔
273
  override def receiveCredentialOffer(
274
      offer: OfferCredential
275
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1✔
276
    for {
×
277
      attachment <- ZIO
×
278
        .fromOption(offer.attachments.headOption)
279
        .mapError(_ => CredentialServiceError.UnexpectedError("Missing attachment in credential offer"))
280

×
281
      format <- ZIO.fromOption(attachment.format).mapError(_ => MissingCredentialFormat)
282

1✔
283
      credentialFormat <- format match
×
284
        case value if value == IssueCredentialOfferFormat.JWT.name      => ZIO.succeed(CredentialFormat.JWT)
×
285
        case value if value == IssueCredentialOfferFormat.Anoncred.name => ZIO.succeed(CredentialFormat.AnonCreds)
×
286
        case value                                                      => ZIO.fail(UnsupportedCredentialFormat(value))
287

1✔
288
      _ <- validateCredentialOfferAttachment(credentialFormat, attachment)
1✔
289
      record <- ZIO.succeed(
290
        IssueCredentialRecord(
×
291
          id = DidCommID(),
×
292
          createdAt = Instant.now,
293
          updatedAt = None,
×
294
          thid = DidCommID(offer.thid.getOrElse(offer.id)),
295
          schemaId = None,
296
          credentialDefinitionId = None,
297
          credentialFormat = credentialFormat,
298
          role = Role.Holder,
299
          subjectId = None,
300
          validityPeriod = None,
301
          automaticIssuance = None,
302
          protocolState = IssueCredentialRecord.ProtocolState.OfferReceived,
303
          offerCredentialData = Some(offer),
304
          requestCredentialData = None,
305
          anonCredsRequestMetadata = None,
306
          issueCredentialData = None,
307
          issuedCredentialRaw = None,
308
          issuingDID = None,
309
          metaRetries = maxRetries,
×
310
          metaNextRetry = Some(Instant.now()),
311
          metaLastFailure = None,
312
        )
313
      )
1✔
314
      count <- credentialRepository
×
315
        .createIssueCredentialRecord(record)
316
        .flatMap {
×
317
          case 1 => ZIO.succeed(())
×
318
          case n => ZIO.fail(UnexpectedException(s"Invalid row count result: $n"))
319
        }
320
        .mapError(RepositoryError.apply)
321
    } yield record
322
  }
323

1✔
324
  private[this] def validateCredentialOfferAttachment(
325
      credentialFormat: CredentialFormat,
326
      attachment: AttachmentDescriptor
1✔
327
  ) = for {
1✔
328
    _ <- credentialFormat match
×
329
      case CredentialFormat.JWT =>
330
        attachment.data match
×
331
          case JsonData(json) =>
×
332
            ZIO
×
333
              .attempt(json.asJson.hcursor.downField("json").as[CredentialOfferAttachment])
334
              .mapError(e =>
335
                CredentialServiceError
×
336
                  .UnexpectedError(s"Unexpected error parsing credential offer attachment: ${e.toString}")
337
              )
×
338
          case _ =>
×
339
            ZIO.fail(
340
              CredentialServiceError
×
341
                .UnexpectedError(s"A JSON attachment is expected in the credential offer")
342
            )
×
343
      case CredentialFormat.AnonCreds =>
344
        attachment.data match
×
345
          case Base64(value) =>
×
346
            for {
×
347
              _ <- ZIO
348
                .attempt(CredentialOffer(value))
349
                .mapError(e =>
350
                  CredentialServiceError.UnexpectedError(
×
351
                    s"Unexpected error parsing credential offer attachment: ${e.toString}"
352
                  )
353
                )
354
            } yield ()
×
355
          case _ =>
×
356
            ZIO.fail(
357
              CredentialServiceError
×
358
                .UnexpectedError(s"A Base64 attachment is expected in the credential offer")
359
            )
360
  } yield ()
361

1✔
362
  override def acceptCredentialOffer(
363
      recordId: DidCommID,
364
      maybeSubjectId: Option[String]
365
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1✔
366
    for {
×
367
      record <- getRecordWithState(recordId, ProtocolState.OfferReceived)
1✔
368
      count <- (record.credentialFormat, maybeSubjectId) match
×
369
        case (CredentialFormat.JWT, Some(subjectId)) =>
×
370
          for {
×
371
            _ <- ZIO
×
372
              .fromEither(PrismDID.fromString(subjectId))
373
              .mapError(_ => CredentialServiceError.UnsupportedDidFormat(subjectId))
×
374
            count <- credentialRepository
×
375
              .updateWithSubjectId(recordId, subjectId, ProtocolState.RequestPending)
×
376
              .mapError(RepositoryError.apply) @@ CustomMetricsAspect.startRecordingTime(
×
377
              s"${record.id}_issuance_flow_holder_req_pending_to_generated"
378
            )
379
          } yield count
×
380
        case (CredentialFormat.AnonCreds, None) =>
×
381
          credentialRepository
×
382
            .updateCredentialRecordProtocolState(recordId, ProtocolState.OfferReceived, ProtocolState.RequestPending)
×
383
            .mapError(RepositoryError.apply) @@ CustomMetricsAspect.startRecordingTime(
×
384
            s"${record.id}_issuance_flow_holder_req_pending_to_generated"
385
          )
×
386
        case (format, _) =>
×
387
          ZIO.fail(
388
            CredentialServiceError.UnexpectedError(
×
389
              s"Invalid subjectId input for $format offer acceptance: $maybeSubjectId"
390
            )
391
          )
1✔
392
      _ <- count match
×
393
        case 1 => ZIO.succeed(())
×
UNCOV
394
        case n => ZIO.fail(RecordIdNotFound(recordId))
×
395
      record <- credentialRepository
×
396
        .getIssueCredentialRecord(record.id)
397
        .mapError(RepositoryError.apply)
398
        .flatMap {
×
399
          case None        => ZIO.fail(RecordIdNotFound(recordId))
×
400
          case Some(value) => ZIO.succeed(value)
401
        }
402
    } yield record
403
  }
404

1✔
405
  private[this] def createPresentationPayload(
406
      record: IssueCredentialRecord,
407
      subject: JwtIssuer
408
  ): ZIO[WalletAccessContext, CredentialServiceError, PresentationPayload] = {
1✔
409
    for {
×
410
      maybeOptions <- getOptionsFromOfferCredentialData(record)
411
    } yield {
1✔
412
      W3cPresentationPayload(
×
413
        `@context` = Vector("https://www.w3.org/2018/presentations/v1"),
414
        maybeId = None,
1✔
415
        `type` = Vector("VerifiablePresentation"),
1✔
416
        verifiableCredential = IndexedSeq.empty,
1✔
417
        holder = subject.did.value,
1✔
418
        verifier = IndexedSeq.empty ++ maybeOptions.map(_.domain),
419
        maybeIssuanceDate = None,
420
        maybeExpirationDate = None
1✔
421
      ).toJwtPresentationPayload.copy(maybeNonce = maybeOptions.map(_.challenge))
422
    }
423
  }
424

1✔
425
  private[this] def getLongForm(
426
      did: PrismDID,
×
427
      allowUnpublishedIssuingDID: Boolean = false
428
  ) = {
1✔
429
    for {
1✔
430
      didState <- managedDIDService
×
431
        .getManagedDIDState(did.asCanonical)
×
432
        .mapError(e => RuntimeException(s"Error occurred while getting did from wallet: ${e.toString}"))
×
433
        .someOrFail(RuntimeException(s"Issuer DID does not exist in the wallet: $did"))
434
        .flatMap {
×
435
          case s @ ManagedDIDState(_, _, PublicationState.Published(_)) => ZIO.succeed(s)
×
436
          case s => ZIO.cond(allowUnpublishedIssuingDID, s, RuntimeException(s"Issuer DID must be published: $did"))
437
        }
×
438
      longFormPrismDID = PrismDID.buildLongFormFromOperation(didState.createOperation)
439
    } yield longFormPrismDID
440
  }
441

1✔
442
  private[this] def createJwtIssuer(
443
      jwtIssuerDID: PrismDID,
444
      verificationRelationship: VerificationRelationship
445
  ) = {
1✔
446
    for {
447
      // Automatically infer keyId to use by resolving DID and choose the corresponding VerificationRelationship
×
448
      issuingKeyId <- didService
×
449
        .resolveDID(jwtIssuerDID)
×
450
        .mapError(e => UnexpectedError(s"Error occured while resolving Issuing DID during VC creation: ${e.toString}"))
×
451
        .someOrFail(UnexpectedError(s"Issuing DID resolution result is not found"))
×
452
        .map { case (_, didData) => didData.publicKeys.find(_.purpose == verificationRelationship).map(_.id) }
453
        .someOrFail(
×
454
          UnexpectedError(s"Issuing DID doesn't have a key in ${verificationRelationship.name} to use: $jwtIssuerDID")
455
        )
×
456
      ecKeyPair <- managedDIDService
×
457
        .javaKeyPairWithDID(jwtIssuerDID.asCanonical, issuingKeyId)
×
458
        .mapError(e => UnexpectedError(s"Error occurred while getting issuer key-pair: ${e.toString}"))
459
        .someOrFail(
×
460
          UnexpectedError(s"Issuer key-pair does not exist in the wallet: ${jwtIssuerDID.toString}#$issuingKeyId")
461
        )
×
462
      (privateKey, publicKey) = ecKeyPair
463
      jwtIssuer = JwtIssuer(
×
464
        io.iohk.atala.pollux.vc.jwt.DID(jwtIssuerDID.toString),
×
465
        ES256KSigner(privateKey),
466
        publicKey
467
      )
468
    } yield jwtIssuer
469
  }
470

1✔
471
  override def generateJWTCredentialRequest(
472
      recordId: DidCommID
473
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1✔
474
    for {
×
475
      record <- getRecordWithState(recordId, ProtocolState.RequestPending)
1✔
476
      subjectId <- ZIO
477
        .fromOption(record.subjectId)
×
478
        .mapError(_ => CredentialServiceError.UnexpectedError(s"Subject Id not found in record: ${recordId.value}"))
×
479
      subjectDID <- ZIO
×
480
        .fromEither(PrismDID.fromString(subjectId))
481
        .mapError(_ => CredentialServiceError.UnsupportedDidFormat(subjectId))
1✔
482
      longFormPrismDID <- getLongForm(subjectDID, true).mapError(err => UnexpectedError(err.getMessage))
×
483
      jwtIssuer <- createJwtIssuer(longFormPrismDID, VerificationRelationship.Authentication)
1✔
484
      presentationPayload <- createPresentationPayload(record, jwtIssuer)
×
485
      signedPayload = JwtPresentation.encodeJwt(presentationPayload.toJwtPresentationPayload, jwtIssuer)
1✔
486
      formatAndOffer <- ZIO
×
487
        .fromOption(record.offerCredentialFormatAndData)
×
488
        .mapError(_ => InvalidFlowStateError(s"No offer found for this record: $recordId"))
×
489
      request = createDidCommRequestCredential(formatAndOffer._1, formatAndOffer._2, signedPayload)
×
490
      count <- credentialRepository
×
491
        .updateWithJWTRequestCredential(recordId, request, ProtocolState.RequestGenerated)
×
492
        .mapError(RepositoryError.apply) @@ CustomMetricsAspect.endRecordingTime(
×
493
        s"${record.id}_issuance_flow_holder_req_pending_to_generated",
494
        "issuance_flow_holder_req_pending_to_generated_ms_gauge"
×
495
      ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_holder_req_generated_to_sent")
1✔
496
      _ <- count match
×
497
        case 1 => ZIO.succeed(())
×
498
        case n => ZIO.fail(RecordIdNotFound(recordId))
×
499
      record <- credentialRepository
×
500
        .getIssueCredentialRecord(record.id)
501
        .mapError(RepositoryError.apply)
502
        .flatMap {
×
503
          case None        => ZIO.fail(RecordIdNotFound(recordId))
×
504
          case Some(value) => ZIO.succeed(value)
505
        }
506
    } yield record
507
  }
508

1✔
509
  override def generateAnonCredsCredentialRequest(
510
      recordId: DidCommID
511
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1✔
512
    for {
×
513
      record <- getRecordWithState(recordId, ProtocolState.RequestPending)
1✔
514
      offerCredential <- ZIO
515
        .fromOption(record.offerCredentialData)
×
516
        .mapError(_ => InvalidFlowStateError(s"No offer found for this record: ${record.id}"))
×
517
      body = RequestCredential.Body(goal_code = Some("Request Credential"))
1✔
518
      createCredentialRequest <- createAnonCredsRequestCredential(offerCredential)
×
519
      attachments = Seq(
×
520
        AttachmentDescriptor.buildBase64Attachment(
521
          mediaType = Some("application/json"),
522
          format = Some(IssueCredentialRequestFormat.Anoncred.name),
×
523
          payload = createCredentialRequest.request.data.getBytes()
524
        )
525
      )
526
      requestMetadata = createCredentialRequest.metadata
×
527
      request = RequestCredential(
528
        body = body,
529
        attachments = attachments,
530
        from = offerCredential.to,
531
        to = offerCredential.from,
532
        thid = offerCredential.thid
533
      )
×
534
      count <- credentialRepository
×
535
        .updateWithAnonCredsRequestCredential(recordId, request, requestMetadata, ProtocolState.RequestGenerated)
×
536
        .mapError(RepositoryError.apply) @@ CustomMetricsAspect.endRecordingTime(
×
537
        s"${record.id}_issuance_flow_holder_req_pending_to_generated",
538
        "issuance_flow_holder_req_pending_to_generated_ms_gauge"
×
539
      ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_holder_req_generated_to_sent")
1✔
540
      _ <- count match
×
541
        case 1 => ZIO.succeed(())
×
542
        case n => ZIO.fail(RecordIdNotFound(recordId))
1✔
543
      record <- credentialRepository
×
544
        .getIssueCredentialRecord(record.id)
545
        .mapError(RepositoryError.apply)
546
        .flatMap {
×
547
          case None        => ZIO.fail(RecordIdNotFound(recordId))
×
548
          case Some(value) => ZIO.succeed(value)
549
        }
550
    } yield record
551
  }
552

1✔
553
  private[this] def createAnonCredsRequestCredential(offerCredential: OfferCredential) = {
1✔
554
    for {
×
555
      attachmentData <- ZIO
556
        .fromOption(
557
          offerCredential.attachments
×
558
            .find(_.format.contains(IssueCredentialOfferFormat.Anoncred.name))
×
559
            .map(_.data)
×
560
            .flatMap {
×
561
              case Base64(value) => Some(new String(java.util.Base64.getDecoder.decode(value)))
×
562
              case _             => None
563
            }
564
        )
×
565
        .mapError(_ => InvalidFlowStateError(s"No AnonCreds offer attachment found"))
566
      credentialOffer = anoncreds.CredentialOffer(attachmentData)
×
567
      _ <- ZIO.logInfo(s"Cred def ID => ${credentialOffer.getCredDefId}")
×
568
      credDefContent <- uriDereferencer
×
569
        .dereference(new URI(credentialOffer.getCredDefId))
570
        .mapError(err => UnexpectedError(err.toString))
571
      credentialDefinition = anoncreds.CredentialDefinition(credDefContent)
1✔
572
      linkSecret <- linkSecretService
×
573
        .fetchOrCreate()
574
        .mapError(e => CredentialServiceError.LinkSecretError.apply(e.cause))
×
575
      createCredentialRequest = AnoncredLib.createCredentialRequest(linkSecret, credentialDefinition, credentialOffer)
576
    } yield createCredentialRequest
577
  }
578

1✔
579
  override def receiveCredentialRequest(
580
      request: RequestCredential
581
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1✔
582
    for {
×
583
      record <- getRecordFromThreadIdWithState(
×
584
        request.thid.map(DidCommID(_)),
585
        ignoreWithZeroRetries = true,
586
        ProtocolState.OfferPending,
587
        ProtocolState.OfferSent
588
      )
1✔
589
      _ <- credentialRepository
×
590
        .updateWithJWTRequestCredential(record.id, request, ProtocolState.RequestReceived)
591
        .flatMap {
×
592
          case 1 => ZIO.succeed(())
×
593
          case n => ZIO.fail(UnexpectedException(s"Invalid row count result: $n"))
594
        }
595
        .mapError(RepositoryError.apply)
×
596
      record <- credentialRepository
×
597
        .getIssueCredentialRecord(record.id)
598
        .mapError(RepositoryError.apply)
599
        .someOrFail(RecordIdNotFound(record.id))
600
    } yield record
601
  }
602

1✔
603
  override def acceptCredentialRequest(
604
      recordId: DidCommID
605
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1✔
606
    for {
×
607
      record <- getRecordWithState(recordId, ProtocolState.RequestReceived)
1✔
608
      request <- ZIO
609
        .fromOption(record.requestCredentialData)
×
610
        .mapError(_ => InvalidFlowStateError(s"No request found for this record: $recordId"))
×
611
      issue = createDidCommIssueCredential(request)
1✔
612
      count <- credentialRepository
×
613
        .updateWithIssueCredential(recordId, issue, ProtocolState.CredentialPending)
×
614
        .mapError(RepositoryError.apply) @@ CustomMetricsAspect.startRecordingTime(
×
615
        s"${record.id}_issuance_flow_issuer_credential_pending_to_generated"
616
      )
1✔
617
      _ <- count match
×
618
        case 1 => ZIO.succeed(())
×
619
        case n => ZIO.fail(RecordIdNotFound(recordId))
1✔
620
      record <- credentialRepository
×
621
        .getIssueCredentialRecord(record.id)
622
        .mapError(RepositoryError.apply)
623
        .someOrFail(RecordIdNotFound(record.id))
624
    } yield record
625
  }
626

1✔
627
  override def receiveCredentialIssue(
628
      issueCredential: IssueCredential
629
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
630
    // TODO We can get rid of this 'raw' representation stored in DB, because it is not used.
×
631
    val rawIssuedCredential = issueCredential.attachments.map(_.data.asJson.noSpaces).headOption.getOrElse("???")
1✔
632
    for {
633
      // TODO Move this type of generic/reusable code to a helper trait
×
634
      attachmentFormatAndData <- ZIO.succeed {
635
        import IssueCredentialIssuedFormat.{Anoncred, JWT}
636
        issueCredential.attachments
×
637
          .collectFirst {
×
638
            case AttachmentDescriptor(_, _, Base64(v), Some(JWT.name), _, _, _, _)      => (JWT, v)
×
639
            case AttachmentDescriptor(_, _, Base64(v), Some(Anoncred.name), _, _, _, _) => (Anoncred, v)
640
          }
×
641
          .map { case (f, v) => (f, java.util.Base64.getUrlDecoder.decode(v)) }
UNCOV
642
      }
×
643
      record <- getRecordFromThreadIdWithState(
×
644
        issueCredential.thid.map(DidCommID(_)),
645
        ignoreWithZeroRetries = true,
646
        ProtocolState.RequestPending,
647
        ProtocolState.RequestSent
648
      )
1✔
649
      _ <- attachmentFormatAndData match
×
650
        case Some(IssueCredentialIssuedFormat.JWT, _)       => ZIO.succeed(())
×
651
        case Some(IssueCredentialIssuedFormat.Anoncred, ba) => processAnonCredsCredential(record, ba)
×
652
        case _ => ZIO.fail(UnexpectedError("No AnonCreds or JWT credential attachment found"))
1✔
653
      _ <- credentialRepository
×
654
        .updateWithIssuedRawCredential(
655
          record.id,
656
          issueCredential,
657
          rawIssuedCredential,
658
          ProtocolState.CredentialReceived
659
        )
660
        .flatMap {
×
661
          case 1 => ZIO.succeed(())
×
662
          case n => ZIO.fail(UnexpectedException(s"Invalid row count result: $n"))
663
        }
664
        .mapError(RepositoryError.apply)
×
665
      record <- credentialRepository
×
666
        .getIssueCredentialRecord(record.id)
667
        .mapError(RepositoryError.apply)
668
        .someOrFail(RecordIdNotFound(record.id))
669
    } yield record
670
  }
671

1✔
672
  private[this] def processAnonCredsCredential(record: IssueCredentialRecord, credentialBytes: Array[Byte]) = {
1✔
673
    for {
×
674
      credential <- ZIO.succeed(anoncreds.Credential(new String(credentialBytes)))
1✔
675
      credDefContent <- uriDereferencer
×
676
        .dereference(new URI(credential.getCredDefId))
677
        .mapError(err => UnexpectedError(err.toString))
UNCOV
678
      credentialDefinition = anoncreds.CredentialDefinition(credDefContent)
×
679
      metadata <- ZIO
680
        .fromOption(record.anonCredsRequestMetadata)
×
681
        .mapError(_ => CredentialServiceError.UnexpectedError(s"No request metadata Id found un record: ${record.id}"))
×
682
      linkSecret <- linkSecretService
×
683
        .fetchOrCreate()
684
        .mapError(e => CredentialServiceError.LinkSecretError.apply(e.cause))
1✔
685
      _ <- ZIO
686
        .attempt(
×
687
          AnoncredLib.processCredential(
×
688
            anoncreds.Credential(new String(credentialBytes)),
689
            metadata,
690
            linkSecret,
691
            credentialDefinition
692
          )
693
        )
×
694
        .mapError(error => UnexpectedError(s"AnonCreds credential processing error: ${error.getMessage}"))
695
    } yield ()
696
  }
697

1✔
698
  override def markOfferSent(
699
      recordId: DidCommID
700
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] =
1✔
701
    updateCredentialRecordProtocolState(
702
      recordId,
703
      IssueCredentialRecord.ProtocolState.OfferPending,
704
      IssueCredentialRecord.ProtocolState.OfferSent
705
    )
706

1✔
707
  override def markRequestSent(
708
      recordId: DidCommID
UNCOV
709
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] =
×
710
    updateCredentialRecordProtocolState(
711
      recordId,
712
      IssueCredentialRecord.ProtocolState.RequestGenerated,
713
      IssueCredentialRecord.ProtocolState.RequestSent
1✔
714
    ) @@ CustomMetricsAspect.endRecordingTime(
1✔
715
      s"${recordId}_issuance_flow_holder_req_generated_to_sent",
716
      "issuance_flow_holder_req_generated_to_sent_ms_gauge"
717
    )
718

1✔
719
  private[this] def markCredentialGenerated(
720
      record: IssueCredentialRecord,
721
      issueCredential: IssueCredential
722
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1✔
723
    for {
×
724
      count <- credentialRepository
×
725
        .updateWithIssueCredential(
726
          record.id,
727
          issueCredential,
728
          IssueCredentialRecord.ProtocolState.CredentialGenerated
729
        )
×
730
        .mapError(RepositoryError.apply) @@ CustomMetricsAspect.endRecordingTime(
×
731
        s"${record.id}_issuance_flow_issuer_credential_pending_to_generated",
732
        "issuance_flow_issuer_credential_pending_to_generated_ms_gauge"
×
733
      ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_issuer_credential_generated_to_sent")
1✔
734
      _ <- count match
×
735
        case 1 => ZIO.succeed(())
×
736
        case n => ZIO.fail(RecordIdNotFound(record.id))
×
737
      record <- credentialRepository
×
738
        .getIssueCredentialRecord(record.id)
739
        .mapError(RepositoryError.apply)
740
        .flatMap {
×
741
          case None        => ZIO.fail(RecordIdNotFound(record.id))
×
742
          case Some(value) => ZIO.succeed(value)
743
        }
744

745
    } yield record
746
  }
747

1✔
748
  override def markCredentialSent(
749
      recordId: DidCommID
750
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] =
×
751
    updateCredentialRecordProtocolState(
752
      recordId,
753
      IssueCredentialRecord.ProtocolState.CredentialGenerated,
754
      IssueCredentialRecord.ProtocolState.CredentialSent
1✔
755
    ) @@ CustomMetricsAspect.endRecordingTime(
1✔
756
      s"${recordId}_issuance_flow_issuer_credential_generated_to_sent",
757
      "issuance_flow_issuer_credential_generated_to_sent_ms_gauge"
758
    )
759

×
760
  override def reportProcessingFailure(
761
      recordId: DidCommID,
762
      failReason: Option[String]
763
  ): ZIO[WalletAccessContext, CredentialServiceError, Unit] =
×
764
    credentialRepository
×
765
      .updateAfterFail(recordId, failReason)
766
      .mapError(RepositoryError.apply)
767
      .flatMap {
×
768
        case 1 => ZIO.unit
×
769
        case n => ZIO.fail(UnexpectedError(s"Invalid number of records updated: $n"))
770
      }
771

1✔
772
  private[this] def getRecordWithState(
773
      recordId: DidCommID,
774
      state: ProtocolState
775
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1✔
776
    for {
×
777
      maybeRecord <- credentialRepository
×
778
        .getIssueCredentialRecord(recordId)
779
        .mapError(RepositoryError.apply)
×
780
      record <- ZIO
781
        .fromOption(maybeRecord)
782
        .mapError(_ => RecordIdNotFound(recordId))
1✔
783
      _ <- record.protocolState match {
×
784
        case s if s == state => ZIO.unit
×
785
        case state           => ZIO.fail(InvalidFlowStateError(s"Invalid protocol state for operation: $state"))
786
      }
787
    } yield record
788
  }
789

1✔
790
  private[this] def getRecordFromThreadIdWithState(
791
      thid: Option[DidCommID],
792
      ignoreWithZeroRetries: Boolean,
793
      states: ProtocolState*
794
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1✔
795
    for {
×
796
      thid <- ZIO
797
        .fromOption(thid)
798
        .mapError(_ => UnexpectedError("No `thid` found in credential request"))
×
799
      maybeRecord <- credentialRepository
×
800
        .getIssueCredentialRecordByThreadId(thid, ignoreWithZeroRetries)
801
        .mapError(RepositoryError.apply)
1✔
802
      record <- ZIO
803
        .fromOption(maybeRecord)
804
        .mapError(_ => ThreadIdNotFound(thid))
1✔
805
      _ <- record.protocolState match {
×
806
        case s if states.contains(s) => ZIO.unit
×
807
        case state                   => ZIO.fail(InvalidFlowStateError(s"Invalid protocol state for operation: $state"))
808
      }
809
    } yield record
810
  }
811

1✔
812
  private[this] def createJWTDidCommOfferCredential(
813
      pairwiseIssuerDID: DidId,
814
      pairwiseHolderDID: DidId,
815
      maybeSchemaId: Option[String],
816
      claims: Seq[Attribute],
817
      thid: DidCommID,
818
      challenge: String,
819
      domain: String
820
  ) = {
1✔
821
    for {
×
822
      credentialPreview <- ZIO.succeed(CredentialPreview(schema_id = maybeSchemaId, attributes = claims))
×
823
      body = OfferCredential.Body(
824
        goal_code = Some("Offer Credential"),
825
        credential_preview = credentialPreview,
UNCOV
826
      )
×
827
      attachments <- ZIO.succeed(
×
828
        Seq(
×
829
          AttachmentDescriptor.buildJsonAttachment(
830
            mediaType = Some("application/json"),
831
            format = Some(IssueCredentialOfferFormat.JWT.name),
832
            payload = PresentationAttachment(
833
              Some(Options(challenge, domain)),
×
834
              PresentationDefinition(format = Some(ClaimFormat(jwt = Some(Jwt(alg = Seq("ES256K"), proof_type = Nil)))))
835
            )
836
          )
837
        )
838
      )
1✔
839
    } yield OfferCredential(
840
      body = body,
841
      attachments = attachments,
842
      from = pairwiseIssuerDID,
843
      to = pairwiseHolderDID,
1✔
844
      thid = Some(thid.value)
845
    )
846
  }
847

1✔
848
  private[this] def createAnonCredsDidCommOfferCredential(
849
      pairwiseIssuerDID: DidId,
850
      pairwiseHolderDID: DidId,
851
      schemaId: String,
852
      credentialDefinitionGUID: UUID,
853
      claims: Seq[Attribute],
854
      thid: DidCommID,
855
      credentialDefinitionId: String
856
  ) = {
1✔
857
    for {
1✔
858
      credentialPreview <- ZIO.succeed(CredentialPreview(schema_id = Some(schemaId), attributes = claims))
×
859
      body = OfferCredential.Body(
860
        goal_code = Some("Offer Credential"),
861
        credential_preview = credentialPreview,
UNCOV
862
      )
×
863
      attachments <- createAnonCredsCredentialOffer(credentialDefinitionGUID, credentialDefinitionId).map { offer =>
×
864
        Seq(
×
865
          AttachmentDescriptor.buildBase64Attachment(
866
            mediaType = Some("application/json"),
867
            format = Some(IssueCredentialOfferFormat.Anoncred.name),
×
868
            payload = offer.data.getBytes()
869
          )
870
        )
871
      }
1✔
872
    } yield OfferCredential(
873
      body = body,
874
      attachments = attachments,
875
      from = pairwiseIssuerDID,
876
      to = pairwiseHolderDID,
1✔
877
      thid = Some(thid.value)
878
    )
879
  }
880

1✔
881
  private[this] def createAnonCredsCredentialOffer(credentialDefinitionGUID: UUID, credentialDefinitionId: String) =
1✔
882
    for {
1✔
883
      credentialDefinition <- credentialDefinitionService
×
884
        .getByGUID(credentialDefinitionGUID)
885
        .mapError(e => CredentialServiceError.UnexpectedError(e.toString))
×
886
      cd = anoncreds.CredentialDefinition(credentialDefinition.definition.toString)
×
887
      kcp = anoncreds.CredentialKeyCorrectnessProof(credentialDefinition.keyCorrectnessProof.toString)
×
888
      maybeCredentialDefinitionSecret <- genericSecretStorage
889
        .get[UUID, CredentialDefinitionSecret](credentialDefinition.guid)
UNCOV
890
        .orDie
×
891
      credentialDefinitionSecret <- ZIO
892
        .fromOption(maybeCredentialDefinitionSecret)
893
        .mapError(_ => CredentialServiceError.CredentialDefinitionPrivatePartNotFound(credentialDefinition.guid))
×
894
      cdp = anoncreds.CredentialDefinitionPrivate(credentialDefinitionSecret.json.toString)
895
      createCredentialDefinition = CreateCredentialDefinition(cd, cdp, kcp)
×
896
      offer = AnoncredLib.createOffer(createCredentialDefinition, credentialDefinitionId)
897
    } yield offer
898

1✔
899
  private[this] def createDidCommRequestCredential(
900
      format: IssueCredentialOfferFormat,
901
      offer: OfferCredential,
902
      signedPresentation: JWT
903
  ): RequestCredential = {
1✔
904
    RequestCredential(
905
      body = RequestCredential.Body(
906
        goal_code = offer.body.goal_code,
907
        comment = offer.body.comment,
UNCOV
908
      ),
×
909
      attachments = Seq(
910
        AttachmentDescriptor
1✔
911
          .buildBase64Attachment(
912
            mediaType = Some("application/json"),
913
            format = Some(format.name),
914
            // FIXME copy payload will probably not work for anoncreds!
1✔
915
            payload = signedPresentation.value.getBytes(),
916
          )
917
      ),
1✔
918
      thid = offer.thid.orElse(Some(offer.id)),
919
      from = offer.to,
920
      to = offer.from
921
    )
922
  }
923

1✔
924
  private[this] def createDidCommIssueCredential(request: RequestCredential): IssueCredential = {
1✔
925
    IssueCredential(
926
      body = IssueCredential.Body(
927
        goal_code = request.body.goal_code,
928
        comment = request.body.comment,
929
        replacement_id = None,
930
        more_available = None,
931
      ),
1✔
932
      attachments = Seq(), // FIXME !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1✔
933
      thid = request.thid.orElse(Some(request.id)),
934
      from = request.to,
935
      to = request.from
936
    )
937
  }
938

939
  /** this is an auxiliary function.
940
    *
941
    * @note
942
    *   Between updating and getting the CredentialRecord back the CredentialRecord can be updated by other operations
943
    *   in the middle.
944
    *
945
    * TODO: this should be improved to behave exactly like atomic operation.
946
    */
1✔
947
  private[this] def updateCredentialRecordProtocolState(
948
      id: DidCommID,
949
      from: IssueCredentialRecord.ProtocolState,
950
      to: IssueCredentialRecord.ProtocolState
951
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1✔
952
    for {
×
953
      record <- credentialRepository
×
954
        .updateCredentialRecordProtocolState(id, from, to)
955
        .mapError(RepositoryError.apply)
956
        .flatMap {
×
957
          case 0 =>
×
958
            credentialRepository
×
959
              .getIssueCredentialRecord(id)
960
              .mapError(RepositoryError.apply)
961
              .flatMap {
×
962
                case None => ZIO.fail(RecordIdNotFound(id))
×
963
                case Some(record) if record.protocolState == to => // Not update by is alredy on the desirable state
×
964
                  ZIO.succeed(record)
×
965
                case Some(record) =>
×
966
                  ZIO.fail(
967
                    OperationNotExecuted(
968
                      id,
×
969
                      s"CredentialRecord was not updated because have the ProtocolState ${record.protocolState}"
970
                    )
971
                  )
972
              }
×
973
          case 1 =>
×
974
            credentialRepository
×
975
              .getIssueCredentialRecord(id)
976
              .mapError(RepositoryError.apply)
977
              .flatMap {
×
978
                case None => ZIO.fail(RecordIdNotFound(id))
×
979
                case Some(record) =>
×
980
                  ZIO
981
                    .logError(
×
982
                      s"The CredentialRecord ($id) is expected to be on the ProtocolState '$to' after updating it"
983
                    )
984
                    .when(record.protocolState != to)
985
                    .as(record)
986
              }
×
987
          case n => ZIO.fail(UnexpectedError(s"Invalid row count result: $n"))
988
        }
989
    } yield record
990
  }
991

1✔
992
  override def generateJWTCredential(
993
      recordId: DidCommID,
994
      statusListRegistryUrl: String,
995
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1✔
996
    for {
×
997
      record <- getRecordWithState(recordId, ProtocolState.CredentialPending)
×
998
      issuingDID <- ZIO
999
        .fromOption(record.issuingDID)
×
1000
        .mapError(_ => CredentialServiceError.UnexpectedError(s"Issuing Id not found in record: ${recordId.value}"))
×
1001
      issue <- ZIO
1002
        .fromOption(record.issueCredentialData)
1003
        .mapError(_ =>
×
1004
          CredentialServiceError.UnexpectedError(s"Issue credential data not found in record: ${recordId.value}")
UNCOV
1005
        )
×
1006
      longFormPrismDID <- getLongForm(issuingDID, true).mapError(err => UnexpectedError(err.getMessage))
1✔
1007
      jwtIssuer <- createJwtIssuer(longFormPrismDID, VerificationRelationship.AssertionMethod)
×
1008
      offerCredentialData <- ZIO
1009
        .fromOption(record.offerCredentialData)
1010
        .mapError(_ =>
1011
          CredentialServiceError.CreateCredentialPayloadFromRecordError(
×
1012
            new Throwable("Could not extract claims from \"requestCredential\" DIDComm message")
1013
          )
1014
        )
1015
      preview = offerCredentialData.body.credential_preview
×
1016
      claims <- CredentialService.convertAttributesToJsonClaims(preview.body.attributes)
×
1017
      maybeOfferOptions <- getOptionsFromOfferCredentialData(record)
1✔
1018
      requestJwt <- getJwtFromRequestCredentialData(record)
1019

1020
      // domain/challenge validation + JWT verification
1✔
1021
      jwtPresentation <- validateRequestCredentialDataProof(maybeOfferOptions, requestJwt).tapBoth(
1022
        error =>
×
1023
          ZIO.logErrorCause("JWT Presentation Validation Failed!!", Cause.fail(error)) *> credentialRepository
×
1024
            .updateCredentialRecordProtocolState(
1025
              record.id,
1026
              ProtocolState.CredentialPending,
1027
              ProtocolState.ProblemReportPending
1028
            )
1029
            .mapError(t => RepositoryError(t)),
×
1030
        payload => ZIO.logInfo("JWT Presentation Validation Successful!")
1031
      )
×
1032
      issuanceDate = Instant.now()
1✔
1033
      credentialStatus <- allocateNewCredentialInStatusListForWallet(record, statusListRegistryUrl, jwtIssuer)
1034
      // TODO: get schema when schema registry is available if schema ID is provided
×
1035
      w3Credential = W3cCredentialPayload(
×
1036
        `@context` = Set(
1037
          "https://www.w3.org/2018/credentials/v1"
1038
        ), // TODO: his information should come from Schema registry by record.schemaId
1039
        maybeId = None,
1040
        `type` =
×
1041
          Set("VerifiableCredential"), // TODO: This information should come from Schema registry by record.schemaId
1042
        issuer = jwtIssuer.did,
1043
        issuanceDate = issuanceDate,
×
1044
        maybeExpirationDate = record.validityPeriod.map(sec => issuanceDate.plusSeconds(sec.toLong)),
1045
        maybeCredentialSchema =
×
1046
          record.schemaId.map(id => io.iohk.atala.pollux.vc.jwt.CredentialSchema(id, VC_JSON_SCHEMA_TYPE)),
1047
        maybeCredentialStatus = Some(credentialStatus),
×
1048
        credentialSubject = claims.add("id", jwtPresentation.iss.asJson).asJson,
1049
        maybeRefreshService = None,
1050
        maybeEvidence = None,
1051
        maybeTermsOfUse = None
1052
      )
×
1053
      signedJwtCredential = W3CCredential.toEncodedJwt(w3Credential, jwtIssuer)
×
1054
      issueCredential = IssueCredential.build(
1055
        fromDID = issue.from,
1056
        toDID = issue.to,
1057
        thid = issue.thid,
×
1058
        credentials = Seq(IssueCredentialIssuedFormat.JWT -> signedJwtCredential.value.getBytes)
1059
      )
×
1060
      record <- markCredentialGenerated(record, issueCredential)
1061
    } yield record
1062
  }
1063

1✔
1064
  private[this] def allocateNewCredentialInStatusListForWallet(
1065
      record: IssueCredentialRecord,
1066
      statusListRegistryUrl: String,
1067
      jwtIssuer: JwtIssuer
1068
  ): ZIO[WalletAccessContext, CredentialServiceError, CredentialStatus] = {
1✔
1069
    for {
×
1070
      lastStatusList <- credentialStatusListRepository.getLatestOfTheWallet.mapError(RepositoryError.apply)
1✔
1071
      currentStatusList <- lastStatusList
×
1072
        .fold(credentialStatusListRepository.createNewForTheWallet(jwtIssuer, statusListRegistryUrl))(
×
1073
          ZIO.succeed(_)
1074
        )
1075
        .mapError(RepositoryError.apply)
1076
      size = currentStatusList.size
UNCOV
1077
      lastUsedIndex = currentStatusList.lastUsedIndex
×
1078
      statusListToBeUsed <- issueCredentialSem.withPermit {
×
1079
        for {
1080
          statusListToBeUsed <-
1081
            if lastUsedIndex < size then ZIO.succeed(currentStatusList)
1082
            else
1083
              credentialStatusListRepository
1084
                .createNewForTheWallet(jwtIssuer, statusListRegistryUrl)
1085
                .mapError(RepositoryError.apply)
1086

×
1087
          _ <- credentialStatusListRepository
×
1088
            .allocateSpaceForCredential(
1089
              issueCredentialRecordId = record.id,
1090
              credentialStatusListId = statusListToBeUsed.id,
1091
              statusListIndex = statusListToBeUsed.lastUsedIndex + 1
1092
            )
1093
            .mapError(RepositoryError.apply)
1094
        } yield statusListToBeUsed
1095
      }
1096
    } yield CredentialStatus(
1✔
1097
      id = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}#${statusListToBeUsed.lastUsedIndex + 1}",
1098
      `type` = "StatusList2021Entry",
1099
      statusPurpose = StatusPurpose.Revocation,
1100
      statusListIndex = lastUsedIndex + 1,
1✔
1101
      statusListCredential = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}"
1102
    )
1103
  }
1104

1✔
1105
  override def generateAnonCredsCredential(
1106
      recordId: DidCommID
1107
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1✔
1108
    for {
×
1109
      record <- getRecordWithState(recordId, ProtocolState.CredentialPending)
1✔
1110
      requestCredential <- ZIO
1111
        .fromOption(record.requestCredentialData)
×
1112
        .mapError(_ => InvalidFlowStateError(s"No request found for this record: ${record.id}"))
×
1113
      body = IssueCredential.Body(goal_code = Some("Issue Credential"))
1✔
1114
      attachments <- createAnonCredsCredential(record).map { credential =>
×
1115
        Seq(
×
1116
          AttachmentDescriptor.buildBase64Attachment(
1117
            mediaType = Some("application/json"),
1118
            format = Some(IssueCredentialIssuedFormat.Anoncred.name),
×
1119
            payload = credential.data.getBytes()
1120
          )
1121
        )
1122
      }
×
1123
      issueCredential = IssueCredential(
1124
        body = body,
1125
        attachments = attachments,
1126
        from = requestCredential.to,
1127
        to = requestCredential.from,
1128
        thid = requestCredential.thid
UNCOV
1129
      )
×
1130
      record <- markCredentialGenerated(record, issueCredential)
1131
    } yield record
1132
  }
1133

1✔
1134
  private[this] def createAnonCredsCredential(record: IssueCredentialRecord) = {
1✔
1135
    for {
×
1136
      credentialDefinitionId <- ZIO
1137
        .fromOption(record.credentialDefinitionId)
×
1138
        .mapError(_ => CredentialServiceError.UnexpectedError(s"No cred def Id found un record: ${record.id}"))
×
1139
      credentialDefinition <- credentialDefinitionService
×
1140
        .getByGUID(credentialDefinitionId)
1141
        .mapError(e => CredentialServiceError.UnexpectedError(e.toString))
×
1142
      cd = anoncreds.CredentialDefinition(credentialDefinition.definition.toString)
×
1143
      offerCredential <- ZIO
1144
        .fromOption(record.offerCredentialData)
×
1145
        .mapError(_ => InvalidFlowStateError(s"No offer found for this record: ${record.id}"))
1✔
1146
      offerCredentialAttachmentData <- ZIO
1147
        .fromOption(
1148
          offerCredential.attachments
×
1149
            .find(_.format.contains(IssueCredentialOfferFormat.Anoncred.name))
×
1150
            .map(_.data)
×
1151
            .flatMap {
×
1152
              case Base64(value) => Some(new String(java.util.Base64.getUrlDecoder.decode(value)))
×
1153
              case _             => None
1154
            }
1155
        )
×
1156
        .mapError(_ => InvalidFlowStateError(s"No AnonCreds offer attachment found"))
1157
      credentialOffer = anoncreds.CredentialOffer(offerCredentialAttachmentData)
×
1158
      requestCredential <- ZIO
1159
        .fromOption(record.requestCredentialData)
×
UNCOV
1160
        .mapError(_ => InvalidFlowStateError(s"No request found for this record: ${record.id}"))
×
1161
      requestCredentialAttachmentData <- ZIO
1162
        .fromOption(
1163
          requestCredential.attachments
×
1164
            .find(_.format.contains(IssueCredentialRequestFormat.Anoncred.name))
×
1165
            .map(_.data)
×
1166
            .flatMap {
×
1167
              case Base64(value) => Some(new String(java.util.Base64.getUrlDecoder.decode(value)))
×
1168
              case _             => None
1169
            }
1170
        )
×
1171
        .mapError(_ => InvalidFlowStateError(s"No AnonCreds request attachment found"))
1172
      credentialRequest = anoncreds.CredentialRequest(requestCredentialAttachmentData)
×
1173
      attrValues = offerCredential.body.credential_preview.body.attributes.map { attr =>
1174
        (attr.name, attr.value)
1175
      }
1✔
1176
      maybeCredentialDefinitionSecret <- genericSecretStorage
1177
        .get[UUID, CredentialDefinitionSecret](credentialDefinition.guid)
1178
        .orDie
×
1179
      credentialDefinitionSecret <- ZIO
1180
        .fromOption(maybeCredentialDefinitionSecret)
1181
        .mapError(_ => CredentialServiceError.CredentialDefinitionPrivatePartNotFound(credentialDefinition.guid))
×
1182
      cdp = anoncreds.CredentialDefinitionPrivate(credentialDefinitionSecret.json.toString)
×
1183
      credential = AnoncredLib.createCredential(
1184
        cd,
1185
        cdp,
1186
        credentialOffer,
1187
        credentialRequest,
1188
        attrValues
1189
      )
1190
    } yield credential
1191
  }
1192

1✔
1193
  private[this] def getOptionsFromOfferCredentialData(record: IssueCredentialRecord) = {
1✔
1194
    for {
×
1195
      offer <- ZIO
1196
        .fromOption(record.offerCredentialData)
×
1197
        .mapError(_ => CredentialServiceError.UnexpectedError(s"Offer data not found in record: ${record.id}"))
×
1198
      attachmentDescriptor <- ZIO
×
1199
        .fromOption(offer.attachments.headOption)
×
1200
        .mapError(_ => UnexpectedError(s"Attachments not found in record: ${record.id}"))
1✔
1201
      json <- attachmentDescriptor.data match
×
1202
        case JsonData(json) => ZIO.succeed(json.asJson)
×
1203
        case _              => ZIO.fail(UnexpectedError(s"Attachment doesn't contain JsonData: ${record.id}"))
1✔
1204
      maybeOptions <- ZIO
×
1205
        .fromEither(json.as[PresentationAttachment].map(_.options))
×
1206
        .mapError(df => UnexpectedError(df.getMessage))
1207
    } yield maybeOptions
1208
  }
1209

1✔
1210
  private[this] def getJwtFromRequestCredentialData(record: IssueCredentialRecord) = {
1✔
1211
    for {
×
1212
      request <- ZIO
1213
        .fromOption(record.requestCredentialData)
×
1214
        .mapError(_ => CredentialServiceError.UnexpectedError(s"Request data not found in record: ${record.id}"))
1✔
1215
      attachmentDescriptor <- ZIO
×
1216
        .fromOption(request.attachments.headOption)
×
1217
        .mapError(_ => UnexpectedError(s"Attachments not found in record: ${record.id}"))
1✔
1218
      jwt <- attachmentDescriptor.data match
×
1219
        case Base64(b64) =>
×
1220
          ZIO.succeed {
×
1221
            val base64Decoded = new String(java.util.Base64.getDecoder().decode(b64))
×
1222
            JWT(base64Decoded)
1223
          }
×
1224
        case _ => ZIO.fail(UnexpectedError(s"Attachment doesn't contain Base64Data: ${record.id}"))
1225
    } yield jwt
1226
  }
1227

1✔
1228
  private[this] def validateRequestCredentialDataProof(maybeOptions: Option[Options], jwt: JWT) = {
1✔
1229
    for {
1✔
1230
      _ <- maybeOptions match
×
1231
        case None => ZIO.unit
×
1232
        case Some(options) =>
×
1233
          JwtPresentation.validatePresentation(jwt, options.domain, options.challenge) match
×
1234
            case ZValidation.Success(log, value) => ZIO.unit
×
1235
            case ZValidation.Failure(log, error) =>
×
1236
              ZIO.fail(CredentialRequestValidationError("JWT presentation domain/validation validation failed"))
1237

×
1238
      clock = java.time.Clock.system(ZoneId.systemDefault)
UNCOV
1239

×
1240
      verificationResult <- JwtPresentation
1241
        .verify(
1242
          jwt,
×
1243
          JwtPresentation.PresentationVerificationOptions(
1244
            maybeProofPurpose = Some(VerificationRelationship.Authentication),
1245
            verifySignature = true,
1246
            verifyDates = false,
1247
            leeway = Duration.Zero
1248
          )
×
NEW
1249
        )(didResolver, (_: String) => ZIO.succeed(""))(clock)
×
1250
        .mapError(errors => CredentialRequestValidationError(s"JWT presentation verification failed: $errors"))
1251

1✔
1252
      result <- verificationResult match
×
1253
        case ZValidation.Success(log, value) => ZIO.unit
×
1254
        case ZValidation.Failure(log, error) =>
×
1255
          ZIO.fail(CredentialRequestValidationError(s"JWT presentation verification failed: $error"))
1256

×
1257
      jwtPresentation <- ZIO
×
1258
        .fromTry(JwtPresentation.decodeJwt(jwt))
×
1259
        .mapError(t => CredentialRequestValidationError(s"JWT presentation decoding failed: ${t.getMessage()}"))
1260
    } yield jwtPresentation
1261
  }
1262

1263
}
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