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

hyperledger / identus-cloud-agent / 11942353508

20 Nov 2024 09:51PM UTC coverage: 48.913% (+0.06%) from 48.854%
11942353508

Pull #1461

mineme0110
fix: update the jwt to have shortform of prosm did

Signed-off-by: mineme0110 <shailesh.patil@iohk.io>
Pull Request #1461: fix: update the jwt to have shortform of prism did

1 of 1 new or added line in 1 file covered. (100.0%)

20 existing lines in 11 files now uncovered.

8078 of 16515 relevant lines covered (48.91%)

0.49 hits per line

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

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

3
import cats.implicits.*
4
import io.circe.*
5
import io.circe.parser.*
6
import io.circe.syntax.*
7
import org.hyperledger.identus.agent.walletapi.model.{ManagedDIDState, PublicationState}
8
import org.hyperledger.identus.agent.walletapi.service.ManagedDIDService
9
import org.hyperledger.identus.agent.walletapi.storage.GenericSecretStorage
10
import org.hyperledger.identus.castor.core.model.did.*
11
import org.hyperledger.identus.castor.core.service.DIDService
12
import org.hyperledger.identus.mercury.model.*
13
import org.hyperledger.identus.mercury.protocol.invitation.v2.Invitation
14
import org.hyperledger.identus.mercury.protocol.issuecredential.*
15
import org.hyperledger.identus.pollux.*
16
import org.hyperledger.identus.pollux.anoncreds.*
17
import org.hyperledger.identus.pollux.core.model.*
18
import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError
19
import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError.*
20
import org.hyperledger.identus.pollux.core.model.presentation.*
21
import org.hyperledger.identus.pollux.core.model.schema.{CredentialDefinition, CredentialSchema}
22
import org.hyperledger.identus.pollux.core.model.secret.CredentialDefinitionSecret
23
import org.hyperledger.identus.pollux.core.model.CredentialFormat.AnonCreds
24
import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.ProtocolState.OfferReceived
25
import org.hyperledger.identus.pollux.core.repository.{CredentialRepository, CredentialStatusListRepository}
26
import org.hyperledger.identus.pollux.prex.{ClaimFormat, Jwt, PresentationDefinition}
27
import org.hyperledger.identus.pollux.sdjwt.*
28
import org.hyperledger.identus.pollux.vc.jwt.{Issuer as JwtIssuer, *}
29
import org.hyperledger.identus.pollux.vc.jwt.PresentationPayload.Implicits.*
30
import org.hyperledger.identus.shared.crypto.{Ed25519KeyPair, Secp256k1KeyPair}
31
import org.hyperledger.identus.shared.http.UriResolver
32
import org.hyperledger.identus.shared.messaging.{Producer, WalletIdAndRecordId}
33
import org.hyperledger.identus.shared.models.*
34
import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect
35
import org.hyperledger.identus.shared.utils.Base64Utils
36
import zio.*
37
import zio.json.*
38
import zio.prelude.ZValidation
39

40
import java.time.{Instant, ZoneId}
41
import java.util.UUID
42
import scala.language.implicitConversions
43

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

79
  //  private val VC_JSON_SCHEMA_URI = "https://w3c-ccg.github.io/vc-json-schemas/schema/2.0/schema.json"
80
  private val VC_JSON_SCHEMA_TYPE = "CredentialSchema2022"
81
}
82

83
class CredentialServiceImpl(
84
    credentialRepository: CredentialRepository,
85
    credentialStatusListRepository: CredentialStatusListRepository,
86
    didResolver: DidResolver,
87
    uriResolver: UriResolver,
88
    genericSecretStorage: GenericSecretStorage,
89
    credentialDefinitionService: CredentialDefinitionService,
90
    linkSecretService: LinkSecretService,
91
    didService: DIDService,
92
    managedDIDService: ManagedDIDService,
93
    maxRetries: Int = 5, // TODO move to config
×
94
    messageProducer: Producer[UUID, WalletIdAndRecordId],
95
) extends CredentialService {
96

97
  import CredentialServiceImpl.*
98
  import IssueCredentialRecord.*
99

100
  private val TOPIC_NAME = "issue"
101

102
  override def getIssueCredentialRecords(
1✔
103
      ignoreWithZeroRetries: Boolean,
104
      offset: Option[Int],
105
      limit: Option[Int]
106
  ): URIO[WalletAccessContext, (Seq[IssueCredentialRecord], Int)] =
107
    credentialRepository.findAll(ignoreWithZeroRetries = ignoreWithZeroRetries, offset = offset, limit = limit)
1✔
108

109
  override def getIssueCredentialRecordByThreadId(
×
110
      thid: DidCommID,
111
      ignoreWithZeroRetries: Boolean
112
  ): URIO[WalletAccessContext, Option[IssueCredentialRecord]] =
113
    credentialRepository.findByThreadId(thid, ignoreWithZeroRetries)
×
114

115
  override def findById(
1✔
116
      recordId: DidCommID
117
  ): URIO[WalletAccessContext, Option[IssueCredentialRecord]] =
118
    credentialRepository.findById(recordId)
1✔
119

120
  override def getById(
×
121
      recordId: DidCommID
122
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] =
123
    for {
×
124
      maybeRecord <- credentialRepository.findById(recordId)
×
125
      record <- ZIO
×
126
        .fromOption(maybeRecord)
127
        .mapError(_ => RecordNotFound(recordId))
×
128
    } yield record
129

130
  private def createIssueCredentialRecord(
1✔
131
      pairwiseIssuerDID: DidId,
132
      kidIssuer: Option[KeyId],
133
      thid: DidCommID,
134
      schemaUris: Option[List[String]],
135
      validityPeriod: Option[Double],
136
      automaticIssuance: Option[Boolean],
137
      issuingDID: Option[CanonicalPrismDID],
138
      credentialFormat: CredentialFormat,
139
      offer: OfferCredential,
140
      credentialDefinitionGUID: Option[UUID] = None,
×
141
      credentialDefinitionId: Option[String] = None,
×
142
      connectionId: Option[UUID],
143
      goalCode: Option[String],
144
      goal: Option[String],
145
      expirationDuration: Option[Duration],
146
  ): URIO[WalletAccessContext, IssueCredentialRecord] = {
147
    for {
1✔
148
      invitation <- ZIO.succeed(
149
        connectionId.fold(
150
          Some(
151
            IssueCredentialInvitation.makeInvitation(
×
152
              pairwiseIssuerDID,
153
              goalCode,
154
              goal,
155
              thid.value,
×
156
              offer,
157
              expirationDuration
158
            )
159
          )
160
        )(_ => None)
1✔
161
      )
162
      record <- ZIO.succeed(
1✔
163
        IssueCredentialRecord(
164
          id = DidCommID(),
1✔
165
          createdAt = Instant.now,
1✔
166
          updatedAt = None,
167
          thid = thid,
168
          schemaUris = schemaUris,
169
          credentialDefinitionId = credentialDefinitionGUID,
170
          credentialDefinitionUri = credentialDefinitionId,
171
          credentialFormat = credentialFormat,
172
          invitation = invitation,
173
          role = IssueCredentialRecord.Role.Issuer,
174
          subjectId = None,
175
          keyId = kidIssuer,
176
          validityPeriod = validityPeriod,
177
          automaticIssuance = automaticIssuance,
178
          protocolState = invitation.fold(IssueCredentialRecord.ProtocolState.OfferPending)(_ =>
1✔
179
            IssueCredentialRecord.ProtocolState.InvitationGenerated
180
          ),
181
          offerCredentialData = Some(offer),
182
          requestCredentialData = None,
183
          anonCredsRequestMetadata = None,
184
          issueCredentialData = None,
185
          issuedCredentialRaw = None,
186
          issuingDID = issuingDID,
187
          metaRetries = maxRetries,
188
          metaNextRetry = Some(Instant.now()),
1✔
189
          metaLastFailure = None,
190
        )
191
      )
192
      count <- credentialRepository
1✔
193
        .create(record) @@ CustomMetricsAspect
1✔
194
        .startRecordingTime(s"${record.id}_issuer_offer_pending_to_sent_ms_gauge")
1✔
195
      walletAccessContext <- ZIO.service[WalletAccessContext]
1✔
196
      _ <- messageProducer
1✔
197
        .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid))
1✔
198
        .orDie
199
    } yield record
1✔
200
  }
201

202
  override def createJWTIssueCredentialRecord(
1✔
203
      pairwiseIssuerDID: DidId,
204
      pairwiseHolderDID: Option[DidId],
205
      kidIssuer: Option[KeyId],
206
      thid: DidCommID,
207
      maybeSchemaIds: Option[List[String]],
208
      claims: Json,
209
      validityPeriod: Option[Double],
210
      automaticIssuance: Option[Boolean],
211
      issuingDID: CanonicalPrismDID,
212
      goalCode: Option[String],
213
      goal: Option[String],
214
      expirationDuration: Option[Duration],
215
      connectionId: Option[UUID],
216
  ): URIO[WalletAccessContext, IssueCredentialRecord] = {
217
    for {
1✔
218
      _ <- validateClaimsAgainstSchemaIfAny(claims, maybeSchemaIds)
1✔
219
      attributes <- CredentialService.convertJsonClaimsToAttributes(claims)
1✔
220
      offer <- createDidCommOfferCredential(
1✔
221
        pairwiseIssuerDID = pairwiseIssuerDID,
222
        pairwiseHolderDID = pairwiseHolderDID,
223
        maybeSchemaIds = maybeSchemaIds,
224
        claims = attributes,
225
        thid = thid,
226
        UUID.randomUUID().toString,
1✔
227
        "domain", // TODO remove the hardcoded domain
228
        IssueCredentialOfferFormat.JWT
229
      )
230
      record <- createIssueCredentialRecord(
1✔
231
        pairwiseIssuerDID = pairwiseIssuerDID,
232
        kidIssuer = kidIssuer,
233
        thid = thid,
234
        schemaUris = maybeSchemaIds,
235
        validityPeriod = validityPeriod,
236
        automaticIssuance = automaticIssuance,
237
        issuingDID = Some(issuingDID),
238
        credentialFormat = CredentialFormat.JWT,
239
        offer = offer,
240
        credentialDefinitionGUID = None,
241
        credentialDefinitionId = None,
242
        connectionId = connectionId,
243
        goalCode = goalCode,
244
        goal = goal,
245
        expirationDuration = expirationDuration,
246
      )
247
    } yield record
248
  }
