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

hyperledger / identus-cloud-agent / 11572175670

29 Oct 2024 10:41AM UTC coverage: 48.422% (-0.02%) from 48.438%
11572175670

Pull #1420

mineme0110
fix:key id for jwt and sdjwt

Signed-off-by: mineme0110 <shailesh.patil@iohk.io>
Pull Request #1420: fix: key id for jwt and sdjwt

13 of 36 new or added lines in 2 files covered. (36.11%)

13 existing lines in 7 files now uncovered.

8022 of 16567 relevant lines covered (48.42%)

0.48 hits per line

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

70.59
/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.shared.crypto.{Ed25519KeyPair, Secp256k1KeyPair}
30
import org.hyperledger.identus.shared.http.UriResolver
31
import org.hyperledger.identus.shared.messaging.{Producer, WalletIdAndRecordId}
32
import org.hyperledger.identus.shared.models.*
33
import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect
34
import org.hyperledger.identus.shared.utils.Base64Utils
35
import zio.*
36
import zio.json.*
37
import zio.prelude.ZValidation
38

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

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

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

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

96
  import CredentialServiceImpl.*
97
  import IssueCredentialRecord.*
98

99
  private val TOPIC_NAME = "issue"
100

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1168
  override def generateJWTCredential(
1✔
1169
      recordId: DidCommID,
1170
      statusListRegistryUrl: String,
1171
  ): ZIO[WalletAccessContext, RecordNotFound | CredentialRequestValidationFailed, IssueCredentialRecord] = {
1172
    for {
1✔
1173
      _ <- ZIO.log(s"!!! *****generateJWTCredential*****JWT********** Handling recordId via Kafka queue $recordId")
1✔
1174

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

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

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

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

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

1332
  }
1333

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

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

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

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

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

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

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

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

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

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

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