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

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

30 Apr 2024 01:30PM UTC coverage: 49.483% (-0.07%) from 49.553%
8895686545

Pull #998

FabioPinheiro
wth
Pull Request #998: [DO NOT MERGE] ci: fix scala-steward after moving repo to hyperledger

7 of 30 new or added lines in 3 files covered. (23.33%)

183 existing lines in 47 files now uncovered.

7377 of 14908 relevant lines covered (49.48%)

0.49 hits per line

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

81.7
/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala
1
package org.hyperledger.identus.pollux.core.service
2

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

35
import java.net.URI
36
import java.rmi.UnexpectedException
37
import java.time.{Instant, ZoneId}
38
import java.util.UUID
39
import scala.language.implicitConversions
40

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

75
  //  private val VC_JSON_SCHEMA_URI = "https://w3c-ccg.github.io/vc-json-schemas/schema/2.0/schema.json"
76
  private val VC_JSON_SCHEMA_TYPE = "CredentialSchema2022"
77
}
78

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

93
  import CredentialServiceImpl.*
94
  import IssueCredentialRecord.*
95

96
  override def getIssueCredentialRecords(
1✔
97
      ignoreWithZeroRetries: Boolean,
98
      offset: Option[Int],
99
      limit: Option[Int]
100
  ): ZIO[WalletAccessContext, CredentialServiceError, (Seq[IssueCredentialRecord], Int)] = {
101
    for {
1✔
102
      records <- credentialRepository
1✔
103
        .getIssueCredentialRecords(ignoreWithZeroRetries = ignoreWithZeroRetries, offset = offset, limit = limit)
1✔
104
        .mapError(RepositoryError.apply)
105
    } yield records
106
  }
107

108
  override def getIssueCredentialRecordByThreadId(
×
109
      thid: DidCommID,
110
      ignoreWithZeroRetries: Boolean
111
  ): ZIO[WalletAccessContext, CredentialServiceError, Option[IssueCredentialRecord]] =
112
    for {
×
113
      record <- credentialRepository
×
114
        .getIssueCredentialRecordByThreadId(thid, ignoreWithZeroRetries)
×
115
        .mapError(RepositoryError.apply)
116
    } yield record
117

118
  override def getIssueCredentialRecord(
1✔
119
      recordId: DidCommID
120
  ): ZIO[WalletAccessContext, CredentialServiceError, Option[IssueCredentialRecord]] = {
121
    for {
1✔
122
      record <- credentialRepository
1✔
123
        .getIssueCredentialRecord(recordId)
1✔
124
        .mapError(RepositoryError.apply)
125
    } yield record
126
  }
127

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

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

257
  override def getIssueCredentialRecordsByStates(
1✔
258
      ignoreWithZeroRetries: Boolean,
259
      limit: Int,
260
      states: IssueCredentialRecord.ProtocolState*
261
  ): ZIO[WalletAccessContext, CredentialServiceError, Seq[IssueCredentialRecord]] = {
262
    for {
1✔
263
      records <- credentialRepository
1✔
264
        .getIssueCredentialRecordsByStates(ignoreWithZeroRetries, limit, states: _*)
1✔
265
        .mapError(RepositoryError.apply)
266
    } yield records
267
  }
268

269
  override def getIssueCredentialRecordsByStatesForAllWallets(
×
270
      ignoreWithZeroRetries: Boolean,
271
      limit: Int,
272
      states: IssueCredentialRecord.ProtocolState*
273
  ): IO[CredentialServiceError, Seq[IssueCredentialRecord]] = {
274
    for {
×
275
      records <- credentialRepository
×
276
        .getIssueCredentialRecordsByStatesForAllWallets(ignoreWithZeroRetries, limit, states: _*)
×
277
        .mapError(RepositoryError.apply)
278
    } yield records
279
  }
280