249

250
  override def createSDJWTIssueCredentialRecord(
×
251
      pairwiseIssuerDID: DidId,
252
      pairwiseHolderDID: Option[DidId],
253
      kidIssuer: Option[KeyId],
254
      thid: DidCommID,
255
      maybeSchemaIds: Option[List[String]],
256
      claims: io.circe.Json,
257
      validityPeriod: Option[Double] = None,
×
258
      automaticIssuance: Option[Boolean],
259
      issuingDID: CanonicalPrismDID,
260
      goalCode: Option[String],
261
      goal: Option[String],
262
      expirationDuration: Option[Duration],
263
      connectionId: Option[UUID],
264
  ): URIO[WalletAccessContext, IssueCredentialRecord] = {
265
    for {
×
266
      _ <- validateClaimsAgainstSchemaIfAny(claims, maybeSchemaIds)
×
267
      attributes <- CredentialService.convertJsonClaimsToAttributes(claims)
×
268
      offer <- createDidCommOfferCredential(
×
269
        pairwiseIssuerDID = pairwiseIssuerDID,
270
        pairwiseHolderDID = pairwiseHolderDID,
271
        maybeSchemaIds = maybeSchemaIds,
272
        claims = attributes,
273
        thid = thid,
274
        UUID.randomUUID().toString,
×
275
        "domain",
276
        IssueCredentialOfferFormat.SDJWT
277
      )
278
      record <- createIssueCredentialRecord(
×
279
        pairwiseIssuerDID = pairwiseIssuerDID,
280
        kidIssuer = kidIssuer,
281
        thid = thid,
282
        schemaUris = maybeSchemaIds,
283
        validityPeriod = validityPeriod,
284
        automaticIssuance = automaticIssuance,
285
        issuingDID = Some(issuingDID),
286
        credentialFormat = CredentialFormat.SDJWT,
287
        offer = offer,
288
        credentialDefinitionGUID = None,
289
        credentialDefinitionId = None,
290
        connectionId = connectionId,
291
        goalCode = goalCode,
292
        goal = goal,
293
        expirationDuration = expirationDuration,
294
      )
295
    } yield record
296
  }
297

298
  override def createAnonCredsIssueCredentialRecord(
1✔
299
      pairwiseIssuerDID: DidId,
300
      pairwiseHolderDID: Option[DidId],
301
      thid: DidCommID,
302
      credentialDefinitionGUID: UUID,
303
      credentialDefinitionId: String,
304
      claims: Json,
305
      validityPeriod: Option[Double],
306
      automaticIssuance: Option[Boolean],
307
      goalCode: Option[String],
308
      goal: Option[String],
309
      expirationDuration: Option[Duration],
310
      connectionId: Option[UUID],
311
  ): URIO[WalletAccessContext, IssueCredentialRecord] = {
312
    for {
1✔
313
      credentialDefinition <- getCredentialDefinition(credentialDefinitionGUID)
1✔
314
      _ <- CredentialSchema
1✔
315
        .validateAnonCredsClaims(
1✔
316
          credentialDefinition.schemaId,
317
          claims.noSpaces,
1✔
318
          uriResolver,
319
        )
320
        .orDieAsUnmanagedFailure
1✔
321
      attributes <- CredentialService.convertJsonClaimsToAttributes(claims)
1✔
322
      offer <- createAnonCredsDidCommOfferCredential(
1✔
323
        pairwiseIssuerDID = pairwiseIssuerDID,
324
        pairwiseHolderDID = pairwiseHolderDID,
325
        schemaUri = credentialDefinition.schemaId,
326
        credentialDefinitionGUID = credentialDefinitionGUID,
327
        credentialDefinitionId = credentialDefinitionId,
328
        claims = attributes,
329
        thid = thid,
330
      )
331
      record <- createIssueCredentialRecord(
1✔
332
        pairwiseIssuerDID = pairwiseIssuerDID,
333
        kidIssuer = None,
334
        thid = thid,
335
        schemaUris = Some(List(credentialDefinition.schemaId)),
1✔
336
        validityPeriod = validityPeriod,
337
        automaticIssuance = automaticIssuance,
338
        issuingDID = None,
339
        credentialFormat = CredentialFormat.AnonCreds,
340
        offer = offer,
341
        credentialDefinitionGUID = Some(credentialDefinitionGUID),
342
        credentialDefinitionId = Some(credentialDefinitionId),
343
        connectionId = connectionId,
344
        goalCode = goalCode,
345
        goal = goal,
346
        expirationDuration = expirationDuration,
347
      )
348
    } yield record
349
  }
350

351
  override def getIssueCredentialRecordsByStates(
1✔
352
      ignoreWithZeroRetries: Boolean,
353
      limit: Int,
354
      states: IssueCredentialRecord.ProtocolState*
355
  ): URIO[WalletAccessContext, Seq[IssueCredentialRecord]] =
356
    credentialRepository.findByStates(ignoreWithZeroRetries, limit, states*)
1✔
357

358
  override def getIssueCredentialRecordsByStatesForAllWallets(
×
359
      ignoreWithZeroRetries: Boolean,
360
      limit: Int,
361
      states: IssueCredentialRecord.ProtocolState*
362
  ): UIO[Seq[IssueCredentialRecord]] =
363
    credentialRepository.findByStatesForAllWallets(ignoreWithZeroRetries, limit, states*)
×
364

365
  override def receiveCredentialOffer(
1✔
366
      offer: OfferCredential
367
  ): ZIO[WalletAccessContext, InvalidCredentialOffer, IssueCredentialRecord] = {
368
    for {
1✔
369
      attachment <- ZIO
1✔
370
        .fromOption(offer.attachments.headOption)
1✔
371
        .mapError(_ => InvalidCredentialOffer("No attachment found"))
372

373
      format <- ZIO
1✔
374
        .fromOption(attachment.format)
375
        .mapError(_ => InvalidCredentialOffer("No attachment format found"))
376

377
      credentialFormat <- format match
1✔
378
        case value if value == IssueCredentialOfferFormat.JWT.name      => ZIO.succeed(CredentialFormat.JWT)
1✔
379
        case value if value == IssueCredentialOfferFormat.SDJWT.name    => ZIO.succeed(CredentialFormat.SDJWT)
×
380
        case value if value == IssueCredentialOfferFormat.Anoncred.name => ZIO.succeed(CredentialFormat.AnonCreds)
1✔
381
        case value => ZIO.fail(InvalidCredentialOffer(s"Unsupported credential format: $value"))
×
382

383
      _ <- validateCredentialOfferAttachment(credentialFormat, attachment)
1✔
384
      record <- ZIO.succeed(
1✔
385
        IssueCredentialRecord(
386
          id = DidCommID(),
1✔
387
          createdAt = Instant.now,
1✔
388
          updatedAt = None,
389
          thid = DidCommID(offer.thid.getOrElse(offer.id)),
1✔
390
          schemaUris = None,
391
          credentialDefinitionId = None,
392
          credentialDefinitionUri = None,
393
          credentialFormat = credentialFormat,
394
          invitation = None,
395
          role = Role.Holder,
396
          subjectId = None,
397
          keyId = None,
398
          validityPeriod = None,
399
          automaticIssuance = None,
400
          protocolState = IssueCredentialRecord.ProtocolState.OfferReceived,
401
          offerCredentialData = Some(offer),
402
          requestCredentialData = None,
403
          anonCredsRequestMetadata = None,
404
          issueCredentialData = None,
405
          issuedCredentialRaw = None,
406
          issuingDID = None,
407
          metaRetries = maxRetries,
408
          metaNextRetry = Some(Instant.now()),
1✔
409
          metaLastFailure = None,
410
        )
411
      )
412
      count <- credentialRepository.create(record)
1✔
413
    } yield record
414
  }
415

416
  private def validateCredentialOfferAttachment(
1✔
417
      credentialFormat: CredentialFormat,
418
      attachment: AttachmentDescriptor
419
  ): IO[InvalidCredentialOffer, Unit] = for {
1✔
420
    _ <- credentialFormat match
421
      case CredentialFormat.JWT | CredentialFormat.SDJWT =>
1✔
422
        attachment.data match
423
          case JsonData(json) =>
1✔
424
            ZIO
1✔
425
              .attempt(json.asJson.hcursor.downField("json").as[CredentialOfferAttachment])
1✔
426
              .mapError(e =>
427
                InvalidCredentialOffer(s"An error occurred when parsing the offer attachment: ${e.toString}")
×
428
              )
429
          case _ =>
×
430
            ZIO.fail(InvalidCredentialOffer(s"Only JSON attachments are supported in JWT offers"))
×
431
      case CredentialFormat.AnonCreds =>
1✔
432
        attachment.data match
433
          case Base64(value) =>
1✔
434
            for {
1✔
435
              _ <- ZIO
1✔
436
                .attempt(AnoncredCredentialOffer(value))
437
                .mapError(e =>
438
                  InvalidCredentialOffer(s"An error occurred when parsing the offer attachment: ${e.toString}")
×
439
                )
440
            } yield ()
1✔
441
          case _ =>
×
442
            ZIO.fail(InvalidCredentialOffer(s"Only Base64 attachments are supported in AnonCreds offers"))
×
443
  } yield ()
