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

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

17 Apr 2024 01:02PM UTC coverage: 31.005% (-0.6%) from 31.633%
8722405814

Pull #966

patlo-iog
chore: resolve conflict

Signed-off-by: Pat Losoponkul <pat.losoponkul@iohk.io>
Pull Request #966: feat: key management for Ed25519 and X25519

109 of 386 new or added lines in 22 files covered. (28.24%)

386 existing lines in 101 files now uncovered.

4478 of 14443 relevant lines covered (31.0%)

0.31 hits per line

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

60.82
/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/ProtoModelHelper.scala
1
package io.iohk.atala.castor.core.model
2

3
import com.google.protobuf.ByteString
4
import io.circe.Json
5
import io.iohk.atala.castor.core.model.did.ServiceEndpoint.UriOrJsonEndpoint
6
import io.iohk.atala.castor.core.model.did.{
7
  DIDData,
8
  EllipticCurve,
9
  InternalKeyPurpose,
10
  InternalPublicKey,
11
  PrismDID,
12
  PrismDIDOperation,
13
  PublicKey,
14
  PublicKeyData,
15
  ScheduledDIDOperationDetail,
16
  ScheduledDIDOperationStatus,
17
  Service,
18
  ServiceEndpoint,
19
  ServiceType,
20
  SignedPrismDIDOperation,
21
  UpdateDIDAction,
22
  VerificationRelationship
23
}
24
import io.iohk.atala.prism.protos.common_models.OperationStatus
25
import io.iohk.atala.prism.protos.node_models.KeyUsage
26
import io.iohk.atala.prism.protos.node_models.PublicKey.KeyData
27
import io.iohk.atala.prism.protos.{common_models, node_api, node_models}
28
import io.iohk.atala.shared.models.Base64UrlString
29
import io.iohk.atala.shared.utils.Traverse.*
30
import java.time.Instant
31
import scala.language.implicitConversions
32
import zio.*
33

34
object ProtoModelHelper extends ProtoModelHelper
35

