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

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

16 Apr 2024 02:54AM UTC coverage: 31.165% (+0.4%) from 30.781%
8699251619

Pull #969

CryptoKnightIOG
ATL-6782: Aud check

Signed-off-by: Bassam Riman <bassam.riman@iohk.io>
Pull Request #969: feat: VC Verification Audiance check

0 of 46 new or added lines in 4 files covered. (0.0%)

486 existing lines in 113 files now uncovered.

4512 of 14478 relevant lines covered (31.16%)

0.31 hits per line

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

84.85
/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/util/DIDOperationValidator.scala
1
package io.iohk.atala.castor.core.util
2

3
import io.iohk.atala.castor.core.model.did.PublicKey
4
import io.iohk.atala.castor.core.model.did.ServiceEndpoint
5
import io.iohk.atala.castor.core.model.did.ServiceType
6
import io.iohk.atala.castor.core.model.did.{InternalKeyPurpose, InternalPublicKey, PrismDIDOperation, UpdateDIDAction}
7
import io.iohk.atala.castor.core.model.error.OperationValidationError
8
import io.iohk.atala.castor.core.util.DIDOperationValidator.Config
9
import io.iohk.atala.castor.core.util.Prelude.*
10
import scala.collection.immutable.ArraySeq
11
import zio.*
12

13
object DIDOperationValidator {
14
  final case class Config(
15
      publicKeyLimit: Int,
16
      serviceLimit: Int,
17
      maxIdSize: Int,
18
      maxServiceTypeSize: Int,
19
      maxServiceEndpointSize: Int
20
  )
21

22
  object Config {
23
    val default: Config = Config(
24
      publicKeyLimit = 50,
25
      serviceLimit = 50,
26
      maxIdSize = 50,
27
      maxServiceTypeSize = 100,
28
      maxServiceEndpointSize = 300
29
    )
30
  }
31

1✔
32
  def layer(config: Config = Config.default): ULayer[DIDOperationValidator] =
1✔
33
    ZLayer.succeed(DIDOperationValidator(config))
34
}
35

36
class DIDOperationValidator(config: Config) extends BaseOperationValidator {
1✔
37
  def validate(operation: PrismDIDOperation): Either[OperationValidationError, Unit] = {
38
    operation match {
1✔
39
      case op: PrismDIDOperation.Create     => CreateOperationValidator.validate(config)(op)
1✔
40
      case op: PrismDIDOperation.Update     => UpdateOperationValidator.validate(config)(op)
1✔
41
      case op: PrismDIDOperation.Deactivate => DeactivateOperationValidator.validate(config)(op)
42
    }
43
  }
44
}
45

46
private object CreateOperationValidator extends BaseOperationValidator {
1✔
47
  def validate(config: Config)(operation: PrismDIDOperation.Create): Either[OperationValidationError, Unit] = {
1✔
48
    for {
1✔
49
      _ <- validateMaxPublicKeysAccess(config)(operation, extractKeyIds)
1✔
50
      _ <- validateMaxServiceAccess(config)(operation, extractServiceIds)
×
51
      _ <- validateUniquePublicKeyId(operation, extractKeyIds)
1✔
UNCOV
52
      _ <- validateUniqueServiceId(operation, extractServiceIds)
×
53
      _ <- validateKeyIdIsUriFragment(operation, extractKeyIds)
1✔
54
      _ <- validateKeyIdLength(config)(operation, extractKeyIds)
1✔
55
      _ <- validateServiceIdIsUriFragment(operation, extractServiceIds)
×
UNCOV
56
      _ <- validateServiceIdLength(config)(operation, extractServiceIds)
×
57
      _ <- validateServiceEndpointLength(config)(operation, extractServiceEndpoint)
1✔
58
      _ <- validateServiceTypeLength(config)(operation, extractServiceType)
1✔
59
      _ <- validateUniqueContext(operation, _.context :: Nil)
1✔
60
      _ <- validateContextLength(operation, _.context :: Nil)
1✔
61
      _ <- validateContextIsUri(operation, _.context :: Nil)
1✔
62
      _ <- validateMasterKeyExists(operation)
63
    } yield ()
64
  }
65

1✔
66
  private def validateMasterKeyExists(operation: PrismDIDOperation.Create): Either[OperationValidationError, Unit] = {
67
    val masterKeys =
1✔
UNCOV
68
      operation.publicKeys.collect { case pk: InternalPublicKey => pk }.filter(_.purpose == InternalKeyPurpose.Master)
×
69
    if (masterKeys.nonEmpty) Right(())
1✔
70
    else Left(OperationValidationError.InvalidArgument("create operation must contain at least 1 master key"))
71
  }
72

1✔
73
  private def extractKeyIds(operation: PrismDIDOperation.Create): Seq[String] =
1✔
74
    operation.publicKeys.map {
1✔
75
      case PublicKey(id, _, _)         => id
1✔
76
      case InternalPublicKey(id, _, _) => id
77
    }
78

1✔
79
  private def extractServiceIds(operation: PrismDIDOperation.Create): Seq[String] = operation.services.map(_.id)
80

1✔
81
  private def extractServiceEndpoint(operation: PrismDIDOperation.Create): Seq[(String, ServiceEndpoint)] = {
1✔
82
    operation.services.map { s => (s.id, s.serviceEndpoint) }
83
  }
84

1✔
85
  private def extractServiceType(operation: PrismDIDOperation.Create): Seq[(String, ServiceType)] = {
1✔
86
    operation.services.map { s => (s.id, s.`type`) }
87
  }
88

89
}
90