1✔
444

445
  private[this] def validatePrismDID(
1✔
446
      did: String
447
  ): IO[UnsupportedDidFormat, PrismDID] = ZIO
1✔
448
    .fromEither(PrismDID.fromString(did))
1✔
449
    .mapError(_ => UnsupportedDidFormat(did))
450

451
  private[this] def validateClaimsAgainstSchemaIfAny(
1✔
452
      claims: Json,
453
      maybeSchemaIds: Option[List[String]]
454
  ): UIO[Unit] = maybeSchemaIds match
455
    case Some(schemaIds) =>
1✔
456
      for {
1✔
457
        _ <- ZIO
1✔
458
          .collectAll(
459
            schemaIds.map(schemaId =>
1✔
460
              CredentialSchema
461
                .validateJWTCredentialSubject(schemaId, claims.noSpaces, uriResolver)
1✔
462
            )
463
          )
464
          .orDieAsUnmanagedFailure
1✔
465
      } yield ZIO.unit
1✔
466
    case None =>
1✔
467
      ZIO.unit
468

469
  private[this] def getCredentialDefinition(
1✔
470
      guid: UUID
471
  ): UIO[CredentialDefinition] = credentialDefinitionService
472
    .getByGUID(guid)
1✔
473
    .orDieAsUnmanagedFailure
1✔
474

475
  private[this] def getCredentialDefinitionPrivatePart(
1✔
476
      guid: UUID
477
  ): URIO[WalletAccessContext, CredentialDefinitionSecret] = for {
1✔
478
    maybeCredentialDefinitionSecret <- genericSecretStorage
1✔
479
      .get[UUID, CredentialDefinitionSecret](guid)
480
      .orDie
481
    credentialDefinitionSecret <- ZIO
1✔
482
      .fromOption(maybeCredentialDefinitionSecret)
483
      .mapError(_ => CredentialDefinitionPrivatePartNotFound(guid))
484
      .orDieAsUnmanagedFailure
1✔
485
  } yield credentialDefinitionSecret
486

487
  override def acceptCredentialOffer(
1✔
488
      recordId: DidCommID,
489
      maybeSubjectId: Option[String],
490
      keyId: Option[KeyId]
491
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] = {
492
    for {
1✔
493
      record <- getRecordWithState(recordId, ProtocolState.OfferReceived)
1✔
494
      count <- (record.credentialFormat, maybeSubjectId) match
1✔
495
        case (CredentialFormat.JWT | CredentialFormat.SDJWT, Some(subjectId)) =>
1✔
496
          for {
1✔
497
            _ <- validatePrismDID(subjectId)
1✔
498
            count <- credentialRepository
1✔
499
              .updateWithSubjectId(recordId, subjectId, keyId, ProtocolState.RequestPending)
1✔
500
              @@ CustomMetricsAspect.startRecordingTime(
1✔
501
                s"${record.id}_issuance_flow_holder_req_pending_to_generated"
1✔
502
              )
503
          } yield count
504
        case (CredentialFormat.AnonCreds, None) =>
1✔
505
          credentialRepository
1✔
506
            .updateProtocolState(recordId, ProtocolState.OfferReceived, ProtocolState.RequestPending)
1✔
507
            @@ CustomMetricsAspect.startRecordingTime(
1✔
508
              s"${record.id}_issuance_flow_holder_req_pending_to_generated"
1✔
509
            )
510
        case (format, maybeSubjectId) =>
×
511
          ZIO.dieMessage(s"Invalid subjectId input for $format offer acceptance: $maybeSubjectId")
×
512
      walletAccessContext <- ZIO.service[WalletAccessContext]
1✔
513
      _ <- messageProducer
1✔
514
        .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid))
1✔
515
        .orDie
516
      record <- credentialRepository.getById(record.id)
1✔
517
    } yield record
518
  }
519

520
  private def createPresentationPayload(
1✔
521
      record: IssueCredentialRecord,
522
      subject: JwtIssuer
523
  ): URIO[WalletAccessContext, PresentationPayload] = {
524
    for {
1✔
525
      maybeOptions <- getOptionsFromOfferCredentialData(record)
1✔
526
    } yield {
527
      W3cPresentationPayload(
1✔
528
        `@context` = Vector("https://www.w3.org/2018/presentations/v1"),
1✔
529
        maybeId = None,
530
        `type` = Vector("VerifiablePresentation"),
1✔
531
        verifiableCredential = IndexedSeq.empty,
1✔
532
        holder = subject.did.toString,
1✔
533
        verifier = IndexedSeq.empty ++ maybeOptions.map(_.domain),
1✔
534
        maybeIssuanceDate = None,
535
        maybeExpirationDate = None
536
      ).toJwtPresentationPayload.copy(maybeNonce = maybeOptions.map(_.challenge))
1✔
537
    }
538
  }
539

540
  private def getLongForm(
1✔
541
      did: PrismDID,
542
      allowUnpublishedIssuingDID: Boolean = false
×
543
  ): URIO[WalletAccessContext, LongFormPrismDID] = {
544
    for {
1✔
545
      maybeDidState <- managedDIDService
1✔
546
        .getManagedDIDState(did.asCanonical)
1✔
547
        .orDieWith(e => RuntimeException(s"Error occurred while getting DID from wallet: ${e.toString}"))
×
548
      didState <- ZIO
1✔
549
        .fromOption(maybeDidState)
550
        .mapError(_ => DIDNotFoundInWallet(did))
551
        .orDieAsUnmanagedFailure
1✔
552
      _ <- (didState match
1✔
553
        case s @ ManagedDIDState(_, _, PublicationState.Published(_)) => ZIO.succeed(s)
1✔
554
        case s => ZIO.cond(allowUnpublishedIssuingDID, s, DIDNotPublished(did, s.publicationState))
×
555
      ).orDieAsUnmanagedFailure
1✔
556
      longFormPrismDID = PrismDID.buildLongFormFromOperation(didState.createOperation)
1✔
557
    } yield longFormPrismDID
1✔
558
  }
559

560
  private[this] def getKeyId(
1✔
561
      did: PrismDID,
562
      verificationRelationship: VerificationRelationship,
563
      keyId: Option[KeyId]
564
  ): UIO[PublicKey] = {
565
    for {
1✔
566
      maybeDidData <- didService
1✔
567
        .resolveDID(did)
1✔
568
        .orDieWith(e => RuntimeException(s"Error occurred while resolving the DID: ${e.toString}"))
×
569
      didData <- ZIO
1✔
570
        .fromOption(maybeDidData)
571
        .mapError(_ => DIDNotResolved(did))
572
        .orDieAsUnmanagedFailure
1✔
573
      matchingKeys = didData._2.publicKeys.filter(pk => pk.purpose == verificationRelationship)
1✔
574
      result <- (matchingKeys, keyId) match {
1✔
575
        case (Seq(), _) =>
×
576
          ZIO.fail(KeyNotFoundInDID(did, verificationRelationship)).orDieAsUnmanagedFailure
×
577
        case (Seq(singleKey), None) =>
1✔
578
          ZIO.succeed(singleKey)
579
        case (multipleKeys, Some(kid)) =>
1✔
580
          ZIO
1✔
581
            .fromOption(multipleKeys.find(_.id.value.endsWith(kid.value)))
1✔
582
            .mapError(_ => KeyNotFoundInDID(did, verificationRelationship))
583
            .orDieAsUnmanagedFailure
1✔
584
        case (multipleKeys, None) =>
×
585
          ZIO
×
586
            .fail(
587
              MultipleKeysWithSamePurposeFoundInDID(did, verificationRelationship)
588
            )
589
            .orDieAsUnmanagedFailure
×
590
      }
591
    } yield result
592
  }
593

594
  override def getJwtIssuer(
1✔
595
      jwtIssuerDID: PrismDID,
596
      verificationRelationship: VerificationRelationship,
597
      keyId: Option[KeyId] = None
×
598
  ): URIO[WalletAccessContext, JwtIssuer] = {
599
    for {
1✔
600
      issuingPublicKey <- getKeyId(jwtIssuerDID, verificationRelationship, keyId)
1✔
601
      jwtIssuer <- managedDIDService
1✔
602
        .findDIDKeyPair(jwtIssuerDID.asCanonical, issuingPublicKey.id)
1✔
603
        .flatMap {
604
          case Some(keyPair: Secp256k1KeyPair) => {
1✔
605
            val jwtIssuer = JwtIssuer(
606
              jwtIssuerDID.did,
1✔
607
              ES256KSigner(keyPair.privateKey.toJavaPrivateKey, keyId),
1✔
608
              keyPair.publicKey.toJavaPublicKey
1✔
609
            )
610
            ZIO.some(jwtIssuer)
1✔
611
          }
612
          case Some(keyPair: Ed25519KeyPair) => {
×
613
            val jwtIssuer = JwtIssuer(
614
              jwtIssuerDID.did,
×
615
              EdSigner(keyPair, keyId),
×
616
              keyPair.publicKey.toJava
×
617
            )
618
            ZIO.some(jwtIssuer)
×
619
          }
620
          case _ => ZIO.none
×
621
        }
622
        .someOrFail(
623
          KeyPairNotFoundInWallet(jwtIssuerDID, issuingPublicKey.id, issuingPublicKey.publicKeyData.crv.name)
×
624
        )
625
        .orDieAsUnmanagedFailure
1✔
626
    } yield jwtIssuer
627
  }
