From a20aca2fb3185bb2bca209b8d857c4a6079467ae Mon Sep 17 00:00:00 2001 From: Pat Losoponkul Date: Tue, 31 Jan 2023 19:17:49 +0700 Subject: [PATCH] feat(castor): add operation validation when resolving unpublished DID --- .../castor/core/model/did/w3c/package.scala | 1 + .../castor/core/model/error/package.scala | 13 +++- .../castor/core/service/DIDService.scala | 8 ++- .../core/util/DIDOperationValidator.scala | 68 ++++++++++--------- .../core/util/DIDOperationValidatorSpec.scala | 12 ++-- 5 files changed, 61 insertions(+), 41 deletions(-) diff --git a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/w3c/package.scala b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/w3c/package.scala index 24e12954d1..a54aa2f8f4 100644 --- a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/w3c/package.scala +++ b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/w3c/package.scala @@ -24,6 +24,7 @@ package object w3c { { case DIDResolutionError.DLTProxyError(_) => DIDResolutionErrorRepr.InternalError case DIDResolutionError.UnexpectedDLTResult(_) => DIDResolutionErrorRepr.InternalError + case DIDResolutionError.ValidationError(_) => DIDResolutionErrorRepr.InvalidDID }, _.toRight(DIDResolutionErrorRepr.NotFound) ) diff --git a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/error/package.scala b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/error/package.scala index 43221db801..bdf1ddfb64 100644 --- a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/error/package.scala +++ b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/error/package.scala @@ -6,9 +6,7 @@ package object error { object DIDOperationError { final case class DLTProxyError(cause: Throwable) extends DIDOperationError final case class UnexpectedDLTResult(msg: String) extends DIDOperationError - final case class TooManyDidPublicKeyAccess(limit: Int, access: Option[Int]) extends DIDOperationError - final case class TooManyDidServiceAccess(limit: Int, access: Option[Int]) extends DIDOperationError - final case class InvalidArgument(msg: String) extends DIDOperationError + final case class ValidationError(cause: OperationValidationError) extends DIDOperationError } sealed trait DIDResolutionError @@ -16,6 +14,15 @@ package object error { object DIDResolutionError { final case class DLTProxyError(cause: Throwable) extends DIDResolutionError final case class UnexpectedDLTResult(msg: String) extends DIDResolutionError + final case class ValidationError(cause: OperationValidationError) extends DIDResolutionError + } + + sealed trait OperationValidationError + + object OperationValidationError { + final case class TooManyDidPublicKeyAccess(limit: Int, access: Option[Int]) extends OperationValidationError + final case class TooManyDidServiceAccess(limit: Int, access: Option[Int]) extends OperationValidationError + final case class InvalidArgument(msg: String) extends OperationValidationError } } diff --git a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/service/DIDService.scala b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/service/DIDService.scala index 3a97ffa4c3..31bbaa3587 100644 --- a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/service/DIDService.scala +++ b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/service/DIDService.scala @@ -48,7 +48,9 @@ private class DIDServiceImpl(didOpValidator: DIDOperationValidator, nodeClient: signedOperations = Seq(signedOperation.toProto) ) for { - _ <- ZIO.fromEither(didOpValidator.validate(signedOperation.operation)) + _ <- ZIO + .fromEither(didOpValidator.validate(signedOperation.operation)) + .mapError(DIDOperationError.ValidationError.apply) operationOutput <- ZIO .fromFuture(_ => nodeClient.scheduleOperations(operationRequest)) .mapBoth(DIDOperationError.DLTProxyError.apply, _.outputs.toList) @@ -110,6 +112,10 @@ private class DIDServiceImpl(didOpValidator: DIDOperationValidator, nodeClient: val request = node_api.GetDidDocumentRequest(did = canonicalDID.toString) for { + // unpublished CreateOperation (if exists) must be validated before the resolution + _ <- createOperation + .fold(ZIO.unit)(op => ZIO.fromEither(didOpValidator.validate(op))) + .mapError(DIDResolutionError.ValidationError.apply) result <- ZIO .fromFuture(_ => nodeClient.getDidDocument(request)) .mapError(DIDResolutionError.DLTProxyError.apply) diff --git a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/util/DIDOperationValidator.scala b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/util/DIDOperationValidator.scala index 418e7a5af0..c70918efc0 100644 --- a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/util/DIDOperationValidator.scala +++ b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/util/DIDOperationValidator.scala @@ -7,7 +7,7 @@ import io.iohk.atala.castor.core.model.did.{ SignedPrismDIDOperation, UpdateDIDAction } -import io.iohk.atala.castor.core.model.error.DIDOperationError +import io.iohk.atala.castor.core.model.error.OperationValidationError import io.iohk.atala.castor.core.util.DIDOperationValidator.Config import io.iohk.atala.castor.core.util.Prelude.* import zio.* @@ -27,7 +27,7 @@ object DIDOperationValidator { } class DIDOperationValidator(config: Config) extends BaseOperationValidator { - def validate(operation: PrismDIDOperation): Either[DIDOperationError, Unit] = { + def validate(operation: PrismDIDOperation): Either[OperationValidationError, Unit] = { operation match { case op: PrismDIDOperation.Create => CreateOperationValidator.validate(config)(op) case op: PrismDIDOperation.Update => UpdateOperationValidator.validate(config)(op) @@ -37,7 +37,7 @@ class DIDOperationValidator(config: Config) extends BaseOperationValidator { } private object CreateOperationValidator extends BaseOperationValidator { - def validate(config: Config)(operation: PrismDIDOperation.Create): Either[DIDOperationError, Unit] = { + def validate(config: Config)(operation: PrismDIDOperation.Create): Either[OperationValidationError, Unit] = { for { _ <- validateMaxPublicKeysAccess(config)(operation, extractKeyIds) _ <- validateMaxServiceAccess(config)(operation, extractServiceIds) @@ -51,20 +51,20 @@ private object CreateOperationValidator extends BaseOperationValidator { } yield () } - private def validateMasterKeyExists(operation: PrismDIDOperation.Create): Either[DIDOperationError, Unit] = { + private def validateMasterKeyExists(operation: PrismDIDOperation.Create): Either[OperationValidationError, Unit] = { val masterKeys = operation.internalKeys.filter(_.purpose == InternalKeyPurpose.Master) if (masterKeys.nonEmpty) Right(()) - else Left(DIDOperationError.InvalidArgument("create operation must contain at least 1 master key")) + else Left(OperationValidationError.InvalidArgument("create operation must contain at least 1 master key")) } private def validateServiceNonEmptyEndpoints( operation: PrismDIDOperation.Create - ): Either[DIDOperationError, Unit] = { + ): Either[OperationValidationError, Unit] = { val serviceWithEmptyEndpoints = operation.services.filter(_.serviceEndpoint.isEmpty).map(_.id) if (serviceWithEmptyEndpoints.isEmpty) Right(()) else Left( - DIDOperationError.InvalidArgument( + OperationValidationError.InvalidArgument( s"service must not have empty serviceEndpoint: ${serviceWithEmptyEndpoints.mkString("[", ", ", "]")}" ) ) @@ -80,7 +80,7 @@ private object CreateOperationValidator extends BaseOperationValidator { } private object UpdateOperationValidator extends BaseOperationValidator { - def validate(config: Config)(operation: PrismDIDOperation.Update): Either[DIDOperationError, Unit] = { + def validate(config: Config)(operation: PrismDIDOperation.Update): Either[OperationValidationError, Unit] = { for { _ <- validateMaxPublicKeysAccess(config)(operation, extractKeyIds) _ <- validateMaxServiceAccess(config)(operation, extractServiceIds) @@ -94,13 +94,17 @@ private object UpdateOperationValidator extends BaseOperationValidator { } yield () } - private def validateNonEmptyUpdateAction(operation: PrismDIDOperation.Update): Either[DIDOperationError, Unit] = { + private def validateNonEmptyUpdateAction( + operation: PrismDIDOperation.Update + ): Either[OperationValidationError, Unit] = { val isActionNonEmpty = operation.actions.nonEmpty if (isActionNonEmpty) Right(()) - else Left(DIDOperationError.InvalidArgument("update operation must contain at least 1 update action")) + else Left(OperationValidationError.InvalidArgument("update operation must contain at least 1 update action")) } - private def validateUpdateServiceNonEmpty(operation: PrismDIDOperation.Update): Either[DIDOperationError, Unit] = { + private def validateUpdateServiceNonEmpty( + operation: PrismDIDOperation.Update + ): Either[OperationValidationError, Unit] = { val isNonEmptyUpdateService = operation.actions.forall { case UpdateDIDAction.UpdateService(_, None, Nil) => false case _ => true @@ -108,7 +112,7 @@ private object UpdateOperationValidator extends BaseOperationValidator { if (isNonEmptyUpdateService) Right(()) else Left( - DIDOperationError.InvalidArgument( + OperationValidationError.InvalidArgument( "update operation with UpdateServiceAction must not have both 'type' and 'serviceEndpoints' empty" ) ) @@ -116,7 +120,7 @@ private object UpdateOperationValidator extends BaseOperationValidator { private def validateAddServiceNonEmptyEndpoint( operation: PrismDIDOperation.Update - ): Either[DIDOperationError, Unit] = { + ): Either[OperationValidationError, Unit] = { val serviceWithEmptyEndpoints = operation.actions .collect { case UpdateDIDAction.AddService(s) => s } .filter(_.serviceEndpoint.isEmpty) @@ -124,7 +128,7 @@ private object UpdateOperationValidator extends BaseOperationValidator { if (serviceWithEmptyEndpoints.isEmpty) Right(()) else Left( - DIDOperationError.InvalidArgument( + OperationValidationError.InvalidArgument( s"service must not have empty serviceEndpoint: ${serviceWithEmptyEndpoints.mkString("[", ", ", "]")}" ) ) @@ -156,7 +160,7 @@ private object UpdateOperationValidator extends BaseOperationValidator { } private object DeactivateOperationValidator extends BaseOperationValidator { - def validate(config: Config)(operation: PrismDIDOperation.Deactivate): Either[DIDOperationError, Unit] = + def validate(config: Config)(operation: PrismDIDOperation.Deactivate): Either[OperationValidationError, Unit] = validatePreviousOperationHash(operation, _.previousOperationHash) } @@ -168,70 +172,72 @@ private trait BaseOperationValidator { protected def validateMaxPublicKeysAccess[T <: PrismDIDOperation]( config: Config - )(operation: T, keyIdExtractor: KeyIdExtractor[T]): Either[DIDOperationError, Unit] = { + )(operation: T, keyIdExtractor: KeyIdExtractor[T]): Either[OperationValidationError, Unit] = { val keyCount = keyIdExtractor(operation).length if (keyCount <= config.publicKeyLimit) Right(()) - else Left(DIDOperationError.TooManyDidPublicKeyAccess(config.publicKeyLimit, Some(keyCount))) + else Left(OperationValidationError.TooManyDidPublicKeyAccess(config.publicKeyLimit, Some(keyCount))) } protected def validateMaxServiceAccess[T <: PrismDIDOperation]( config: Config - )(operation: T, serviceIdExtractor: ServiceIdExtractor[T]): Either[DIDOperationError, Unit] = { + )(operation: T, serviceIdExtractor: ServiceIdExtractor[T]): Either[OperationValidationError, Unit] = { val serviceCount = serviceIdExtractor(operation).length if (serviceCount <= config.serviceLimit) Right(()) - else Left(DIDOperationError.TooManyDidServiceAccess(config.serviceLimit, Some(serviceCount))) + else Left(OperationValidationError.TooManyDidServiceAccess(config.serviceLimit, Some(serviceCount))) } protected def validateUniquePublicKeyId[T <: PrismDIDOperation]( operation: T, keyIdExtractor: KeyIdExtractor[T] - ): Either[DIDOperationError, Unit] = { + ): Either[OperationValidationError, Unit] = { val ids = keyIdExtractor(operation) if (ids.isUnique) Right(()) - else Left(DIDOperationError.InvalidArgument("id for public-keys is not unique")) + else Left(OperationValidationError.InvalidArgument("id for public-keys is not unique")) } protected def validateUniqueServiceId[T <: PrismDIDOperation]( operation: T, serviceIdExtractor: ServiceIdExtractor[T] - ): Either[DIDOperationError, Unit] = { + ): Either[OperationValidationError, Unit] = { val ids = serviceIdExtractor(operation) if (ids.isUnique) Right(()) - else Left(DIDOperationError.InvalidArgument("id for services is not unique")) + else Left(OperationValidationError.InvalidArgument("id for services is not unique")) } protected def validateKeyIdIsUriFragment[T <: PrismDIDOperation]( operation: T, keyIdExtractor: KeyIdExtractor[T] - ): Either[DIDOperationError, Unit] = { + ): Either[OperationValidationError, Unit] = { val ids = keyIdExtractor(operation) val invalidIds = ids.filterNot(isValidUriFragment) if (invalidIds.isEmpty) Right(()) else - Left(DIDOperationError.InvalidArgument(s"public key id is invalid: ${invalidIds.mkString("[", ", ", "]")}")) + Left( + OperationValidationError.InvalidArgument(s"public key id is invalid: ${invalidIds.mkString("[", ", ", "]")}") + ) } protected def validateServiceIdIsUriFragment[T <: PrismDIDOperation]( operation: T, serviceIdExtractor: ServiceIdExtractor[T] - ): Either[DIDOperationError, Unit] = { + ): Either[OperationValidationError, Unit] = { val ids = serviceIdExtractor(operation) val invalidIds = ids.filterNot(isValidUriFragment) if (invalidIds.isEmpty) Right(()) else - Left(DIDOperationError.InvalidArgument(s"service id is invalid: ${invalidIds.mkString("[", ", ", "]")}")) + Left(OperationValidationError.InvalidArgument(s"service id is invalid: ${invalidIds.mkString("[", ", ", "]")}")) } protected def validateServiceEndpointNormalized[T <: PrismDIDOperation]( operation: T, endpointExtractor: ServiceEndpointExtractor[T] - ): Either[DIDOperationError, Unit] = { + ): Either[OperationValidationError, Unit] = { val uris = endpointExtractor(operation).flatMap(_._2) val nonNormalizedUris = uris.filterNot(isUriNormalized) if (nonNormalizedUris.isEmpty) Right(()) else Left( - DIDOperationError.InvalidArgument( + OperationValidationError.InvalidArgument( s"serviceEndpoint URIs must be normalized: ${nonNormalizedUris.mkString("[", ", ", "]")}" ) ) @@ -240,10 +246,10 @@ private trait BaseOperationValidator { protected def validatePreviousOperationHash[T <: PrismDIDOperation]( operation: T, previousOperationHashExtractor: T => ArraySeq[Byte] - ): Either[DIDOperationError, Unit] = { + ): Either[OperationValidationError, Unit] = { val previousOperationHash = previousOperationHashExtractor(operation) if (previousOperationHash.length == 32) Right(()) - else Left(DIDOperationError.InvalidArgument(s"previousOperationHash must have a size of 32 bytes")) + else Left(OperationValidationError.InvalidArgument(s"previousOperationHash must have a size of 32 bytes")) } /** @return true if a given uri is normalized */ diff --git a/castor/lib/core/src/test/scala/io/iohk/atala/castor/core/util/DIDOperationValidatorSpec.scala b/castor/lib/core/src/test/scala/io/iohk/atala/castor/core/util/DIDOperationValidatorSpec.scala index 6d6c4faf5c..9f6373c201 100644 --- a/castor/lib/core/src/test/scala/io/iohk/atala/castor/core/util/DIDOperationValidatorSpec.scala +++ b/castor/lib/core/src/test/scala/io/iohk/atala/castor/core/util/DIDOperationValidatorSpec.scala @@ -16,7 +16,7 @@ import io.iohk.atala.castor.core.model.did.{ UpdateDIDAction, VerificationRelationship } -import io.iohk.atala.castor.core.model.error.DIDOperationError +import io.iohk.atala.castor.core.model.error.OperationValidationError import io.iohk.atala.castor.core.util.DIDOperationValidator.Config import zio.* import zio.test.* @@ -35,7 +35,7 @@ object DIDOperationValidatorSpec extends ZIOSpecDefault { ) private def invalidArgumentContainsString(text: String): Assertion[Either[Any, Any]] = isLeft( - isSubtype[DIDOperationError.InvalidArgument](hasField("msg", _.msg, containsString(text))) + isSubtype[OperationValidationError.InvalidArgument](hasField("msg", _.msg, containsString(text))) ) private val createOperationValidationSpec = { @@ -74,7 +74,7 @@ object DIDOperationValidatorSpec extends ZIOSpecDefault { ) val op = createPrismDIDOperation(publicKeys = publicKeys, internalKeys = internalKeys) assert(DIDOperationValidator(Config(15, 15)).validate(op))( - isLeft(isSubtype[DIDOperationError.TooManyDidPublicKeyAccess](anything)) + isLeft(isSubtype[OperationValidationError.TooManyDidPublicKeyAccess](anything)) ) }, test("reject CreateOperation on duplicated DID public key id") { @@ -107,7 +107,7 @@ object DIDOperationValidatorSpec extends ZIOSpecDefault { ) val op = createPrismDIDOperation(services = services) assert(DIDOperationValidator(Config(15, 15)).validate(op))( - isLeft(isSubtype[DIDOperationError.TooManyDidServiceAccess](anything)) + isLeft(isSubtype[OperationValidationError.TooManyDidServiceAccess](anything)) ) }, test("reject CreateOperation on duplicated service id") { @@ -243,7 +243,7 @@ object DIDOperationValidatorSpec extends ZIOSpecDefault { val removeKeyActions = (1 to 10).map(i => UpdateDIDAction.RemoveKey(s"remove$i")) val op = updatePrismDIDOperation(addKeyActions ++ addInternalKeyActions ++ removeKeyActions) assert(DIDOperationValidator(Config(25, 25)).validate(op))( - isLeft(isSubtype[DIDOperationError.TooManyDidPublicKeyAccess](anything)) + isLeft(isSubtype[OperationValidationError.TooManyDidPublicKeyAccess](anything)) ) }, test("reject UpdateOperation on too many service access") { @@ -266,7 +266,7 @@ object DIDOperationValidatorSpec extends ZIOSpecDefault { ) val op = updatePrismDIDOperation(addServiceActions ++ removeServiceActions ++ updateServiceActions) assert(DIDOperationValidator(Config(25, 25)).validate(op))( - isLeft(isSubtype[DIDOperationError.TooManyDidServiceAccess](anything)) + isLeft(isSubtype[OperationValidationError.TooManyDidServiceAccess](anything)) ) }, test("reject UpdateOperation on invalid key-id") {