91
private object UpdateOperationValidator extends BaseOperationValidator {
1✔
92
  def validate(config: Config)(operation: PrismDIDOperation.Update): Either[OperationValidationError, Unit] = {
1✔
93
    for {
1✔
94
      _ <- validateMaxPublicKeysAccess(config)(operation, extractKeyIds)
1✔
95
      _ <- validateMaxServiceAccess(config)(operation, extractServiceIds)
1✔
96
      _ <- validateKeyIdIsUriFragment(operation, extractKeyIds)
×
97
      _ <- validateKeyIdLength(config)(operation, extractKeyIds)
1✔
98
      _ <- validateServiceIdIsUriFragment(operation, extractServiceIds)
1✔
99
      _ <- validateServiceIdLength(config)(operation, extractServiceIds)
×
100
      _ <- validateServiceEndpointLength(config)(operation, extractServiceEndpoint)
×
101
      _ <- validateServiceTypeLength(config)(operation, extractServiceType)
×
102
      _ <- validateUniqueContext(operation, extractContexts)
1✔
103
      _ <- validateContextLength(operation, extractContexts)
×
104
      _ <- validateContextIsUri(operation, extractContexts)
1✔
105
      _ <- validatePreviousOperationHash(operation, _.previousOperationHash)
1✔
UNCOV
106
      _ <- validateNonEmptyUpdateAction(operation)
×
107
      _ <- validateUpdateServiceNonEmpty(operation)
108
    } yield ()
109
  }
110

1✔
111
  private def validateNonEmptyUpdateAction(
112
      operation: PrismDIDOperation.Update
113
  ): Either[OperationValidationError, Unit] = {
1✔
114
    val isActionNonEmpty = operation.actions.nonEmpty
1✔
115
    if (isActionNonEmpty) Right(())
×
116
    else Left(OperationValidationError.InvalidArgument("update operation must contain at least 1 update action"))
117
  }
118

1✔
119
  private def validateUpdateServiceNonEmpty(
120
      operation: PrismDIDOperation.Update
121
  ): Either[OperationValidationError, Unit] = {
1✔
122
    val isNonEmptyUpdateService = operation.actions.forall {
1✔
123
      case UpdateDIDAction.UpdateService(_, None, None) => false
1✔
124
      case _                                            => true
125
    }
1✔
126
    if (isNonEmptyUpdateService) Right(())
127
    else
1✔
128
      Left(
129
        OperationValidationError.InvalidArgument(
130
          "update operation with UpdateServiceAction must not have both 'type' and 'serviceEndpoints' empty"
131
        )
132
      )
133
  }
134

1✔
135
  private def extractKeyIds(operation: PrismDIDOperation.Update): Seq[String] = operation.actions.flatMap {
1✔
136
    case UpdateDIDAction.AddKey(publicKey)         => Some(publicKey.id)
1✔
137
    case UpdateDIDAction.AddInternalKey(publicKey) => Some(publicKey.id)
1✔
138
    case UpdateDIDAction.RemoveKey(id)             => Some(id)
1✔
139
    case _: UpdateDIDAction.AddService             => None
1✔
140
    case _: UpdateDIDAction.RemoveService          => None
1✔
141
    case _: UpdateDIDAction.UpdateService          => None
1✔
142
    case _: UpdateDIDAction.PatchContext           => None
143
  }
144

1✔
145
  private def extractServiceIds(operation: PrismDIDOperation.Update): Seq[String] = operation.actions.flatMap {
1✔
146
    case _: UpdateDIDAction.AddKey               => None
1✔
147
    case _: UpdateDIDAction.AddInternalKey       => None
1✔
148
    case _: UpdateDIDAction.RemoveKey            => None
1✔
149
    case UpdateDIDAction.AddService(service)     => Some(service.id)
1✔
150
    case UpdateDIDAction.RemoveService(id)       => Some(id)
1✔
151
    case UpdateDIDAction.UpdateService(id, _, _) => Some(id)
1✔
152
    case _: UpdateDIDAction.PatchContext         => None
153
  }
154

1✔
155
  private def extractServiceEndpoint(operation: PrismDIDOperation.Update): Seq[(String, ServiceEndpoint)] =
1✔
156
    operation.actions.collect {
1✔
157
      case UpdateDIDAction.AddService(service)                  => service.id -> service.serviceEndpoint
1✔
158
      case UpdateDIDAction.UpdateService(id, _, Some(endpoint)) => id -> endpoint
159
    }
160

1✔
161
  private def extractServiceType(operatio: PrismDIDOperation.Update): Seq[(String, ServiceType)] =
1✔
162
    operatio.actions.collect {
1✔
163
      case UpdateDIDAction.AddService(service)                     => service.id -> service.`type`
1✔
164
      case UpdateDIDAction.UpdateService(id, Some(serviceType), _) => id -> serviceType
165
    }
166

1✔
167
  private def extractContexts(operation: PrismDIDOperation.Update): Seq[Seq[String]] = {
1✔
168
    operation.actions.flatMap {
1✔
169
      case UpdateDIDAction.PatchContext(context) => Some(context)
1✔
170
      case _                                     => None
171
    }
172
  }
173
}
174