628

629
  private def getEd25519SigningKeyPair(
×
630
      jwtIssuerDID: PrismDID,
631
      verificationRelationship: VerificationRelationship,
632
      keyId: Option[KeyId] = None
×
633
  ): URIO[WalletAccessContext, Ed25519KeyPair] = {
634
    for {
×
635
      issuingPublicKey <- getKeyId(jwtIssuerDID, verificationRelationship, keyId)
×
636
      ed25519keyPair <- managedDIDService
×
637
        .findDIDKeyPair(jwtIssuerDID.asCanonical, issuingPublicKey.id)
×
638
        .map(_.collect { case keyPair: Ed25519KeyPair => keyPair })
×
639
        .someOrFail(KeyPairNotFoundInWallet(jwtIssuerDID, issuingPublicKey.id, issuingPublicKey.publicKeyData.crv.name))
×
640
        .orDieAsUnmanagedFailure
×
641
    } yield ed25519keyPair
642
  }
643

644
  /** @param jwtIssuerDID
645
    *   This can holder prism did / issuer prism did
646
    * @param verificationRelationship
647
    *   Holder it Authentication and Issuer it is AssertionMethod
648
    * @param keyId
649
    *   Optional KID parameter in case of DID has multiple keys with same purpose
650
    * @return
651
    *   JwtIssuer
652
    * @see
653
    *   org.hyperledger.identus.pollux.vc.jwt.Issuer
654
    */
655
  private def getSDJwtIssuer(
×
656
      jwtIssuerDID: PrismDID,
657
      verificationRelationship: VerificationRelationship,
658
      keyId: Option[KeyId]
659
  ): URIO[WalletAccessContext, JwtIssuer] = {
660
    for {
×
661
      ed25519keyPair <- getEd25519SigningKeyPair(jwtIssuerDID, verificationRelationship, keyId)
×
662
    } yield {
663
      JwtIssuer(
664
        jwtIssuerDID.did,
×
665
        EdSigner(ed25519keyPair, keyId),
×
666
        ed25519keyPair.publicKey.toJava
×
667
      )
668
    }
669
  }
670

671
  private[this] def generateCredentialRequest(
1✔
672
      recordId: DidCommID,
673
      getIssuer: (
674
          did: LongFormPrismDID,
675
          verificationRelation: VerificationRelationship,
676
          keyId: Option[KeyId]
677
      ) => URIO[WalletAccessContext, JwtIssuer]
678
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] = {
679
    for {
1✔
680
      record <- getRecordWithState(recordId, ProtocolState.RequestPending)
1✔
681
      subjectId <- ZIO
1✔
682
        .fromOption(record.subjectId)
683
        .orDieWith(_ => RuntimeException(s"No 'subjectId' found in record: ${recordId.value}"))
×
684
      formatAndOffer <- ZIO
1✔
685
        .fromOption(record.offerCredentialFormatAndData)
1✔
686
        .orDieWith(_ => RuntimeException(s"No 'offer' found in record: ${recordId.value}"))
×
687
      subjectDID <- validatePrismDID(subjectId)
1✔
688
      longFormPrismDID <- getLongForm(subjectDID, true)
1✔
689
      jwtIssuer <- getIssuer(longFormPrismDID, VerificationRelationship.Authentication, record.keyId)
1✔
690
      presentationPayload <- createPresentationPayload(record, jwtIssuer)
1✔
691
      signedPayload = JwtPresentation.encodeJwt(presentationPayload.toJwtPresentationPayload, jwtIssuer)
1✔
692
      request = createDidCommRequestCredential(formatAndOffer._1, formatAndOffer._2, signedPayload)
1✔
693
      count <- credentialRepository
1✔
694
        .updateWithJWTRequestCredential(recordId, request, ProtocolState.RequestGenerated)
1✔
695
        @@ CustomMetricsAspect.endRecordingTime(
1✔
696
          s"${record.id}_issuance_flow_holder_req_pending_to_generated",
1✔
697
          "issuance_flow_holder_req_pending_to_generated_ms_gauge"
698
        ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_holder_req_generated_to_sent")
1✔
699
      walletAccessContext <- ZIO.service[WalletAccessContext]
1✔
700
      _ <- messageProducer
1✔
701
        .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid))
1✔
702
        .orDie
703
      record <- credentialRepository.getById(record.id)
1✔
704
    } yield record
705
  }
706

707
  override def generateJWTCredentialRequest(
1✔
708
      recordId: DidCommID
709
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] =
710
    generateCredentialRequest(recordId, getJwtIssuer)
1✔
711

712
  override def generateSDJWTCredentialRequest(
×
713
      recordId: DidCommID
714
  ): ZIO[WalletAccessContext, RecordNotFound | UnsupportedDidFormat, IssueCredentialRecord] =
715
    generateCredentialRequest(recordId, getSDJwtIssuer)
×
716

717
  override def generateAnonCredsCredentialRequest(
1✔
718
      recordId: DidCommID
719
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
720
    for {
1✔
721
      record <- getRecordWithState(recordId, ProtocolState.RequestPending)
1✔
722
      offerCredential <- ZIO
1✔
723
        .fromOption(record.offerCredentialData)
724
        .orDieWith(_ => RuntimeException(s"No 'offer' found in record: ${recordId.value}"))
×
725
      body = RequestCredential.Body(goal_code = Some("Request Credential"))
1✔
726
      createCredentialRequest <- createAnonCredsRequestCredential(offerCredential)
1✔
727
      attachments = Seq(
1✔
728
        AttachmentDescriptor.buildBase64Attachment(
1✔
729
          mediaType = Some("application/json"),
730
          format = Some(IssueCredentialRequestFormat.Anoncred.name),
731
          payload = createCredentialRequest.request.data.getBytes()
1✔
732
        )
733
      )
734
      requestMetadata = createCredentialRequest.metadata
735
      request = RequestCredential(
1✔
736
        body = body,
737
        attachments = attachments,
738
        from =
UNCOV
739
          offerCredential.to.getOrElse(throw new IllegalArgumentException("OfferCredential must have a recipient")),
×
740
        to = offerCredential.from,
741
        thid = offerCredential.thid
742
      )
743
      count <- credentialRepository
1✔
744
        .updateWithAnonCredsRequestCredential(recordId, request, requestMetadata, ProtocolState.RequestGenerated)
1✔
745
        @@ CustomMetricsAspect.endRecordingTime(
1✔
746
          s"${record.id}_issuance_flow_holder_req_pending_to_generated",
1✔
747
          "issuance_flow_holder_req_pending_to_generated_ms_gauge"
748
        ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_holder_req_generated_to_sent")
1✔
749
      walletAccessContext <- ZIO.service[WalletAccessContext]
1✔
750
      _ <- messageProducer
1✔
751
        .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid))
1✔
752
        .orDie
753
      record <- credentialRepository.getById(record.id)
1✔
754
    } yield record
755
  }
756

757
  private def createAnonCredsRequestCredential(
1✔
758
      offerCredential: OfferCredential
759
  ): URIO[WalletAccessContext, AnoncredCreateCrendentialRequest] = {
760
    for {
1✔
761
      attachmentData <- ZIO
1✔
762
        .fromOption(
763
          offerCredential.attachments
764
            .find(_.format.contains(IssueCredentialOfferFormat.Anoncred.name))
1✔
765
            .map(_.data)
1✔
766
            .flatMap {
1✔
767
              case Base64(value) => Some(new String(java.util.Base64.getUrlDecoder.decode(value)))
1✔
768
              case _             => None
×
769
            }
770
        )
771
        .orDieWith(_ => RuntimeException(s"No AnonCreds attachment found in the offer"))
×
772
      credentialOffer = anoncreds.AnoncredCredentialOffer(attachmentData)
773
      credDefContent <- uriResolver
1✔
774
        .resolve(credentialOffer.getCredDefId)
1✔
775
        .orDieAsUnmanagedFailure
1✔
776
      credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent)
777
      linkSecret <- linkSecretService.fetchOrCreate()
1✔
778
      createCredentialRequest = AnoncredLib.createCredentialRequest(linkSecret, credentialDefinition, credentialOffer)
1✔
779
    } yield createCredentialRequest
1✔
780
  }
781

782
  override def receiveCredentialRequest(
1✔
783
      request: RequestCredential
784
  ): ZIO[WalletAccessContext, InvalidCredentialRequest | RecordNotFoundForThreadIdAndStates, IssueCredentialRecord] = {
785
    for {
1✔
786
      thid <- ZIO
1✔
787
        .fromOption(request.thid.map(DidCommID(_)))
1✔
788
        .mapError(_ => InvalidCredentialRequest("No 'thid' found"))
789
      record <- getRecordWithThreadIdAndStates(
1✔
790
        thid,
791
        ignoreWithZeroRetries = true,
792
        ProtocolState.InvitationGenerated,
793
        ProtocolState.OfferPending,
794
        ProtocolState.OfferSent
795
      )
796
      _ <- credentialRepository.updateWithJWTRequestCredential(record.id, request, ProtocolState.RequestReceived)
1✔
797
      walletAccessContext <- ZIO.service[WalletAccessContext]
1✔
798
      _ <- messageProducer
1✔
799
        .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid))
1✔
800
        .orDie
801
      record <- credentialRepository.getById(record.id)
1✔
802
    } yield record
803
  }
804