36
private[castor] trait ProtoModelHelper {
37

38
  extension (bytes: Array[Byte]) {
1✔
39
    def toProto: ByteString = ByteString.copyFrom(bytes)
40
  }
41

42
  extension (signedOperation: SignedPrismDIDOperation) {
1✔
43
    def toProto: node_models.SignedAtalaOperation =
1✔
44
      node_models.SignedAtalaOperation(
45
        signedWith = signedOperation.signedWithKey,
1✔
46
        signature = signedOperation.signature.toArray.toProto,
1✔
47
        operation = Some(signedOperation.operation.toAtalaOperation)
48
      )
49
  }
50

51
  extension (operation: PrismDIDOperation.Create) {
1✔
52
    def toProto: node_models.AtalaOperation.Operation.CreateDid = {
53
      node_models.AtalaOperation.Operation.CreateDid(
1✔
54
        value = node_models.CreateDIDOperation(
55
          didData = Some(
1✔
56
            node_models.CreateDIDOperation.DIDCreationData(
1✔
57
              publicKeys = operation.publicKeys.map {
1✔
58
                case pk: PublicKey         => pk.toProto
1✔
59
                case pk: InternalPublicKey => pk.toProto
UNCOV
60
              },
×
61
              services = operation.services.map(_.toProto),
62
              context = operation.context
63
            )
64
          )
65
        )
66
      )
67
    }
68
  }
69

70
  extension (operation: PrismDIDOperation.Deactivate) {
1✔
71
    def toProto: node_models.AtalaOperation.Operation.DeactivateDid = {
72
      node_models.AtalaOperation.Operation.DeactivateDid(
1✔
73
        value = node_models.DeactivateDIDOperation(
1✔
74
          previousOperationHash = operation.previousOperationHash.toArray.toProto,
75
          id = operation.did.suffix.toString
76
        )
77
      )
78
    }
79
  }
80

81
  extension (operation: PrismDIDOperation.Update) {
1✔
82
    def toProto: node_models.AtalaOperation.Operation.UpdateDid = {
83
      node_models.AtalaOperation.Operation.UpdateDid(
1✔
84
        value = node_models.UpdateDIDOperation(
1✔
85
          previousOperationHash = operation.previousOperationHash.toArray.toProto,
86
          id = operation.did.suffix.toString,
1✔
87
          actions = operation.actions.map(_.toProto)
88
        )
89
      )
90
    }
91
  }
92

93
  extension (action: UpdateDIDAction) {
1✔
94
    def toProto: node_models.UpdateDIDAction = {
95
      val a = action match {
1✔
96
        case UpdateDIDAction.AddKey(publicKey) =>
1✔
97
          node_models.UpdateDIDAction.Action.AddKey(node_models.AddKeyAction(Some(publicKey.toProto)))
×
98
        case UpdateDIDAction.AddInternalKey(publicKey) =>
×
99
          node_models.UpdateDIDAction.Action.AddKey(node_models.AddKeyAction(Some(publicKey.toProto)))
1✔
100
        case UpdateDIDAction.RemoveKey(id) =>
1✔
101
          node_models.UpdateDIDAction.Action.RemoveKey(node_models.RemoveKeyAction(id))
×
102
        case UpdateDIDAction.AddService(service) =>
×
103
          node_models.UpdateDIDAction.Action.AddService(node_models.AddServiceAction(Some(service.toProto)))
×
104
        case UpdateDIDAction.RemoveService(id) =>
×
105
          node_models.UpdateDIDAction.Action.RemoveService(node_models.RemoveServiceAction(id))
×
106
        case UpdateDIDAction.UpdateService(serviceId, serviceType, endpoint) =>
107
          node_models.UpdateDIDAction.Action.UpdateService(
×
108
            node_models.UpdateServiceAction(
109
              serviceId = serviceId,
×
110
              `type` = serviceType.fold("")(_.toProto),
×
111
              serviceEndpoints = endpoint.fold("")(_.toProto)
112
            )
113
          )
×
114
        case UpdateDIDAction.PatchContext(context) =>
×
115
          node_models.UpdateDIDAction.Action.PatchContext(node_models.PatchContextAction(context))
116
      }
1✔
117
      node_models.UpdateDIDAction(action = a)
118
    }
119
  }
120

121
  extension (publicKey: PublicKey) {
1✔
122
    def toProto: node_models.PublicKey = {
1✔
123
      node_models.PublicKey(
124
        id = publicKey.id,
125
        usage = publicKey.purpose match {
1✔
126
          case VerificationRelationship.Authentication       => node_models.KeyUsage.AUTHENTICATION_KEY
1✔
127
          case VerificationRelationship.AssertionMethod      => node_models.KeyUsage.ISSUING_KEY
1✔
128
          case VerificationRelationship.KeyAgreement         => node_models.KeyUsage.KEY_AGREEMENT_KEY
1✔
129
          case VerificationRelationship.CapabilityInvocation => node_models.KeyUsage.CAPABILITY_INVOCATION_KEY
1✔
130
          case VerificationRelationship.CapabilityDelegation => node_models.KeyUsage.CAPABILITY_DELEGATION_KEY
131
        },
132
        addedOn = None,
133
        revokedOn = None,
1✔
134
        keyData = publicKey.publicKeyData.toProto
135
      )
136
    }
137
  }
138

139
  extension (internalPublicKey: InternalPublicKey) {
1✔
140
    def toProto: node_models.PublicKey = {
1✔
141
      node_models.PublicKey(
142
        id = internalPublicKey.id,
143
        usage = internalPublicKey.purpose match {
1✔
144
          case InternalKeyPurpose.Master     => node_models.KeyUsage.MASTER_KEY
1✔
145
          case InternalKeyPurpose.Revocation => node_models.KeyUsage.REVOCATION_KEY
146
        },
147
        addedOn = None,
148
        revokedOn = None,
1✔
149
        keyData = internalPublicKey.publicKeyData.toProto
150
      )
151
    }
152
  }
153

154
  extension (publicKeyData: PublicKeyData) {
1✔
155
    def toProto: node_models.PublicKey.KeyData = {
156
      publicKeyData match {
1✔
157
        case PublicKeyData.ECKeyData(crv, x, y) =>
158
          node_models.PublicKey.KeyData.EcKeyData(
1✔
159
            value = node_models.ECKeyData(
160
              curve = crv.name,
1✔
161
              x = x.toByteArray.toProto,
1✔
162
              y = y.toByteArray.toProto
163
            )
164
          )
1✔
165
        case PublicKeyData.ECCompressedKeyData(crv, data) =>
166
          node_models.PublicKey.KeyData.CompressedEcKeyData(
1✔
167
            value = node_models.CompressedECKeyData(
168
              curve = crv.name,
1✔
169
              data = data.toByteArray.toProto
170
            )
171
          )
172
      }
173
    }
174
  }
175

176
  extension (service: Service) {
1✔
177
    def toProto: node_models.Service = {
1✔
178
      node_models.Service(
179
        id = service.id,
1✔
180
        `type` = service.`type`.toProto,
1✔
181
        serviceEndpoint = service.serviceEndpoint.toProto,
182
        addedOn = None,
183
        deletedOn = None
184
      )
185
    }
186
  }
187

188
  extension (serviceType: ServiceType) {
1✔
189
    def toProto: String = {
190
      serviceType match {
1✔
191
        case ServiceType.Single(name) => name.value
1✔
192
        case ts: ServiceType.Multiple =>
1✔
193
          val names = ts.values.map(_.value).map(Json.fromString)
1✔
194
          Json.arr(names: _*).noSpaces
195
      }
196
    }
197
  }
198

199
  extension (serviceEndpoint: ServiceEndpoint) {
1✔
200
    def toProto: String = {
201
      serviceEndpoint match {
1✔
202
        case ServiceEndpoint.Single(value) =>
203
          value match {
1✔
204
            case UriOrJsonEndpoint.Uri(uri)   => uri.value
1✔
205
            case UriOrJsonEndpoint.Json(json) => Json.fromJsonObject(json).noSpaces
206
          }
1✔
207
        case endpoints: ServiceEndpoint.Multiple =>
×
208
          val uris = endpoints.values.map {
1✔
209
            case UriOrJsonEndpoint.Uri(uri)   => Json.fromString(uri.value)
1✔
210
            case UriOrJsonEndpoint.Json(json) => Json.fromJsonObject(json)
211
          }
1✔
212
          Json.arr(uris: _*).noSpaces
213
      }
214
    }
215
  }
216

217
  extension (resp: node_api.GetOperationInfoResponse) {
×
218
    def toDomain: Either[String, Option[ScheduledDIDOperationDetail]] = {
219
      val status = resp.operationStatus match {
×
220
        case OperationStatus.UNKNOWN_OPERATION      => Right(None)
×
221
        case OperationStatus.PENDING_SUBMISSION     => Right(Some(ScheduledDIDOperationStatus.Pending))
×
222
        case OperationStatus.AWAIT_CONFIRMATION     => Right(Some(ScheduledDIDOperationStatus.AwaitingConfirmation))
×
223
        case OperationStatus.CONFIRMED_AND_APPLIED  => Right(Some(ScheduledDIDOperationStatus.Confirmed))
×
224
        case OperationStatus.CONFIRMED_AND_REJECTED => Right(Some(ScheduledDIDOperationStatus.Rejected))
×
225
        case OperationStatus.Unrecognized(unrecognizedValue) =>
×
226
          Left(s"unrecognized status of GetOperationInfoResponse: $unrecognizedValue")
227
      }
×
228
      status.map(s => s.map(ScheduledDIDOperationDetail.apply))
229
    }
230
  }
231

232
  extension (didData: node_models.DIDData) {
1✔
233
    def toDomain: Either[String, DIDData] = {
1✔
234
      for {
×
235
        canonicalDID <- PrismDID.buildCanonicalFromSuffix(didData.id)
×
236
        allKeys <- didData.publicKeys.traverse(_.toDomain)
1✔
237
        services <- didData.services.traverse(_.toDomain)
238
      } yield DIDData(
239
        id = canonicalDID,
×
240
        publicKeys = allKeys.collect { case key: PublicKey => key },
1✔
241
        internalKeys = allKeys.collect { case key: InternalPublicKey => key },
242
        services = services,
243
        context = didData.context
244
      )
245
    }
246

247
    /** Return DIDData with keys and services removed by checking revocation time against the current time */
1✔
UNCOV
248
    def filterRevokedKeysAndServices: UIO[node_models.DIDData] = {
×
249
      Clock.instant.map { now =>
250
        didData
1✔
251
          .withPublicKeys(didData.publicKeys.filter { publicKey =>
1✔
252
            publicKey.revokedOn.flatMap(_.toInstant).forall(revokeTime => revokeTime isAfter now)
253
          })
1✔
254
          .withServices(didData.services.filter { service =>
1✔
255
            service.deletedOn.flatMap(_.toInstant).forall(revokeTime => revokeTime isAfter now)
256
          })
257
      }
258
    }
259
  }
260

261
  extension (ledgerData: node_models.LedgerData) {
1✔
262
    def toInstant: Option[Instant] = ledgerData.timestampInfo
×
263
      .flatMap(_.blockTimestamp)
1✔
264
      .map(ts => Instant.ofEpochSecond(ts.seconds).plusNanos(ts.nanos))
265
  }
266

267
  extension (operation: node_models.CreateDIDOperation) {
1✔
268
    def toDomain: Either[String, PrismDIDOperation.Create] = {
1✔
269
      for {
×
270
        allKeys <- operation.didData.map(_.publicKeys.traverse(_.toDomain)).getOrElse(Right(Nil))
×
271
        services <- operation.didData.map(_.services.traverse(_.toDomain)).getOrElse(Right(Nil))
×
272
        context = operation.didData.map(_.context).getOrElse(Nil)
273
      } yield PrismDIDOperation.Create(
274
        publicKeys = allKeys,
275
        services = services,
276
        context = context
277
      )
278
    }
279
  }
280

281
  extension (service: node_models.Service) {
1✔
282
    def toDomain: Either[String, Service] = {
1✔
283
      for {
×
UNCOV
284
        serviceType <- parseServiceType(service.`type`)
×
285
        serviceEndpoint <- parseServiceEndpoint(service.serviceEndpoint)
286
      } yield Service(
287
        id = service.id,
288
        `type` = serviceType,
289
        serviceEndpoint = serviceEndpoint
290
      )
291
    }
292
  }
293

294
  extension (publicKey: node_models.PublicKey) {
1✔
295
    def toDomain: Either[String, PublicKey | InternalPublicKey] = {
296
      val purpose: Either[String, VerificationRelationship | InternalKeyPurpose] = publicKey.usage match {
×
297
        case node_models.KeyUsage.UNKNOWN_KEY => Left(s"unsupported use of KeyUsage.UNKNOWN_KEY on key ${publicKey.id}")
×
298
        case node_models.KeyUsage.MASTER_KEY  => Right(InternalKeyPurpose.Master)
×
299
        case node_models.KeyUsage.ISSUING_KEY => Right(VerificationRelationship.AssertionMethod)
1✔
300
        case node_models.KeyUsage.KEY_AGREEMENT_KEY         => Right(VerificationRelationship.KeyAgreement)
1✔
301
        case node_models.KeyUsage.AUTHENTICATION_KEY        => Right(VerificationRelationship.Authentication)
1✔
302
        case node_models.KeyUsage.CAPABILITY_INVOCATION_KEY => Right(VerificationRelationship.CapabilityInvocation)
×
303
        case node_models.KeyUsage.CAPABILITY_DELEGATION_KEY => Right(VerificationRelationship.CapabilityDelegation)
×
304
        case node_models.KeyUsage.REVOCATION_KEY            => Right(InternalKeyPurpose.Revocation)
×
305
        case node_models.KeyUsage.Unrecognized(unrecognizedValue) =>
×
306
          Left(s"unrecognized KeyUsage: $unrecognizedValue on key ${publicKey.id}")
307
      }
308

1✔
309
      for {
310
        purpose <- purpose
1✔
311
        keyData <- publicKey.keyData.toDomain
312
      } yield purpose match {
1✔
313
        case purpose: VerificationRelationship =>
314
          PublicKey(
315
            id = publicKey.id,
316
            purpose = purpose,
317
            publicKeyData = keyData
318
          )
1✔
319
        case purpose: InternalKeyPurpose =>
320
          InternalPublicKey(
321
            id = publicKey.id,
322
            purpose = purpose,
323
            publicKeyData = keyData
324
          )
325
      }
326
    }
327
  }
328

329
  extension (publicKeyData: node_models.PublicKey.KeyData) {
1✔
330
    def toDomain: Either[String, PublicKeyData] = {
331
      publicKeyData match {
×
332
        case KeyData.Empty => Left(s"unable to convert KeyData.Emtpy to PublicKeyData")
1✔
333
        case KeyData.EcKeyData(ecKeyData) =>
1✔
334
          for {
335
            curve <- EllipticCurve
×
336
              .parseString(ecKeyData.curve)
×
337
              .toRight(s"unsupported elliptic curve ${ecKeyData.curve}")
338
          } yield PublicKeyData.ECKeyData(
339
            crv = curve,
1✔
340
            x = Base64UrlString.fromByteArray(ecKeyData.x.toByteArray),
1✔
341
            y = Base64UrlString.fromByteArray(ecKeyData.y.toByteArray)
342
          )
1✔
343
        case KeyData.CompressedEcKeyData(ecKeyData) =>
1✔
344
          for {
345
            curve <- EllipticCurve
×
346
              .parseString(ecKeyData.curve)
×
347
              .toRight(s"unsupported elliptic curve ${ecKeyData.curve}")
348
          } yield PublicKeyData.ECCompressedKeyData(
349
            crv = curve,
1✔
350
            data = Base64UrlString.fromByteArray(ecKeyData.data.toByteArray)
351
          )
352
      }
353
    }
354
  }
355

1✔
356
  def parseServiceType(s: String): Either[String, ServiceType] = {
357
    // The type field MUST be a string or a non-empty JSON array of strings.
358
    val parsedJson: Option[Either[String, ServiceType.Multiple]] = io.circe.parser
×
359
      .parse(s)
×
360
      .toOption // it's OK to let parsing fail (e.g. LinkedDomains without quote is not a JSON string)
×
361
      .flatMap(_.asArray)
1✔
362
      .map { jsonArr =>
×
363
        jsonArr
×
364
          .traverse(_.asString.toRight("the service type is not a JSON array of strings"))
×
365
          .flatMap(_.traverse(ServiceType.Name.fromString))
×
366
          .map(_.toList)
×
367
          .flatMap {
×
368
            case head :: tail => Right(ServiceType.Multiple(head, tail))
×
369
            case Nil          => Left("the service type cannot be an empty JSON array")
370
          }
1✔
371
          .filterOrElse(
1✔
372
            _ => s == io.circe.Json.arr(jsonArr: _*).noSpaces,
373
            "the service type is a valid JSON array of strings, but not conform to the ABNF"
374
          )
375
      }
376

377
    parsedJson match {
UNCOV
378
      // serviceType is a valid JSON array of strings
×
379
      case Some(Right(parsed)) => Right(parsed)
380
      // serviceType is a valid JSON array but contains invalid items
×
381
      case Some(Left(error)) => Left(error)
382
      // serviceType is a string (raw string, not JSON quoted string)
1✔
383
      case None => ServiceType.Name.fromString(s).map(name => ServiceType.Single(name))
384
    }
385
  }
386

1✔
387
  def parseServiceEndpoint(s: String): Either[String, ServiceEndpoint] = {
388
    /* The service_endpoint field MUST contain one of:
389
     * 1. a URI
390
     * 2. a JSON object
391
     * 3. a non-empty JSON array of URIs and/or JSON objects
392
     */
393
    val parsedJson: Option[Either[String, ServiceEndpoint]] = io.circe.parser
×
394
      .parse(s)
×
395
      .toOption // it's OK to let parsing fail (e.g. http://example.com without quote is not a JSON string)
1✔
396
      .flatMap { json =>
1✔
397
        val parsedObject = json.asObject.map(obj => Right(ServiceEndpoint.Single(obj)))
×
398
        val parsedArray = json.asArray.map(_.traverse[String, UriOrJsonEndpoint] { js =>
×
399
          val obj = js.asObject.map(obj => Right(obj: UriOrJsonEndpoint))
×
400
          val str = js.asString.map(str => ServiceEndpoint.UriValue.fromString(str).map[UriOrJsonEndpoint](i => i))
×
401
          obj.orElse(str).getOrElse(Left("the service endpoint is not a JSON array of URIs and/or JSON objects"))
1✔
UNCOV
402
        }.map(_.toList).flatMap {
×
403
          case head :: tail => Right(ServiceEndpoint.Multiple(head, tail))
×
404
          case Nil          => Left("the service endpoint cannot be an empty JSON array")
405
        })
406

1✔
407
        parsedObject.orElse(parsedArray)
408
      }
409

410
    parsedJson match {
411
      // serviceEndpoint is a valid JSON object or array
×
412
      case Some(Right(parsed)) => Right(parsed)
UNCOV
413
      // serviceEndpoint is a valid JSON but contains invalid values
×
414
      case Some(Left(error)) => Left(error)
415
      // serviceEndpoint is a string (raw string, not JSON quoted string)
1✔
416
      case None => ServiceEndpoint.UriValue.fromString(s).map(ServiceEndpoint.Single(_))
417
    }
418
  }
419

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