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

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

02 May 2024 07:45AM UTC coverage: 47.494% (-1.8%) from 49.277%
8920398831

Pull #909

patlo-iog
Merge branch 'main' into oidc
Pull Request #909: feat(agent): define the OAS for CredentialIssuerEndpoints [WIP]

47 of 618 new or added lines in 31 files covered. (7.61%)

220 existing lines in 64 files now uncovered.

7336 of 15446 relevant lines covered (47.49%)

0.47 hits per line

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

82.1
/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
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
  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
  def createJwtIssuer(
1✔
452
      jwtIssuerDID: PrismDID,
453
      verificationRelationship: VerificationRelationship
454
  ): ZIO[WalletAccessContext, CredentialServiceError, JwtIssuer] = {
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✔
NEW
459
        .mapError(e => UnexpectedError(s"Error occurred while resolving Issuing DID during VC creation: ${e.toString}"))
×
460
        .someOrFail(UnexpectedError(s"Issuing DID resolution result is not found"))
×
461
        .map { case (_, didData) =>
1✔
462
          didData.publicKeys.find(_.purpose == verificationRelationship).map(_.id)
1✔
463
        }
464
        .someOrFail(
465
          UnexpectedError(s"Issuing DID doesn't have a key in ${verificationRelationship.name} to use: $jwtIssuerDID")
×
466
        )
467
      ecKeyPair <- managedDIDService
1✔
468
        .javaKeyPairWithDID(jwtIssuerDID.asCanonical, issuingKeyId)
1✔
469
        .mapError(e => UnexpectedError(s"Error occurred while getting issuer key-pair: ${e.toString}"))
×
470
        .someOrFail(
471
          UnexpectedError(s"Issuer key-pair does not exist in the wallet: ${jwtIssuerDID.toString}#$issuingKeyId")
×
472
        )
473
      (privateKey, publicKey) = ecKeyPair
1✔
474
      jwtIssuer = JwtIssuer(
475
        org.hyperledger.identus.pollux.vc.jwt.DID(jwtIssuerDID.toString),
1✔
476
        ES256KSigner(privateKey),
1✔
477
        publicKey
478
      )
479
    } yield jwtIssuer
1✔
480
  }
481

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

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

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

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

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

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

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

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

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

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

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

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

792
    } yield record
793
  }
794

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1282
      clock = java.time.Clock.system(ZoneId.systemDefault)
1✔
1283

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

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

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

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