805
  override def acceptCredentialRequest(
1✔
806
      recordId: DidCommID
807
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
808
    for {
1✔
809
      record <- getRecordWithState(recordId, ProtocolState.RequestReceived)
1✔
810
      request <- ZIO
1✔
811
        .fromOption(record.requestCredentialData)
812
        .orDieWith(_ => RuntimeException(s"No 'requestCredentialData' found in record: ${recordId.value}"))
×
813
      issue = createDidCommIssueCredential(request)
1✔
814
      count <- credentialRepository
1✔
815
        .updateWithIssueCredential(recordId, issue, ProtocolState.CredentialPending)
1✔
816
        @@ CustomMetricsAspect.startRecordingTime(
1✔
817
          s"${record.id}_issuance_flow_issuer_credential_pending_to_generated"
1✔
818
        )
819
      walletAccessContext <- ZIO.service[WalletAccessContext]
1✔
820
      _ <- messageProducer
1✔
821
        .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid))
1✔
822
        .orDie
823
      record <- credentialRepository.getById(record.id)
1✔
824
    } yield record
825
  }
826

827
  override def receiveCredentialIssue(
1✔
828
      issueCredential: IssueCredential
829
  ): ZIO[WalletAccessContext, InvalidCredentialIssue | RecordNotFoundForThreadIdAndStates, IssueCredentialRecord] =
830
    for {
1✔
831
      thid <- ZIO
1✔
832
        .fromOption(issueCredential.thid.map(DidCommID(_)))
1✔
833
        .mapError(_ => InvalidCredentialIssue("No 'thid' found"))
834
      record <- getRecordWithThreadIdAndStates(
1✔
835
        thid,
836
        ignoreWithZeroRetries = true,
837
        ProtocolState.RequestPending,
838
        ProtocolState.RequestSent
839
      )
840
      attachment <- ZIO
1✔
841
        .fromOption(issueCredential.attachments.headOption)
1✔
842
        .mapError(_ => InvalidCredentialIssue("No attachment found"))
843

844
      _ <- {
1✔
845
        val result = attachment match {
846
          case AttachmentDescriptor(
847
                id,
848
                media_type,
849
                Base64(v),
850
                Some(IssueCredentialIssuedFormat.Anoncred.name),
851
                _,
852
                _,
853
                _,
854
                _
855
              ) =>
1✔
856
            for {
1✔
857
              processedCredential <- processAnonCredsCredential(record, java.util.Base64.getUrlDecoder.decode(v))
1✔
858
              attachment = AttachmentDescriptor.buildBase64Attachment(
1✔
859
                id = id,
860
                mediaType = media_type,
861
                format = Some(IssueCredentialIssuedFormat.Anoncred.name),
862
                payload = processedCredential.data.getBytes
1✔
863
              )
864
              processedIssuedCredential = issueCredential.copy(attachments = Seq(attachment))
1✔
865
              result <-
1✔
866
                updateWithCredential(
1✔
867
                  processedIssuedCredential,
868
                  record,
869
                  attachment,
870
                  Some(List(processedCredential.getSchemaId)),
1✔
871
                  Some(processedCredential.getCredDefId)
1✔
872
                )
873
            } yield result
874
          case attachment =>
1✔
875
            updateWithCredential(issueCredential, record, attachment, None, None)
1✔
876
        }
877
        result
878
      }
879
      record <- credentialRepository.getById(record.id)
1✔
880
    } yield record
881

882
  private def updateWithCredential(
1✔
883
      issueCredential: IssueCredential,
884
      record: IssueCredentialRecord,
885
      attachment: AttachmentDescriptor,
886
      schemaId: Option[List[String]],
887
      credDefId: Option[String]
888
  ) = {
889
    credentialRepository
890
      .updateWithIssuedRawCredential(
1✔
891
        record.id,
892
        issueCredential,
893
        attachment.data.asJson.noSpaces,
1✔
894
        schemaId,
895
        credDefId,
896
        ProtocolState.CredentialReceived
897
      )
898
  }
899

900
  private def processAnonCredsCredential(
1✔
901
      record: IssueCredentialRecord,
902
      credentialBytes: Array[Byte]
903
  ): URIO[WalletAccessContext, anoncreds.AnoncredCredential] = {
904
    for {
1✔
905
      credential <- ZIO.succeed(anoncreds.AnoncredCredential(new String(credentialBytes)))
1✔
906
      credDefContent <- uriResolver
1✔
907
        .resolve(credential.getCredDefId)
1✔
908
        .orDieAsUnmanagedFailure
1✔
909
      credentialDefinition = anoncreds.AnoncredCredentialDefinition(credDefContent)
910
      metadata <- ZIO
1✔
911
        .fromOption(record.anonCredsRequestMetadata)
912
        .orDieWith(_ => RuntimeException(s"No AnonCreds request metadata found in record: ${record.id.value}"))
×
913
      linkSecret <- linkSecretService.fetchOrCreate()
1✔
914
      credential <- ZIO
1✔
915
        .attempt(
916
          AnoncredLib.processCredential(
1✔
917
            anoncreds.AnoncredCredential(new String(credentialBytes)),
1✔
918
            metadata,
919
            linkSecret,
920
            credentialDefinition
921
          )
922
        )
923
        .orDieWith(error => RuntimeException(s"AnonCreds credential processing error: ${error.getMessage}"))
×
924
    } yield credential
925
  }
926