175
private object DeactivateOperationValidator extends BaseOperationValidator {
1✔
176
  def validate(config: Config)(operation: PrismDIDOperation.Deactivate): Either[OperationValidationError, Unit] =
1✔
177
    validatePreviousOperationHash(operation, _.previousOperationHash)
178
}
179

180
private trait BaseOperationValidator {
181

182
  type KeyIdExtractor[T] = T => Seq[String]
183
  type ServiceIdExtractor[T] = T => Seq[String]
184
  type ServiceTypeExtractor[T] = T => Seq[(String, ServiceType)]
185
  type ServiceEndpointExtractor[T] = T => Seq[(String, ServiceEndpoint)]
186
  type ContextExtractor[T] = T => Seq[Seq[String]]
187

1✔
188
  protected def validateMaxPublicKeysAccess[T <: PrismDIDOperation](
189
      config: Config
190
  )(operation: T, keyIdExtractor: KeyIdExtractor[T]): Either[OperationValidationError, Unit] = {
1✔
191
    val keyCount = keyIdExtractor(operation).length
1✔
192
    if (keyCount <= config.publicKeyLimit) Right(())
×
193
    else Left(OperationValidationError.TooManyDidPublicKeyAccess(config.publicKeyLimit, Some(keyCount)))
194
  }
195

1✔
196
  protected def validateMaxServiceAccess[T <: PrismDIDOperation](
197
      config: Config
198
  )(operation: T, serviceIdExtractor: ServiceIdExtractor[T]): Either[OperationValidationError, Unit] = {
1✔
199
    val serviceCount = serviceIdExtractor(operation).length
×
200
    if (serviceCount <= config.serviceLimit) Right(())
1✔
201
    else Left(OperationValidationError.TooManyDidServiceAccess(config.serviceLimit, Some(serviceCount)))
202
  }
203

1✔
204
  protected def validateUniquePublicKeyId[T <: PrismDIDOperation](
205
      operation: T,
206
      keyIdExtractor: KeyIdExtractor[T]
207
  ): Either[OperationValidationError, Unit] = {
1✔
208
    val ids = keyIdExtractor(operation)
×
209
    if (ids.isUnique) Right(())
1✔
210
    else Left(OperationValidationError.InvalidArgument("id for public-keys is not unique"))
211
  }
212

1✔
213
  protected def validateUniqueServiceId[T <: PrismDIDOperation](
214
      operation: T,
215
      serviceIdExtractor: ServiceIdExtractor[T]
216
  ): Either[OperationValidationError, Unit] = {
1✔
217
    val ids = serviceIdExtractor(operation)
1✔
218
    if (ids.isUnique) Right(())
1✔
219
    else Left(OperationValidationError.InvalidArgument("id for services is not unique"))
220
  }
221

1✔
222
  protected def validateUniqueContext[T <: PrismDIDOperation](
223
      operation: T,
224
      contextExtractor: ContextExtractor[T]
225
  ): Either[OperationValidationError, Unit] = {
1✔
226
    val contexts = contextExtractor(operation)
1✔
227
    val nonUniqueContextList = contexts.filterNot(_.isUnique)
1✔
228
    if (nonUniqueContextList.isEmpty) Right(())
×
229
    else Left(OperationValidationError.InvalidArgument("context is not unique"))
230
  }
231

1✔
232
  protected def validateContextIsUri[T <: PrismDIDOperation](
233
      operation: T,
234
      contextExtractor: ContextExtractor[T]
235
  ): Either[OperationValidationError, Unit] = {
1✔
236
    val contexts = contextExtractor(operation)
1✔
237
    val nonUriContexts = contexts.flatten.filterNot(UriUtils.isValidUriString)
1✔
238
    if (nonUriContexts.isEmpty) Right(())
239
    else
×
240
      Left(
241
        OperationValidationError.InvalidArgument(
1✔
242
          s"context is not a valid URI: ${nonUriContexts.mkString("[", ", ", "]")}"
243
        )
244
      )
245
  }
246

1✔
247
  protected def validateContextLength[T <: PrismDIDOperation](
248
      operation: T,
249
      contextExtractor: ContextExtractor[T]
250
  ): Either[OperationValidationError, Unit] = {
1✔
251
    val contexts = contextExtractor(operation)
1✔
252
    val invalidContexts = contexts.flatten.filter(_.length > 100) // FIXME: confirm this value  with the spec
1✔
253
    if (invalidContexts.isEmpty) Right(())
254
    else
×
255
      Left(
1✔
256
        OperationValidationError.InvalidArgument(s"context is too long: ${invalidContexts.mkString("[", ", ", "]")}")
257
      )
258
  }
259

1✔
260
  protected def validateKeyIdIsUriFragment[T <: PrismDIDOperation](
261
      operation: T,
262
      keyIdExtractor: KeyIdExtractor[T]
263
  ): Either[OperationValidationError, Unit] = {
1✔
264
    val ids = keyIdExtractor(operation)
1✔
265
    val invalidIds = ids.filterNot(UriUtils.isValidUriFragment)
1✔
266
    if (invalidIds.isEmpty) Right(())
267
    else
1✔
268
      Left(
1✔
269
        OperationValidationError.InvalidArgument(s"public key id is invalid: ${invalidIds.mkString("[", ", ", "]")}")
270
      )
271
  }
272

1✔
273
  protected def validateServiceIdIsUriFragment[T <: PrismDIDOperation](
274
      operation: T,
275
      serviceIdExtractor: ServiceIdExtractor[T]
276
  ): Either[OperationValidationError, Unit] = {
1✔
277
    val ids = serviceIdExtractor(operation)
1✔
UNCOV
278
    val invalidIds = ids.filterNot(UriUtils.isValidUriFragment)
×
279
    if (invalidIds.isEmpty) Right(())
280
    else
1✔
281
      Left(OperationValidationError.InvalidArgument(s"service id is invalid: ${invalidIds.mkString("[", ", ", "]")}"))
282
  }
283

1✔
284
  protected def validateKeyIdLength[T <: PrismDIDOperation](
285
      config: Config
286
  )(operation: T, keyIdExtractor: KeyIdExtractor[T]): Either[OperationValidationError, Unit] = {
1✔
287
    val ids = keyIdExtractor(operation)
1✔
288
    val invalidIds = ids.filter(_.length > config.maxIdSize)
1✔
289
    if (invalidIds.isEmpty) Right(())
290
    else
×
291
      Left(
292
        OperationValidationError.InvalidArgument(
1✔
293
          s"public key id is too long: ${invalidIds.mkString("[", ", ", "]")}"
294
        )
295
      )
296
  }
297

1✔
298
  protected def validateServiceIdLength[T <: PrismDIDOperation](
299
      config: Config
300
  )(operation: T, serviceIdExtractor: ServiceIdExtractor[T]): Either[OperationValidationError, Unit] = {
1✔
301
    val ids = serviceIdExtractor(operation)
1✔
302
    val invalidIds = ids.filter(_.length > config.maxIdSize)
1✔
303
    if (invalidIds.isEmpty) Right(())
304
    else
×
305
      Left(
306
        OperationValidationError.InvalidArgument(
1✔
307
          s"service id is too long: ${invalidIds.mkString("[", ", ", "]")}"
308
        )
309
      )
310
  }
311

1✔
312
  protected def validateServiceTypeLength[T <: PrismDIDOperation](
313
      config: Config
314
  )(operation: T, serviceTypeExtractor: ServiceTypeExtractor[T]): Either[OperationValidationError, Unit] = {
315
    import io.iohk.atala.castor.core.model.ProtoModelHelper.*
1✔
316
    val serviceTypes = serviceTypeExtractor(operation)
1✔
317
    val invalidServiceTypes = serviceTypes.filter(_._2.toProto.length > config.maxServiceTypeSize)
1✔
318
    if (invalidServiceTypes.isEmpty) Right(())
319
    else
1✔
320
      Left(
321
        OperationValidationError.InvalidArgument(
1✔
322
          s"service type is too long: ${invalidServiceTypes.map(_._1).mkString("[", ", ", "]")}"
323
        )
324
      )
325
  }
326

1✔
327
  protected def validateServiceEndpointLength[T <: PrismDIDOperation](
328
      config: Config
329
  )(operation: T, serviceEndpointExtractor: ServiceEndpointExtractor[T]): Either[OperationValidationError, Unit] = {
330
    import io.iohk.atala.castor.core.model.ProtoModelHelper.*
1✔
331
    val serviceEndpoints = serviceEndpointExtractor(operation)
1✔
UNCOV
332
    val invalidServiceEndpoints = serviceEndpoints.filter(_._2.toProto.length > config.maxServiceEndpointSize)
×
333
    if (invalidServiceEndpoints.isEmpty) Right(())
334
    else
1✔
335
      Left(
336
        OperationValidationError.InvalidArgument(
1✔
337
          s"service endpoint is too long: ${invalidServiceEndpoints.map(_._1).mkString("[", ", ", "]")}"
338
        )
339
      )
340
  }
341

1✔
342
  protected def validatePreviousOperationHash[T <: PrismDIDOperation](
343
      operation: T,
344
      previousOperationHashExtractor: T => ArraySeq[Byte]
345
  ): Either[OperationValidationError, Unit] = {
1✔
346
    val previousOperationHash = previousOperationHashExtractor(operation)
1✔
347
    if (previousOperationHash.length == 32) Right(())
×
348
    else Left(OperationValidationError.InvalidArgument(s"previousOperationHash must have a size of 32 bytes"))
349
  }
350

351
  /** @return true if a given uri is normalized */
×
352
  protected def isUriNormalized(uri: String): Boolean = {
×
353
    UriUtils.normalizeUri(uri).contains(uri)
354
  }
355

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