281
  override def receiveCredentialOffer(
1✔
282
      offer: OfferCredential
283
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
284
    for {
1✔
285
      attachment <- ZIO
1✔
286
        .fromOption(offer.attachments.headOption)
1✔
287
        .mapError(_ => CredentialServiceError.UnexpectedError("Missing attachment in credential offer"))
288

289
      format <- ZIO.fromOption(attachment.format).mapError(_ => MissingCredentialFormat)
1✔
290

291
      credentialFormat <- format match
1✔
292
        case value if value == IssueCredentialOfferFormat.JWT.name      => ZIO.succeed(CredentialFormat.JWT)
1✔
293
        case value if value == IssueCredentialOfferFormat.Anoncred.name => ZIO.succeed(CredentialFormat.AnonCreds)
1✔
294
        case value                                                      => ZIO.fail(UnsupportedCredentialFormat(value))
×
295

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

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

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

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

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

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

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

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

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

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

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

636
  override def receiveCredentialIssue(
1✔
637
      issueCredential: IssueCredential
638
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = for {
1✔
639
    // TODO Move this type of generic/reusable code to a helper trait
640
    record <- getRecordFromThreadIdWithState(
1✔
641
      issueCredential.thid.map(DidCommID(_)),
1✔
642
      ignoreWithZeroRetries = true,
643
      ProtocolState.RequestPending,
644
      ProtocolState.RequestSent
645
    )
646
    attachment <- ZIO
1✔
647
      .fromOption(issueCredential.attachments.headOption)
1✔
648
      .mapError(_ => CredentialServiceError.UnexpectedError("Missing attachment in credential issued credential"))
649

650
    _ <- {
1✔
651
      val result = attachment match {
652
        case AttachmentDescriptor(
653
              id,
654
              media_type,
655
              Base64(v),
656
              Some(IssueCredentialIssuedFormat.Anoncred.name),
657
              _,
658
              _,
659
              _,
660
              _
661
            ) =>
1✔
662
          for {
1✔
663
            processedCredential <- processAnonCredsCredential(record, java.util.Base64.getUrlDecoder.decode(v))
1✔
664
            attachment = AttachmentDescriptor.buildBase64Attachment(
1✔
665
              id = id,
666
              mediaType = media_type,
667
              format = Some(IssueCredentialIssuedFormat.Anoncred.name),
668
              payload = processedCredential.data.getBytes
1✔
669
            )
670
            processedIssuedCredential = issueCredential.copy(attachments = Seq(attachment))
1✔
671
            result <-
1✔
672
              updateWithCredential(
1✔
673
                processedIssuedCredential,
674
                record,
675
                attachment,
676
                Some(processedCredential.getSchemaId),
1✔
677
                Some(processedCredential.getCredDefId)
1✔
678
              )
679
          } yield result
680
        case attachment =>
1✔
681
          updateWithCredential(issueCredential, record, attachment, None, None)
1✔
682
      }
683
      result
684
    }
685
    record <- credentialRepository
1✔
686
      .getIssueCredentialRecord(record.id)
1✔
687
      .mapError(RepositoryError.apply)
688
      .someOrFail(RecordIdNotFound(record.id))
689
  } yield record
690

691
  private def updateWithCredential(
1✔
692
      issueCredential: IssueCredential,
693
      record: IssueCredentialRecord,
694
      attachment: AttachmentDescriptor,
695
      schemaId: Option[String],
696
      credDefId: Option[String]
697
  ) = {
698
    credentialRepository
1✔
699
      .updateWithIssuedRawCredential(
1✔
700
        record.id,
701
        issueCredential,
702
        attachment.data.asJson.noSpaces,
1✔
703
        schemaId,
704
        credDefId,
705
        ProtocolState.CredentialReceived
706
      )
707
      .flatMap {
708
        case 1 => ZIO.succeed(())
1✔
709
        case n => ZIO.fail(UnexpectedException(s"Invalid row count result: $n"))
×
710
      }
711
      .mapError(RepositoryError.apply)
712
  }
713

714
  private[this] def processAnonCredsCredential(
1✔
715
      record: IssueCredentialRecord,
716
      credentialBytes: Array[Byte]
717
  ): ZIO[WalletAccessContext, CredentialServiceError, anoncreds.AnoncredCredential] = {
718
    for {
1✔
719
      credential <- ZIO.succeed(anoncreds.AnoncredCredential(new String(credentialBytes)))
1✔
720
      credDefContent <- uriDereferencer
1✔
721
        .dereference(new URI(credential.getCredDefId))
1✔
722
        .mapError(err => UnexpectedError(err.toString))
723
      credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent)
724
      metadata <- ZIO
1✔
725
        .fromOption(record.anonCredsRequestMetadata)
726
        .mapError(_ => CredentialServiceError.UnexpectedError(s"No request metadata Id found un record: ${record.id}"))
×
727
      linkSecret <- linkSecretService
1✔
728
        .fetchOrCreate()
1✔
729
        .mapError(e => CredentialServiceError.LinkSecretError.apply(e.cause))
730
      credential <- ZIO
1✔
731
        .attempt(
732
          AnoncredLib.processCredential(
1✔
733
            anoncreds.AnoncredCredential(new String(credentialBytes)),
1✔
734
            metadata,
735
            linkSecret,
736
            credentialDefinition
737
          )
738
        )
739
        .mapError(error => UnexpectedError(s"AnonCreds credential processing error: ${error.getMessage}"))
×
740
    } yield credential
741
  }
742

743
  override def markOfferSent(
1✔
744
      recordId: DidCommID
745
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] =
746
    updateCredentialRecordProtocolState(
1✔
747
      recordId,
748
      IssueCredentialRecord.ProtocolState.OfferPending,
749
      IssueCredentialRecord.ProtocolState.OfferSent
750
    )
751

752
  override def markRequestSent(
1✔
753
      recordId: DidCommID
754
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] =
755
    updateCredentialRecordProtocolState(
1✔
756
      recordId,
757
      IssueCredentialRecord.ProtocolState.RequestGenerated,
758
      IssueCredentialRecord.ProtocolState.RequestSent
759
    ) @@ CustomMetricsAspect.endRecordingTime(
1✔
760
      s"${recordId}_issuance_flow_holder_req_generated_to_sent",
1✔
761
      "issuance_flow_holder_req_generated_to_sent_ms_gauge"
762
    )
763

764
  private[this] def markCredentialGenerated(
1✔
765
      record: IssueCredentialRecord,
766
      issueCredential: IssueCredential
767
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
768
    for {
1✔
769
      count <- credentialRepository
1✔
770
        .updateWithIssueCredential(
1✔
771
          record.id,
772
          issueCredential,
773
          IssueCredentialRecord.ProtocolState.CredentialGenerated
774
        )
775
        .mapError(RepositoryError.apply) @@ CustomMetricsAspect.endRecordingTime(
1✔
776
        s"${record.id}_issuance_flow_issuer_credential_pending_to_generated",
1✔
777
        "issuance_flow_issuer_credential_pending_to_generated_ms_gauge"
778
      ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_issuer_credential_generated_to_sent")
1✔
779
      _ <- count match
1✔
780
        case 1 => ZIO.succeed(())
1✔
781
        case n => ZIO.fail(RecordIdNotFound(record.id))
×
782
      record <- credentialRepository
1✔
783
        .getIssueCredentialRecord(record.id)
1✔
784
        .mapError(RepositoryError.apply)
785
        .flatMap {
786
          case None        => ZIO.fail(RecordIdNotFound(record.id))
×
787
          case Some(value) => ZIO.succeed(value)
1✔
788
        }
789

790
    } yield record
791
  }
792

793
  override def markCredentialSent(
1✔
794
      recordId: DidCommID
795
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] =
796
    updateCredentialRecordProtocolState(
1✔
797
      recordId,
798
      IssueCredentialRecord.ProtocolState.CredentialGenerated,
799
      IssueCredentialRecord.ProtocolState.CredentialSent
800
    ) @@ CustomMetricsAspect.endRecordingTime(
1✔
801
      s"${recordId}_issuance_flow_issuer_credential_generated_to_sent",
1✔
802
      "issuance_flow_issuer_credential_generated_to_sent_ms_gauge"
803
    )
804

805
  override def reportProcessingFailure(
×
806
      recordId: DidCommID,
807
      failReason: Option[String]
808
  ): ZIO[WalletAccessContext, CredentialServiceError, Unit] =
809
    credentialRepository
×
810
      .updateAfterFail(recordId, failReason)
×
811
      .mapError(RepositoryError.apply)
812
      .flatMap {
813
        case 1 => ZIO.unit
×
814
        case n => ZIO.fail(UnexpectedError(s"Invalid number of records updated: $n"))
×
815
      }
816

817
  private[this] def getRecordWithState(
1✔
818
      recordId: DidCommID,
819
      state: ProtocolState
820
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
821
    for {
1✔
822
      maybeRecord <- credentialRepository
1✔
823
        .getIssueCredentialRecord(recordId)
1✔
824
        .mapError(RepositoryError.apply)
825
      record <- ZIO
1✔
826
        .fromOption(maybeRecord)
827
        .mapError(_ => RecordIdNotFound(recordId))
828
      _ <- record.protocolState match {
1✔
829
        case s if s == state => ZIO.unit
1✔
830
        case state           => ZIO.fail(InvalidFlowStateError(s"Invalid protocol state for operation: $state"))
1✔
831
      }
832
    } yield record
1✔
833
  }
834

835
  private[this] def getRecordFromThreadIdWithState(
1✔
836
      thid: Option[DidCommID],
837
      ignoreWithZeroRetries: Boolean,
838
      states: ProtocolState*
839
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
840
    for {
1✔
841
      thid <- ZIO
1✔
842
        .fromOption(thid)
843
        .mapError(_ => UnexpectedError("No `thid` found in credential request"))
844
      maybeRecord <- credentialRepository
1✔
845
        .getIssueCredentialRecordByThreadId(thid, ignoreWithZeroRetries)
1✔
846
        .mapError(RepositoryError.apply)
847
      record <- ZIO
1✔
848
        .fromOption(maybeRecord)
849
        .mapError(_ => ThreadIdNotFound(thid))
850
      _ <- record.protocolState match {
1✔
851
        case s if states.contains(s) => ZIO.unit
1✔
852
        case state                   => ZIO.fail(InvalidFlowStateError(s"Invalid protocol state for operation: $state"))
1✔
853
      }
854
    } yield record
1✔
855
  }
856

857
  private[this] def createJWTDidCommOfferCredential(
1✔
858
      pairwiseIssuerDID: DidId,
859
      pairwiseHolderDID: DidId,
860
      maybeSchemaId: Option[String],
861
      claims: Seq[Attribute],
862
      thid: DidCommID,
863
      challenge: String,
864
      domain: String
865
  ) = {
866
    for {
1✔
867
      credentialPreview <- ZIO.succeed(CredentialPreview(schema_id = maybeSchemaId, attributes = claims))
1✔
868
      body = OfferCredential.Body(
1✔
869
        goal_code = Some("Offer Credential"),
870
        credential_preview = credentialPreview,
871
      )
872
      attachments <- ZIO.succeed(
1✔
873
        Seq(
1✔
874
          AttachmentDescriptor.buildJsonAttachment(
1✔
875
            mediaType = Some("application/json"),
876
            format = Some(IssueCredentialOfferFormat.JWT.name),
877
            payload = PresentationAttachment(
878
              Some(Options(challenge, domain)),
879
              PresentationDefinition(format = Some(ClaimFormat(jwt = Some(Jwt(alg = Seq("ES256K"), proof_type = Nil)))))
1✔
880
            )
881
          )
882
        )
883
      )
884
    } yield OfferCredential(
1✔
885
      body = body,
886
      attachments = attachments,
887
      from = pairwiseIssuerDID,
888
      to = pairwiseHolderDID,
889
      thid = Some(thid.value)
1✔
890
    )
891
  }
892

893
  private[this] def createAnonCredsDidCommOfferCredential(
1✔
894
      pairwiseIssuerDID: DidId,
895
      pairwiseHolderDID: DidId,
896
      schemaUri: String,
897
      credentialDefinitionGUID: UUID,
898
      credentialDefinitionId: String,
899
      claims: Seq[Attribute],
900
      thid: DidCommID
901
  ) = {
902
    for {
1✔
903
      credentialPreview <- ZIO.succeed(CredentialPreview(schema_id = Some(schemaUri), attributes = claims))
1✔
904
      body = OfferCredential.Body(
1✔
905
        goal_code = Some("Offer Credential"),
906
        credential_preview = credentialPreview,
907
      )
908
      attachments <- createAnonCredsCredentialOffer(credentialDefinitionGUID, credentialDefinitionId).map { offer =>
1✔
909
        Seq(
1✔
910
          AttachmentDescriptor.buildBase64Attachment(
1✔
911
            mediaType = Some("application/json"),
912
            format = Some(IssueCredentialOfferFormat.Anoncred.name),
913
            payload = offer.data.getBytes()
1✔
914
          )
915
        )
916
      }
917
    } yield OfferCredential(
1✔
918
      body = body,
919
      attachments = attachments,
920
      from = pairwiseIssuerDID,
921
      to = pairwiseHolderDID,
922
      thid = Some(thid.value)
1✔
923
    )
924
  }
925

926
  private[this] def createAnonCredsCredentialOffer(credentialDefinitionGUID: UUID, credentialDefinitionId: String) =
1✔
927
    for {
1✔
928
      credentialDefinition <- credentialDefinitionService
1✔
929
        .getByGUID(credentialDefinitionGUID)
1✔
930
        .mapError(e => CredentialServiceError.UnexpectedError(e.toString))
931
      cd = anoncreds.AnoncredCredentialDefinition(credentialDefinition.definition.toString)
1✔
932
      kcp = anoncreds.AnoncredCredentialKeyCorrectnessProof(credentialDefinition.keyCorrectnessProof.toString)
1✔
933
      maybeCredentialDefinitionSecret <- genericSecretStorage
1✔
934
        .get[UUID, CredentialDefinitionSecret](credentialDefinition.guid)
935
        .orDie
936
      credentialDefinitionSecret <- ZIO
1✔
937
        .fromOption(maybeCredentialDefinitionSecret)
938
        .mapError(_ => CredentialServiceError.CredentialDefinitionPrivatePartNotFound(credentialDefinition.guid))
939
      cdp = anoncreds.AnoncredCredentialDefinitionPrivate(credentialDefinitionSecret.json.toString)
1✔
940
      createCredentialDefinition = AnoncredCreateCredentialDefinition(cd, cdp, kcp)
941
      offer = AnoncredLib.createOffer(createCredentialDefinition, credentialDefinitionId)
1✔
942
    } yield offer
1✔
943

944
  private[this] def createDidCommRequestCredential(
1✔
945
      format: IssueCredentialOfferFormat,
946
      offer: OfferCredential,
947
      signedPresentation: JWT
948
  ): RequestCredential = {
949
    RequestCredential(
1✔
950
      body = RequestCredential.Body(
951
        goal_code = offer.body.goal_code,
952
        comment = offer.body.comment,
953
      ),
954
      attachments = Seq(
1✔
955
        AttachmentDescriptor
956
          .buildBase64Attachment(
1✔
957
            mediaType = Some("application/json"),
958
            format = Some(format.name),
959
            // FIXME copy payload will probably not work for anoncreds!
960
            payload = signedPresentation.value.getBytes(),
1✔
961
          )
962
      ),
963
      thid = offer.thid.orElse(Some(offer.id)),
1✔
964
      from = offer.to,
965
      to = offer.from
966
    )
967
  }
968

969
  private[this] def createDidCommIssueCredential(request: RequestCredential): IssueCredential = {
1✔
970
    IssueCredential(
1✔
971
      body = IssueCredential.Body(
972
        goal_code = request.body.goal_code,
973
        comment = request.body.comment,
974
        replacement_id = None,
975
        more_available = None,
976
      ),
977
      attachments = Seq(), // FIXME !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1✔
978
      thid = request.thid.orElse(Some(request.id)),
1✔
979
      from = request.to,
980
      to = request.from
981
    )
982
  }
983

984
  /** this is an auxiliary function.
985
    *
986
    * @note
987
    *   Between updating and getting the CredentialRecord back the CredentialRecord can be updated by other operations
988
    *   in the middle.
989
    *
990
    * TODO: this should be improved to behave exactly like atomic operation.
991
    */
992
  private[this] def updateCredentialRecordProtocolState(
1✔
993
      id: DidCommID,
994
      from: IssueCredentialRecord.ProtocolState,
995
      to: IssueCredentialRecord.ProtocolState
996
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
997
    for {
1✔
998
      record <- credentialRepository
1✔
999
        .updateCredentialRecordProtocolState(id, from, to)
1✔
1000
        .mapError(RepositoryError.apply)
1001
        .flatMap {
1002
          case 0 =>
×
1003
            credentialRepository
×
1004
              .getIssueCredentialRecord(id)
×
1005
              .mapError(RepositoryError.apply)
1006
              .flatMap {
1007
                case None => ZIO.fail(RecordIdNotFound(id))
×
1008
                case Some(record) if record.protocolState == to => // Not update by is alredy on the desirable state
×
1009
                  ZIO.succeed(record)
×
1010
                case Some(record) =>
×
1011
                  ZIO.fail(
×
1012
                    OperationNotExecuted(
1013
                      id,
1014
                      s"CredentialRecord was not updated because have the ProtocolState ${record.protocolState}"
×
1015
                    )
1016
                  )
1017
              }
1018
          case 1 =>
1✔
1019
            credentialRepository
1✔
1020
              .getIssueCredentialRecord(id)
1✔
1021
              .mapError(RepositoryError.apply)
1022
              .flatMap {
1023
                case None => ZIO.fail(RecordIdNotFound(id))
×
1024
                case Some(record) =>
1✔
1025
                  ZIO
1✔
1026
                    .logError(
1027
                      s"The CredentialRecord ($id) is expected to be on the ProtocolState '$to' after updating it"
×
1028
                    )
1029
                    .when(record.protocolState != to)
1030
                    .as(record)
1031
              }
1032
          case n => ZIO.fail(UnexpectedError(s"Invalid row count result: $n"))
×
1033
        }
1034
    } yield record
1035
  }
1036

1037
  override def generateJWTCredential(
1✔
1038
      recordId: DidCommID,
1039
      statusListRegistryUrl: String,
1040
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1041
    for {
1✔
1042
      record <- getRecordWithState(recordId, ProtocolState.CredentialPending)
1✔
1043
      issuingDID <- ZIO
1✔
1044
        .fromOption(record.issuingDID)
1045
        .mapError(_ => CredentialServiceError.UnexpectedError(s"Issuing Id not found in record: ${recordId.value}"))
×
1046
      issue <- ZIO
1✔
1047
        .fromOption(record.issueCredentialData)
1048
        .mapError(_ =>
1049
          CredentialServiceError.UnexpectedError(s"Issue credential data not found in record: ${recordId.value}")
×
1050
        )
1051
      longFormPrismDID <- getLongForm(issuingDID, true).mapError(err => UnexpectedError(err.getMessage))
1✔
1052
      jwtIssuer <- createJwtIssuer(longFormPrismDID, VerificationRelationship.AssertionMethod)
1✔
1053
      offerCredentialData <- ZIO
1✔
1054
        .fromOption(record.offerCredentialData)
1055
        .mapError(_ =>
1056
          CredentialServiceError.CreateCredentialPayloadFromRecordError(
1057
            new Throwable("Could not extract claims from \"requestCredential\" DIDComm message")
×
1058
          )
1059
        )
1060
      preview = offerCredentialData.body.credential_preview
1061
      claims <- CredentialService.convertAttributesToJsonClaims(preview.body.attributes)
1✔
1062
      maybeOfferOptions <- getOptionsFromOfferCredentialData(record)
1✔
1063
      requestJwt <- getJwtFromRequestCredentialData(record)
1✔
1064

1065
      // domain/challenge validation + JWT verification
1066
      jwtPresentation <- validateRequestCredentialDataProof(maybeOfferOptions, requestJwt).tapBoth(
1✔
1067
        error =>
1068
          ZIO.logErrorCause("JWT Presentation Validation Failed!!", Cause.fail(error)) *> credentialRepository
×
1069
            .updateCredentialRecordProtocolState(
×
1070
              record.id,
1071
              ProtocolState.CredentialPending,
1072
              ProtocolState.ProblemReportPending
1073
            )
1074
            .mapError(t => RepositoryError(t)),
1075
        payload => ZIO.logInfo("JWT Presentation Validation Successful!")
1✔
1076
      )
1077
      issuanceDate = Instant.now()
1✔
1078
      credentialStatus <- allocateNewCredentialInStatusListForWallet(record, statusListRegistryUrl, jwtIssuer)
1✔
1079
      // TODO: get schema when schema registry is available if schema ID is provided
1080
      w3Credential = W3cCredentialPayload(
1✔
1081
        `@context` = Set(
1✔
1082
          "https://www.w3.org/2018/credentials/v1"
1083
        ), // TODO: his information should come from Schema registry by record.schemaId
1084
        maybeId = None,
1085
        `type` =
1086
          Set("VerifiableCredential"), // TODO: This information should come from Schema registry by record.schemaId
1✔
1087
        issuer = jwtIssuer.did,
1088
        issuanceDate = issuanceDate,
1089
        maybeExpirationDate = record.validityPeriod.map(sec => issuanceDate.plusSeconds(sec.toLong)),
×
1090
        maybeCredentialSchema =
1091
          record.schemaUri.map(id => org.hyperledger.identus.pollux.vc.jwt.CredentialSchema(id, VC_JSON_SCHEMA_TYPE)),
1✔
1092
        maybeCredentialStatus = Some(credentialStatus),
1093
        credentialSubject = claims.add("id", jwtPresentation.iss.asJson).asJson,
1✔
1094
        maybeRefreshService = None,
1095
        maybeEvidence = None,
1096
        maybeTermsOfUse = None
1097
      )
1098
      signedJwtCredential = W3CCredential.toEncodedJwt(w3Credential, jwtIssuer)
1✔
1099
      issueCredential = IssueCredential.build(
1✔
1100
        fromDID = issue.from,
1101
        toDID = issue.to,
1102
        thid = issue.thid,
1103
        credentials = Seq(IssueCredentialIssuedFormat.JWT -> signedJwtCredential.value.getBytes)
1✔
1104
      )
1105
      record <- markCredentialGenerated(record, issueCredential)
1✔
1106
    } yield record
1107
  }
1108

1109
  private[this] def allocateNewCredentialInStatusListForWallet(
1✔
1110
      record: IssueCredentialRecord,
1111
      statusListRegistryUrl: String,
1112
      jwtIssuer: JwtIssuer
1113
  ): ZIO[WalletAccessContext, CredentialServiceError, CredentialStatus] = {
1114
    val effect = for {
1✔
1115
      lastStatusList <- credentialStatusListRepository.getLatestOfTheWallet.mapError(RepositoryError.apply)
1✔
1116
      currentStatusList <- lastStatusList
1✔
1117
        .fold(credentialStatusListRepository.createNewForTheWallet(jwtIssuer, statusListRegistryUrl))(
1✔
1118
          ZIO.succeed(_)
×
1119
        )
1120
        .mapError(RepositoryError.apply)
1121
      size = currentStatusList.size
1122
      lastUsedIndex = currentStatusList.lastUsedIndex
1123
      statusListToBeUsed <-
1✔
1124
        if lastUsedIndex < size then ZIO.succeed(currentStatusList)
1✔
1125
        else
1126
          credentialStatusListRepository
×
1127
            .createNewForTheWallet(jwtIssuer, statusListRegistryUrl)
×
1128
            .mapError(RepositoryError.apply)
1129
      _ <- credentialStatusListRepository
1✔
1130
        .allocateSpaceForCredential(
1✔
1131
          issueCredentialRecordId = record.id,
1132
          credentialStatusListId = statusListToBeUsed.id,
1133
          statusListIndex = statusListToBeUsed.lastUsedIndex + 1
1134
        )
1135
        .mapError(RepositoryError.apply)
1136
    } yield CredentialStatus(
1✔
1137
      id = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}#${statusListToBeUsed.lastUsedIndex + 1}",
1✔
1138
      `type` = "StatusList2021Entry",
1139
      statusPurpose = StatusPurpose.Revocation,
1140
      statusListIndex = lastUsedIndex + 1,
1141
      statusListCredential = s"$statusListRegistryUrl/credential-status/${statusListToBeUsed.id}"
1✔
1142
    )
1143
    issueCredentialSem.withPermit(effect)
1✔
1144
  }
1145

1146
  override def generateAnonCredsCredential(
1✔
1147
      recordId: DidCommID
1148
  ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = {
1149
    for {
1✔
1150
      record <- getRecordWithState(recordId, ProtocolState.CredentialPending)
1✔
1151
      requestCredential <- ZIO
1✔
1152
        .fromOption(record.requestCredentialData)
1153
        .mapError(_ => InvalidFlowStateError(s"No request found for this record: ${record.id}"))
×
1154
      body = IssueCredential.Body(goal_code = Some("Issue Credential"))
1✔
1155
      attachments <- createAnonCredsCredential(record).map { credential =>
1✔
1156
        Seq(
1✔
1157
          AttachmentDescriptor.buildBase64Attachment(
1✔
1158
            mediaType = Some("application/json"),
1159
            format = Some(IssueCredentialIssuedFormat.Anoncred.name),
1160
            payload = credential.data.getBytes()
1✔
1161
          )
1162
        )
1163
      }
1164
      issueCredential = IssueCredential(
1✔
1165
        body = body,
1166
        attachments = attachments,
1167
        from = requestCredential.to,
1168
        to = requestCredential.from,
1169
        thid = requestCredential.thid
1170
      )
1171
      record <- markCredentialGenerated(record, issueCredential)
1✔
1172
    } yield record
1173
  }
1174

1175
  private[this] def createAnonCredsCredential(record: IssueCredentialRecord) = {
1✔
1176
    for {
1✔
1177
      credentialDefinitionId <- ZIO
1✔
1178
        .fromOption(record.credentialDefinitionId)
1179
        .mapError(_ => CredentialServiceError.UnexpectedError(s"No cred def Id found un record: ${record.id}"))
×
1180
      credentialDefinition <- credentialDefinitionService
1✔
1181
        .getByGUID(credentialDefinitionId)
1✔
1182
        .mapError(e => CredentialServiceError.UnexpectedError(e.toString))
1183
      cd = anoncreds.AnoncredCredentialDefinition(credentialDefinition.definition.toString)
1✔
1184
      offerCredential <- ZIO
1✔
1185
        .fromOption(record.offerCredentialData)
1186
        .mapError(_ => InvalidFlowStateError(s"No offer found for this record: ${record.id}"))
×
1187
      offerCredentialAttachmentData <- ZIO
1✔
1188
        .fromOption(
1189
          offerCredential.attachments
1190
            .find(_.format.contains(IssueCredentialOfferFormat.Anoncred.name))
1✔
1191
            .map(_.data)
1✔
1192
            .flatMap {
1✔
1193
              case Base64(value) => Some(new String(java.util.Base64.getUrlDecoder.decode(value)))
1✔
1194
              case _             => None
×
1195
            }
1196
        )
1197
        .mapError(_ => InvalidFlowStateError(s"No AnonCreds offer attachment found"))
×
1198
      credentialOffer = anoncreds.AnoncredCredentialOffer(offerCredentialAttachmentData)
1199
      requestCredential <- ZIO
1✔
1200
        .fromOption(record.requestCredentialData)
1201
        .mapError(_ => InvalidFlowStateError(s"No request found for this record: ${record.id}"))
×
1202
      requestCredentialAttachmentData <- ZIO
1✔
1203
        .fromOption(
1204
          requestCredential.attachments
1205
            .find(_.format.contains(IssueCredentialRequestFormat.Anoncred.name))
1✔
1206
            .map(_.data)
1✔
1207
            .flatMap {
1✔
1208
              case Base64(value) => Some(new String(java.util.Base64.getUrlDecoder.decode(value)))
1✔
1209
              case _             => None
×
1210
            }
1211
        )
1212
        .mapError(_ => InvalidFlowStateError(s"No AnonCreds request attachment found"))
×
1213
      credentialRequest = anoncreds.AnoncredCredentialRequest(requestCredentialAttachmentData)
1214
      attrValues = offerCredential.body.credential_preview.body.attributes.map { attr =>
1✔
1215
        (attr.name, attr.value)
1216
      }
1217
      maybeCredentialDefinitionSecret <- genericSecretStorage
1✔
1218
        .get[UUID, CredentialDefinitionSecret](credentialDefinition.guid)
1219
        .orDie
1220
      credentialDefinitionSecret <- ZIO
1✔
1221
        .fromOption(maybeCredentialDefinitionSecret)
1222
        .mapError(_ => CredentialServiceError.CredentialDefinitionPrivatePartNotFound(credentialDefinition.guid))
1223
      cdp = anoncreds.AnoncredCredentialDefinitionPrivate(credentialDefinitionSecret.json.toString)
1✔
1224
      credential =
1225
        AnoncredLib.createCredential(
1✔
1226
          cd,
1227
          cdp,
1228
          credentialOffer,
1229
          credentialRequest,
1230
          attrValues
1231
        )
1232
    } yield credential
1✔
1233
  }
1234

1235
  private[this] def getOptionsFromOfferCredentialData(record: IssueCredentialRecord) = {
1✔
1236
    for {
1✔
1237
      offer <- ZIO
1✔
1238
        .fromOption(record.offerCredentialData)
1239
        .mapError(_ => CredentialServiceError.UnexpectedError(s"Offer data not found in record: ${record.id}"))
×
1240
      attachmentDescriptor <- ZIO
1✔
1241
        .fromOption(offer.attachments.headOption)
1✔
1242
        .mapError(_ => UnexpectedError(s"Attachments not found in record: ${record.id}"))
×
1243
      json <- attachmentDescriptor.data match
1✔
1244
        case JsonData(json) => ZIO.succeed(json.asJson)
1✔
1245
        case _              => ZIO.fail(UnexpectedError(s"Attachment doesn't contain JsonData: ${record.id}"))
×
1246
      maybeOptions <- ZIO
1✔
1247
        .fromEither(json.as[PresentationAttachment].map(_.options))
1✔
1248
        .mapError(df => UnexpectedError(df.getMessage))
×
1249
    } yield maybeOptions
1250
  }
1251

1252
  private[this] def getJwtFromRequestCredentialData(record: IssueCredentialRecord) = {
1✔
1253
    for {
1✔
1254
      request <- ZIO
1✔
1255
        .fromOption(record.requestCredentialData)
1256
        .mapError(_ => CredentialServiceError.UnexpectedError(s"Request data not found in record: ${record.id}"))
×
1257
      attachmentDescriptor <- ZIO
1✔
1258
        .fromOption(request.attachments.headOption)
1✔
1259
        .mapError(_ => UnexpectedError(s"Attachments not found in record: ${record.id}"))
×
1260
      jwt <- attachmentDescriptor.data match
1✔
1261
        case Base64(b64) =>
1✔
1262
          ZIO.succeed {
1✔
1263
            val base64Decoded = new String(java.util.Base64.getDecoder().decode(b64))
1✔
1264
            JWT(base64Decoded)
1✔
1265
          }
1266
        case _ => ZIO.fail(UnexpectedError(s"Attachment doesn't contain Base64Data: ${record.id}"))
×
1267
    } yield jwt
1268
  }
1269

1270
  private[this] def validateRequestCredentialDataProof(maybeOptions: Option[Options], jwt: JWT) = {
1✔
1271
    for {
1✔
1272
      _ <- maybeOptions match
1✔
1273
        case None => ZIO.unit
×
1274
        case Some(options) =>
1✔
1275
          JwtPresentation.validatePresentation(jwt, options.domain, options.challenge) match
1✔
1276
            case ZValidation.Success(log, value) => ZIO.unit
1✔
1277
            case ZValidation.Failure(log, error) =>
×
1278
              ZIO.fail(CredentialRequestValidationError("JWT presentation domain/validation validation failed"))
×
1279

1280
      clock = java.time.Clock.system(ZoneId.systemDefault)
1✔
1281

1282
      genericUriResolver = GenericUriResolver(
1✔
1283
        Map(
1✔
1284
          "data" -> DataUrlResolver(),
1✔
1285
        )
1286
      )
1287
      verificationResult <- JwtPresentation
1✔
1288
        .verify(
1289
          jwt,
1290
          JwtPresentation.PresentationVerificationOptions(
1✔
1291
            maybeProofPurpose = Some(VerificationRelationship.Authentication),
1292
            verifySignature = true,
1293
            verifyDates = false,
1294
            leeway = Duration.Zero
1295
          )
1296
        )(didResolver, genericUriResolver)(clock)
1✔
1297
        .mapError(errors => CredentialRequestValidationError(s"JWT presentation verification failed: $errors"))
×
1298

1299
      result <- verificationResult match
1✔
1300
        case ZValidation.Success(log, value) => ZIO.unit
1✔
1301
        case ZValidation.Failure(log, error) =>
×
1302
          ZIO.fail(CredentialRequestValidationError(s"JWT presentation verification failed: $error"))
×
1303

1304
      jwtPresentation <- ZIO
1✔
1305
        .fromTry(JwtPresentation.decodeJwt(jwt))
1✔
1306
        .mapError(t => CredentialRequestValidationError(s"JWT presentation decoding failed: ${t.getMessage()}"))
×
1307
    } yield jwtPresentation
1308
  }
1309

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