927
  override def markOfferSent(
1✔
928
      recordId: DidCommID
929
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
930
    updateCredentialRecordProtocolState(
1✔
931
      recordId,
932
      IssueCredentialRecord.ProtocolState.OfferPending,
933
      IssueCredentialRecord.ProtocolState.OfferSent
934
    )
935

936
  override def markCredentialOfferInvitationExpired(
×
937
      recordId: DidCommID
938
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
939
    updateCredentialRecordProtocolState(
×
940
      recordId,
941
      IssueCredentialRecord.ProtocolState.RequestReceived,
942
      IssueCredentialRecord.ProtocolState.InvitationExpired
943
    )
944
  override def markRequestSent(
1✔
945
      recordId: DidCommID
946
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
947
    updateCredentialRecordProtocolState(
1✔
948
      recordId,
949
      IssueCredentialRecord.ProtocolState.RequestGenerated,
950
      IssueCredentialRecord.ProtocolState.RequestSent
951
    ) @@ CustomMetricsAspect.endRecordingTime(
1✔
952
      s"${recordId}_issuance_flow_holder_req_generated_to_sent",
1✔
953
      "issuance_flow_holder_req_generated_to_sent_ms_gauge"
954
    )
955

956
  private def markCredentialGenerated(
1✔
957
      record: IssueCredentialRecord,
958
      issueCredential: IssueCredential
959
  ): URIO[WalletAccessContext, IssueCredentialRecord] = {
960
    for {
1✔
961
      count <- credentialRepository
1✔
962
        .updateWithIssueCredential(record.id, issueCredential, IssueCredentialRecord.ProtocolState.CredentialGenerated)
1✔
963
        @@ CustomMetricsAspect.endRecordingTime(
1✔
964
          s"${record.id}_issuance_flow_issuer_credential_pending_to_generated",
1✔
965
          "issuance_flow_issuer_credential_pending_to_generated_ms_gauge"
966
        ) @@ CustomMetricsAspect.startRecordingTime(s"${record.id}_issuance_flow_issuer_credential_generated_to_sent")
1✔
967
      walletAccessContext <- ZIO.service[WalletAccessContext]
1✔
968
      _ <- messageProducer
1✔
969
        .produce(TOPIC_NAME, record.id.uuid, WalletIdAndRecordId(walletAccessContext.walletId.toUUID, record.id.uuid))
1✔
970
        .orDie
971
      record <- credentialRepository.getById(record.id)
1✔
972
    } yield record
973
  }
974

975
  override def markCredentialSent(
1✔
976
      recordId: DidCommID
977
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] =
978
    updateCredentialRecordProtocolState(
1✔
979
      recordId,
980
      IssueCredentialRecord.ProtocolState.CredentialGenerated,
981
      IssueCredentialRecord.ProtocolState.CredentialSent
982
    ) @@ CustomMetricsAspect.endRecordingTime(
1✔
983
      s"${recordId}_issuance_flow_issuer_credential_generated_to_sent",
1✔
984
      "issuance_flow_issuer_credential_generated_to_sent_ms_gauge"
985
    )
986

987
  override def reportProcessingFailure(
×
988
      recordId: DidCommID,
989
      failReason: Option[Failure]
990
  ): URIO[WalletAccessContext, Unit] =
991
    credentialRepository.updateAfterFail(recordId, failReason)
×
992

993
  private def getRecordWithState(
1✔
994
      recordId: DidCommID,
995
      state: ProtocolState
996
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
997
    for {
1✔
998
      record <- credentialRepository.getById(recordId)
1✔
999
      _ <- record.protocolState match {
1✔
1000
        case s if s == state => ZIO.unit
1✔
1001
        case s               => ZIO.fail(RecordNotFound(recordId, Some(s)))
1✔
1002
      }
1003
    } yield record
1✔
1004
  }
1005

1006
  private def getRecordWithThreadIdAndStates(
1✔
1007
      thid: DidCommID,
1008
      ignoreWithZeroRetries: Boolean,
1009
      states: ProtocolState*
1010
  ): ZIO[WalletAccessContext, RecordNotFoundForThreadIdAndStates, IssueCredentialRecord] = {
1011
    for {
1✔
1012
      record <- credentialRepository
1✔
1013
        .findByThreadId(thid, ignoreWithZeroRetries)
1✔
1014
        .someOrFail(RecordNotFoundForThreadIdAndStates(thid, states*))
1015
      _ <- record.protocolState match {
1✔
1016
        case s if states.contains(s) => ZIO.unit
1✔
1017
        case state                   => ZIO.fail(RecordNotFoundForThreadIdAndStates(thid, states*))
1✔
1018
      }
1019
    } yield record
1✔
1020
  }
1021

1022
  private def createDidCommOfferCredential(
1✔
1023
      pairwiseIssuerDID: DidId,
1024
      pairwiseHolderDID: Option[DidId],
1025
      maybeSchemaIds: Option[List[String]],
1026
      claims: Seq[Attribute],
1027
      thid: DidCommID,
1028
      challenge: String,
1029
      domain: String,
1030
      offerFormat: IssueCredentialOfferFormat
1031
  ): UIO[OfferCredential] = {
1032
    for {
1✔
1033
      credentialPreview <- ZIO.succeed(CredentialPreview(schema_ids = maybeSchemaIds, attributes = claims))
1✔
1034
      body = OfferCredential.Body(
1✔
1035
        goal_code = Some("Offer Credential"),
1036
        credential_preview = credentialPreview,
1037
      )
1038
      attachments <- ZIO.succeed(
1✔
1039
        Seq(
1✔
1040
          AttachmentDescriptor.buildJsonAttachment(
1✔
1041
            mediaType = Some("application/json"),
1042
            format = Some(offerFormat.name),
1043
            payload = PresentationAttachment(
1044
              Some(Options(challenge, domain)),
1045
              PresentationDefinition(format = Some(ClaimFormat(jwt = Some(Jwt(alg = Seq("ES256K"))))))
1✔
1046
            )
1047
          )
1048
        )
1049
      )
1050
    } yield OfferCredential(
1✔
1051
      body = body,
1052
      attachments = attachments,
1053
      from = pairwiseIssuerDID,
1054
      to = pairwiseHolderDID,
1055
      thid = Some(thid.value)
1✔
1056
    )
1057
  }
1058

1059
  private def createAnonCredsDidCommOfferCredential(
1✔
1060
      pairwiseIssuerDID: DidId,
1061
      pairwiseHolderDID: Option[DidId],
1062
      schemaUri: String,
1063
      credentialDefinitionGUID: UUID,
1064
      credentialDefinitionId: String,
1065
      claims: Seq[Attribute],
1066
      thid: DidCommID
1067
  ): URIO[WalletAccessContext, OfferCredential] = {
1068
    for {
1✔
1069
      credentialPreview <- ZIO.succeed(CredentialPreview(schema_ids = Some(List(schemaUri)), attributes = claims))
1✔
1070
      body = OfferCredential.Body(
1✔
1071
        goal_code = Some("Offer Credential"),
1072
        credential_preview = credentialPreview,
1073
      )
1074
      attachments <- createAnonCredsCredentialOffer(credentialDefinitionGUID, credentialDefinitionId).map { offer =>
1✔
1075
        Seq(
1✔
1076
          AttachmentDescriptor.buildBase64Attachment(
1✔
1077
            mediaType = Some("application/json"),
1078
            format = Some(IssueCredentialOfferFormat.Anoncred.name),
1079
            payload = offer.data.getBytes()
1✔
1080
          )
1081
        )
1082
      }
1083
    } yield OfferCredential(
1✔
1084
      body = body,
1085
      attachments = attachments,
1086
      from = pairwiseIssuerDID,
1087
      to = pairwiseHolderDID,
1088
      thid = Some(thid.value)
1✔
1089
    )
1090
  }
1091

1092
  private def createAnonCredsCredentialOffer(
1✔
1093
      credentialDefinitionGUID: UUID,
1094
      credentialDefinitionId: String
1095
  ): URIO[WalletAccessContext, AnoncredCredentialOffer] =
1096
    for {
1✔
1097
      credentialDefinition <- getCredentialDefinition(credentialDefinitionGUID)
1✔
1098
      cd = anoncreds.AnoncredCredentialDefinition(credentialDefinition.definition.toString)
1✔
1099
      kcp = anoncreds.AnoncredCredentialKeyCorrectnessProof(credentialDefinition.keyCorrectnessProof.toString)
1✔
1100
      credentialDefinitionSecret <- getCredentialDefinitionPrivatePart(credentialDefinition.guid)
1✔
1101
      cdp = anoncreds.AnoncredCredentialDefinitionPrivate(credentialDefinitionSecret.json.toString)
1✔
1102
      createCredentialDefinition = AnoncredCreateCredentialDefinition(cd, cdp, kcp)
1103
      offer = AnoncredLib.createOffer(createCredentialDefinition, credentialDefinitionId)
1✔
1104
    } yield offer
1✔
1105

1106
  private[this] def createDidCommRequestCredential(
1✔
1107
      format: IssueCredentialOfferFormat,
1108
      offer: OfferCredential,
1109
      signedPresentation: JWT
1110
  ): RequestCredential = {
1111
    RequestCredential(
1✔
1112
      body = RequestCredential.Body(
1113
        goal_code = offer.body.goal_code,
1114
        comment = offer.body.comment,
1115
      ),
1116
      attachments = Seq(
1✔
1117
        AttachmentDescriptor
1118
          .buildBase64Attachment(
1✔
1119
            mediaType = Some("application/json"),
1120
            format = Some(format.name),
1121
            // FIXME copy payload will probably not work for anoncreds!
1122
            payload = signedPresentation.value.getBytes(),
1✔
1123
          )
1124
      ),
1125
      thid = offer.thid.orElse(Some(offer.id)),
1✔
1126
      from = offer.to.getOrElse(throw new IllegalArgumentException("OfferCredential must have a recipient")),
1✔
1127
      to = offer.from
1128
    )
1129
  }
1130

1131
  private def createDidCommIssueCredential(request: RequestCredential): IssueCredential = {
1✔
1132
    IssueCredential(
1✔
1133
      body = IssueCredential.Body(
1134
        goal_code = request.body.goal_code,
1135
        comment = request.body.comment,
1136
        replacement_id = None,
1137
        more_available = None,
1138
      ),
1139
      attachments = Seq(), // FIXME !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1✔
1140
      thid = request.thid.orElse(Some(request.id)),
1✔
1141
      from = request.to,
1142
      to = request.from
1143
    )
1144
  }
1145

1146
  /** this is an auxiliary function.
1147
    *
1148
    * @note
1149
    *   Between updating and getting the CredentialRecord back the CredentialRecord can be updated by other operations
1150
    *   in the middle.
1151
    *
1152
    * TODO: this should be improved to behave exactly like atomic operation.
1153
    */
1154
  private def updateCredentialRecordProtocolState(
1✔
1155
      id: DidCommID,
1156
      from: IssueCredentialRecord.ProtocolState,
1157
      to: IssueCredentialRecord.ProtocolState
1158
  ): ZIO[WalletAccessContext, InvalidStateForOperation, IssueCredentialRecord] = {
1159
    for {
1✔
1160
      record <- credentialRepository.getById(id)
1✔
1161
      updatedRecord <- record.protocolState match
1✔
1162
        case currentState if currentState == to => ZIO.succeed(record) // Idempotent behaviour
×
1163
        case currentState if currentState == from =>
1✔
1164
          credentialRepository.updateProtocolState(id, from, to) *> credentialRepository.getById(id)
1✔
1165
        case _ => ZIO.fail(InvalidStateForOperation(record.protocolState))
×
1166
    } yield updatedRecord
1167
  }
1168

1169
  override def generateJWTCredential(
1✔
1170
      recordId: DidCommID,
1171
      statusListRegistryUrl: String,
1172
  ): ZIO[WalletAccessContext, RecordNotFound | CredentialRequestValidationFailed, IssueCredentialRecord] = {
1173
    for {
1✔
1174
      record <- getRecordWithState(recordId, ProtocolState.CredentialPending)
1✔
1175
      issuingDID <- ZIO
1✔
1176
        .fromOption(record.issuingDID)
1177
        .orElse(ZIO.dieMessage(s"Issuing DID not found in record: ${recordId.value}"))
×
1178
      issue <- ZIO
1✔
1179
        .fromOption(record.issueCredentialData)
1180
        .orElse(ZIO.dieMessage(s"Issue credential data not found in record: ${recordId.value}"))
×
1181
      longFormPrismDID <- getLongForm(issuingDID, true)
1✔
1182
      maybeOfferOptions <- getOptionsFromOfferCredentialData(record)
1✔
1183
      requestJwt <- getJwtFromRequestCredentialData(record)
1✔
1184
      offerCredentialData <- ZIO
1✔
1185
        .fromOption(record.offerCredentialData)
1186
        .orElse(ZIO.dieMessage(s"Offer credential data not found in record: ${recordId.value}"))
×
1187
      preview = offerCredentialData.body.credential_preview
1188
      claims <- CredentialService.convertAttributesToJsonClaims(preview.body.attributes).orDieAsUnmanagedFailure
1✔
1189
      jwtIssuer <- getJwtIssuer(longFormPrismDID, VerificationRelationship.AssertionMethod, record.keyId)
1✔
1190
      jwtPresentation <- validateRequestCredentialDataProof(maybeOfferOptions, requestJwt)
1✔
1191
        .tapError(error =>
1192
          credentialRepository
1193
            .updateProtocolState(record.id, ProtocolState.CredentialPending, ProtocolState.ProblemReportPending)
×
1194
        )
1195
        .orDieAsUnmanagedFailure
1✔
1196

1197
      // Custom for JWT
1198
      issuanceDate = Instant.now()
1✔
1199
      credentialStatus <- allocateNewCredentialInStatusListForWallet(record, statusListRegistryUrl, jwtIssuer)
1✔
1200
      // TODO: get schema when schema registry is available if schema ID is provided
1201
      w3Credential = W3cCredentialPayload(
1✔
1202
        `@context` = Set(
1✔
1203
          "https://www.w3.org/2018/credentials/v1"
1204
        ), // TODO: his information should come from Schema registry by record.schemaId
1205
        maybeId = None,
1206
        `type` =
1207
          Set("VerifiableCredential"), // TODO: This information should come from Schema registry by record.schemaId
1✔
1208
        issuer = CredentialIssuer(issuingDID.did.toString, `type` = "Profile"),
1✔
1209
        issuanceDate = issuanceDate,
1210
        maybeExpirationDate = record.validityPeriod.map(sec => issuanceDate.plusSeconds(sec.toLong)),
×
1211
        maybeCredentialSchema = record.schemaUris.map(ids =>
1✔
1212
          ids.map(id => org.hyperledger.identus.pollux.vc.jwt.CredentialSchema(id, VC_JSON_SCHEMA_TYPE))
×
1213
        ),
1214
        maybeCredentialStatus = Some(credentialStatus),
1215
        credentialSubject = claims.add("id", jwtPresentation.iss.asJson).asJson,
1✔
1216
        maybeRefreshService = None,
1217
        maybeEvidence = None,
1218
        maybeTermsOfUse = None,
1219
        maybeValidFrom = None,
1220
        maybeValidUntil = None
1221
      )
1222
      signedJwtCredential = W3CCredential.toEncodedJwt(w3Credential, jwtIssuer)
1✔
1223
      issueCredential = IssueCredential.build(
1✔
1224
        fromDID = issue.from,
1225
        toDID = issue.to,
1226
        thid = issue.thid,
1227
        credentials = Seq(IssueCredentialIssuedFormat.JWT -> signedJwtCredential.value.getBytes)
1✔
1228
      )
1229
      // End custom
1230

1231
      record <- markCredentialGenerated(record, issueCredential)
1✔
1232
    } yield record
1233
  }
1234

1235
  override def generateSDJWTCredential(
×
1236
      recordId: DidCommID,
1237
      expirationTime: Duration,
1238
  ): ZIO[
1239
    WalletAccessContext,
1240
    RecordNotFound | ExpirationDateHasPassed | VCJwtHeaderParsingError,
1241
    IssueCredentialRecord
1242
  ] = {
1243
    for {
×
1244
      record <- getRecordWithState(recordId, ProtocolState.CredentialPending)
×
1245
      issuingDID <- ZIO
×
1246
        .fromOption(record.issuingDID)
1247
        .orElse(ZIO.dieMessage(s"Issuing DID not found in record: ${recordId.value}"))
×
1248
      issue <- ZIO
×
1249
        .fromOption(record.issueCredentialData)
1250
        .orElse(ZIO.dieMessage(s"Issue credential data not found in record: ${recordId.value}"))
×
1251
      longFormPrismDID <- getLongForm(issuingDID, true)
×
1252
      maybeOfferOptions <- getOptionsFromOfferCredentialData(record)
×
1253
      requestJwt <- getJwtFromRequestCredentialData(record)
×
1254
      offerCredentialData <- ZIO
×
1255
        .fromOption(record.offerCredentialData)
1256
        .orElse(ZIO.dieMessage(s"Offer credential data not found in record: ${recordId.value}"))
×
1257
      preview = offerCredentialData.body.credential_preview
1258
      claims <- CredentialService.convertAttributesToJsonClaims(preview.body.attributes).orDieAsUnmanagedFailure
×
1259
      jwtPresentation <- validateRequestCredentialDataProof(maybeOfferOptions, requestJwt)
×
1260
        .tapError(error =>
1261
          credentialRepository
1262
            .updateProtocolState(record.id, ProtocolState.CredentialPending, ProtocolState.ProblemReportPending)
×
1263
        )
1264
        .orDieAsUnmanagedFailure
×
1265
      jwtHeader <- JWTVerification.extractJwtHeader(requestJwt) match
×
1266
        case ZValidation.Success(log, header) => ZIO.succeed(header)
×
1267
        case ZValidation.Failure(log, failure) =>
×
1268
          ZIO.fail(VCJwtHeaderParsingError(s"Extraction of JwtHeader failed ${failure.toChunk.toString}"))
×
1269
      ed25519KeyPair <- getEd25519SigningKeyPair(
×
1270
        longFormPrismDID,
1271
        VerificationRelationship.AssertionMethod,
1272
        record.keyId
1273
      )
1274
      sdJwtPrivateKey = sdjwt.IssuerPrivateKey(ed25519KeyPair.privateKey)
×
1275
      jsonWebKey <- didResolver.resolve(jwtPresentation.iss) flatMap {
×
1276
        case failed: DIDResolutionFailed =>
×
1277
          ZIO.dieMessage(s"Error occurred while resolving the DID: ${failed.error.toString}")
×
1278
        case succeeded: DIDResolutionSucceeded =>
×
1279
          jwtHeader.keyId match {
1280
            case Some(
1281
                  kid
1282
                ) => // TODO should we check in authentication and assertion or just in verificationMethod since this cane different how did document is implemented
×
1283
              ZIO
×
1284
                .fromOption(succeeded.didDocument.verificationMethod.find(_.id.endsWith(kid)).map(_.publicKeyJwk))
×
1285
                .orElse(
1286
                  ZIO.dieMessage(
×
1287
                    s"Required public Key for holder binding is not found in DID document for the kid: $kid"
×
1288
                  )
1289
                )
1290
            case None =>
×
1291
              ZIO.succeed(None) // JwtHeader keyId is None, Issued credential is not bound to any holder public key
1292
          }
1293
      }
1294

1295
      now = Instant.now.getEpochSecond
×
1296
      exp = claims("exp").flatMap(_.asNumber).flatMap(_.toLong)
×
1297
      expInSeconds <- ZIO.fromEither(exp match {
×
1298
        case Some(e) if e > now => Right(e)
×
1299
        case Some(e)            => Left(ExpirationDateHasPassed(e))
×
1300
        case _                  => Right(Instant.now.plus(expirationTime).getEpochSecond)
×
1301
      })
1302
      claimsUpdated = claims
1303
        .add("iss", issuingDID.did.toString.asJson) // This is issuer did
×
1304
        .add("sub", jwtPresentation.iss.asJson) // This is subject did
×
1305
        .add("iat", now.asJson)
×
1306
        .add("exp", expInSeconds.asJson)
×
1307
      credential = {
1308
        jsonWebKey match {
1309
          case Some(jwk) =>
×
1310
            SDJWT.issueCredential(
×
1311
              sdJwtPrivateKey,
1312
              claimsUpdated.asJson.noSpaces,
×
1313
              sdjwt.HolderPublicKey.fromJWT(jwk.toJson)
×
1314
            )
1315
          case None =>
×
1316
            SDJWT.issueCredential(
×
1317
              sdJwtPrivateKey,
1318
              claimsUpdated.asJson.noSpaces,
×
1319
            )
1320
        }
1321
      }
1322
      issueCredential = IssueCredential.build(
×
1323
        fromDID = issue.from,
1324
        toDID = issue.to,
1325
        thid = issue.thid,
1326
        credentials = Seq(IssueCredentialIssuedFormat.SDJWT -> credential.compact.getBytes)
×
1327
      )
1328
      record <- markCredentialGenerated(record, issueCredential)
×
1329
    } yield record
1330

1331
  }
1332

1333
  private def allocateNewCredentialInStatusListForWallet(
1✔
1334
      record: IssueCredentialRecord,
1335
      statusListRegistryUrl: String,
1336
      jwtIssuer: JwtIssuer
1337
  ): URIO[WalletAccessContext, CredentialStatus] =
1338
    for {
1✔
1339
      cslAndIndex <- credentialStatusListRepository.incrementAndGetStatusListIndex(
1✔
1340
        jwtIssuer,
1341
        statusListRegistryUrl
1342
      )
1343
      statusListId = cslAndIndex._1
1✔
1344
      indexInStatusList = cslAndIndex._2
1✔
1345
      _ <- credentialStatusListRepository.allocateSpaceForCredential(
1✔
1346
        issueCredentialRecordId = record.id,
1347
        credentialStatusListId = statusListId,
1348
        statusListIndex = indexInStatusList
1349
      )
1350
    } yield CredentialStatus(
1✔
1351
      id = s"$statusListRegistryUrl/credential-status/$statusListId#$indexInStatusList",
1✔
1352
      `type` = "StatusList2021Entry",
1353
      statusPurpose = StatusPurpose.Revocation,
1354
      statusListIndex = indexInStatusList,
1355
      statusListCredential = s"$statusListRegistryUrl/credential-status/$statusListId"
1✔
1356
    )
1357

1358
  override def generateAnonCredsCredential(
1✔
1359
      recordId: DidCommID
1360
  ): ZIO[WalletAccessContext, RecordNotFound, IssueCredentialRecord] = {
1361
    for {
1✔
1362
      record <- getRecordWithState(recordId, ProtocolState.CredentialPending)
1✔
1363
      requestCredential <- ZIO
1✔
1364
        .fromOption(record.requestCredentialData)
1365
        .orElse(ZIO.dieMessage(s"No request credential data found in record: ${record.id}"))
×
1366
      body = IssueCredential.Body(goal_code = Some("Issue Credential"))
1✔
1367
      attachments <- createAnonCredsCredential(record).map { credential =>
1✔
1368
        Seq(
1✔
1369
          AttachmentDescriptor.buildBase64Attachment(
1✔
1370
            mediaType = Some("application/json"),
1371
            format = Some(IssueCredentialIssuedFormat.Anoncred.name),
1372
            payload = credential.data.getBytes()
1✔
1373
          )
1374
        )
1375
      }
1376
      issueCredential = IssueCredential(
1✔
1377
        body = body,
1378
        attachments = attachments,
1379
        from = requestCredential.to,
1380
        to = requestCredential.from,
1381
        thid = requestCredential.thid
1382
      )
1383
      record <- markCredentialGenerated(record, issueCredential)
1✔
1384
    } yield record
1385
  }
1386

1387
  private def createAnonCredsCredential(
1✔
1388
      record: IssueCredentialRecord
1389
  ): URIO[WalletAccessContext, AnoncredCredential] = {
1390
    for {
1✔
1391
      credentialDefinitionId <- ZIO
1✔
1392
        .fromOption(record.credentialDefinitionId)
1393
        .orElse(ZIO.dieMessage(s"No credential definition Id found in record: ${record.id}"))
×
1394
      credentialDefinition <- getCredentialDefinition(credentialDefinitionId)
1✔
1395
      cd = anoncreds.AnoncredCredentialDefinition(credentialDefinition.definition.toString)
1✔
1396
      offerCredential <- ZIO
1✔
1397
        .fromOption(record.offerCredentialData)
1398
        .orElse(ZIO.dieMessage(s"No offer credential data found in record: ${record.id}"))
×
1399
      offerCredentialAttachmentData <- ZIO
1✔
1400
        .fromOption(
1401
          offerCredential.attachments
1402
            .find(_.format.contains(IssueCredentialOfferFormat.Anoncred.name))
1✔
1403
            .map(_.data)
1✔
1404
            .flatMap {
1✔
1405
              case Base64(value) => Some(new String(java.util.Base64.getUrlDecoder.decode(value)))
1✔
1406
              case _             => None
×
1407
            }
1408
        )
1409
        .orElse(ZIO.dieMessage(s"No 'AnonCreds' offer credential attachment found in record: ${record.id}"))
×
1410
      credentialOffer = anoncreds.AnoncredCredentialOffer(offerCredentialAttachmentData)
1411
      requestCredential <- ZIO
1✔
1412
        .fromOption(record.requestCredentialData)
1413
        .orElse(ZIO.dieMessage(s"No request credential data found in record: ${record.id}"))
×
1414
      requestCredentialAttachmentData <- ZIO
1✔
1415
        .fromOption(
1416
          requestCredential.attachments
1417
            .find(_.format.contains(IssueCredentialRequestFormat.Anoncred.name))
1✔
1418
            .map(_.data)
1✔
1419
            .flatMap {
1✔
1420
              case Base64(value) => Some(new String(java.util.Base64.getUrlDecoder.decode(value)))
1✔
1421
              case _             => None
×
1422
            }
1423
        )
1424
        .orElse(ZIO.dieMessage(s"No 'AnonCreds' request credential attachment found in record: ${record.id}"))
×
1425
      credentialRequest = anoncreds.AnoncredCredentialRequest(requestCredentialAttachmentData)
1426
      attrValues = offerCredential.body.credential_preview.body.attributes.map { attr =>
1✔
1427
        (attr.name, attr.value)
1428
      }
1429
      credentialDefinitionSecret <- getCredentialDefinitionPrivatePart(credentialDefinition.guid)
1✔
1430
      cdp = anoncreds.AnoncredCredentialDefinitionPrivate(credentialDefinitionSecret.json.toString)
1✔
1431
      credential =
1432
        AnoncredLib.createCredential(
1✔
1433
          cd,
1434
          cdp,
1435
          credentialOffer,
1436
          credentialRequest,
1437
          attrValues
1438
        )
1439
    } yield credential
1✔
1440
  }
1441

1442
  private def getOptionsFromOfferCredentialData(record: IssueCredentialRecord): UIO[Option[Options]] = {
1✔
1443
    for {
1✔
1444
      offer <- ZIO
1✔
1445
        .fromOption(record.offerCredentialData)
1446
        .orElse(ZIO.dieMessage(s"Offer data not found in record: ${record.id}"))
×
1447
      attachmentDescriptor <- ZIO
1✔
1448
        .fromOption(offer.attachments.headOption)
1✔
1449
        .orElse(ZIO.dieMessage(s"Attachments not found in record: ${record.id}"))
×
1450
      json <- attachmentDescriptor.data match
1✔
1451
        case JsonData(json) => ZIO.succeed(json.asJson)
1✔
1452
        case _              => ZIO.dieMessage(s"Attachment doesn't contain JsonData: ${record.id}")
×
1453
      maybeOptions <- ZIO
1✔
1454
        .fromEither(json.as[PresentationAttachment].map(_.options))
1✔
1455
        .flatMapError(df => ZIO.dieMessage(df.getMessage))
×
1456
    } yield maybeOptions
1457
  }
1458

1459
  private def getJwtFromRequestCredentialData(record: IssueCredentialRecord): UIO[JWT] = {
1✔
1460
    for {
1✔
1461
      request <- ZIO
1✔
1462
        .fromOption(record.requestCredentialData)
1463
        .orElse(ZIO.dieMessage(s"Request data not found in record: ${record.id}"))
×
1464
      attachmentDescriptor <- ZIO
1✔
1465
        .fromOption(request.attachments.headOption)
1✔
1466
        .orElse(ZIO.dieMessage(s"Attachment not found in record: ${record.id}"))
×
1467
      jwt <- attachmentDescriptor.data match
1✔
1468
        case Base64(b64) =>
1✔
1469
          ZIO.succeed {
1470
            val base64Decoded = new String(java.util.Base64.getUrlDecoder.decode(b64))
1✔
1471
            JWT(base64Decoded)
1✔
1472
          }
1473
        case _ => ZIO.dieMessage(s"Attachment does not contain Base64Data: ${record.id}")
×
1474
    } yield jwt
1475
  }
1476

1477
  private def validateRequestCredentialDataProof(
1✔
1478
      maybeOptions: Option[Options],
1479
      jwt: JWT
1480
  ): IO[CredentialRequestValidationFailed, JwtPresentationPayload] = {
1481
    for {
1✔
1482
      _ <- maybeOptions match
1✔
1483
        case None => ZIO.unit
×
1484
        case Some(options) =>
1✔
1485
          JwtPresentation.validatePresentation(jwt, options.domain, options.challenge) match
1✔
1486
            case ZValidation.Success(log, value) => ZIO.unit
1✔
1487
            case ZValidation.Failure(log, error) =>
×
1488
              ZIO.fail(CredentialRequestValidationFailed("domain/challenge proof validation failed"))
×
1489

1490
      clock = java.time.Clock.system(ZoneId.systemDefault)
1✔
1491
      verificationResult <- JwtPresentation
1✔
1492
        .verify(
1493
          jwt,
1494
          JwtPresentation.PresentationVerificationOptions(
1✔
1495
            maybeProofPurpose = Some(VerificationRelationship.Authentication),
1496
            verifySignature = true,
1497
            verifyDates = false,
1498
            leeway = Duration.Zero
1499
          )
1500
        )(didResolver, uriResolver)(clock)
1✔
1501
        .mapError(errors => CredentialRequestValidationFailed(errors*))
1502

1503
      result <- verificationResult match
1✔
1504
        case ZValidation.Success(log, value) => ZIO.unit
1✔
1505
        case ZValidation.Failure(log, error) =>
×
1506
          ZIO.fail(CredentialRequestValidationFailed(s"JWT presentation verification failed: $error"))
×
1507

1508
      jwtPresentation <- ZIO
1✔
1509
        .fromTry(JwtPresentation.decodeJwt[JwtPresentationPayload](jwt))
1✔
1510
        .mapError(t => CredentialRequestValidationFailed(s"JWT presentation decoding failed: ${t.getMessage}"))
×
1511
    } yield jwtPresentation
1512
  }
1513

1514
  override def getCredentialOfferInvitation(
×
1515
      pairwiseHolderDID: DidId,
1516
      invitation: String
1517
  ): ZIO[WalletAccessContext, CredentialServiceError, OfferCredential] = {
1518
    for {
×
1519
      invitation <- ZIO
×
1520
        .fromEither(io.circe.parser.decode[Invitation](Base64Utils.decodeUrlToString(invitation)))
×
1521
        .mapError(err => InvitationParsingError(err.getMessage))
×
1522
      _ <- invitation.expires_time match {
×
1523
        case Some(expiryTime) =>
×
1524
          ZIO
×
1525
            .fail(InvitationExpired(expiryTime))
1526
            .when(Instant.now().getEpochSecond > expiryTime)
×
1527
        case None => ZIO.unit
×
1528
      }
1529
      _ <- getIssueCredentialRecordByThreadId(DidCommID(invitation.id), false)
×
1530
        .flatMap {
1531
          case None    => ZIO.unit
×
1532
          case Some(_) => ZIO.fail(InvitationAlreadyReceived(invitation.id))
×
1533
        }
1534
      credentialOffer <- ZIO.fromEither {
×
1535
        invitation.attachments
1536
          .flatMap(
×
1537
            _.headOption.map(attachment =>
×
1538
              decode[org.hyperledger.identus.mercury.model.JsonData](
×
1539
                attachment.data.asJson.noSpaces
×
1540
              ) // TODO Move mercury to use ZIO JSON
1541
                .flatMap { data =>
×
1542
                  OfferCredential.given_Decoder_OfferCredential
×
1543
                    .decodeJson(data.json.asJson)
×
1544
                    .map(r => r.copy(to = Some(pairwiseHolderDID)))
×
1545
                    .leftMap(err =>
×
1546
                      CredentialOfferDecodingError(
1547
                        s"Credential Offer As Attachment decoding error: ${err.getMessage}"
×
1548
                      )
1549
                    )
1550
                }
1551
                .leftMap(err => CredentialOfferDecodingError(s"Invitation Attachment JsonData decoding error: $err"))
×
1552
            )
1553
          )
1554
          .getOrElse(
×
1555
            Left(MissingInvitationAttachment("Missing Invitation Attachment for Credential Offer"))
×
1556
          )
1557
      }
1558
    } yield credentialOffer
1559

1560
  }
1561
}
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