Skip to content

Commit

Permalink
feat(castor): add operation validation when resolving unpublished DID
Browse files Browse the repository at this point in the history
  • Loading branch information
Pat Losoponkul committed Feb 1, 2023
1 parent 1ab6058 commit 93a04e3
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,23 @@ 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

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
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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("[", ", ", "]")}"
)
)
Expand All @@ -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)
Expand All @@ -94,37 +94,41 @@ 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
}
if (isNonEmptyUpdateService) Right(())
else
Left(
DIDOperationError.InvalidArgument(
OperationValidationError.InvalidArgument(
"update operation with UpdateServiceAction must not have both 'type' and 'serviceEndpoints' empty"
)
)
}

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)
.map(_.id)
if (serviceWithEmptyEndpoints.isEmpty) Right(())
else
Left(
DIDOperationError.InvalidArgument(
OperationValidationError.InvalidArgument(
s"service must not have empty serviceEndpoint: ${serviceWithEmptyEndpoints.mkString("[", ", ", "]")}"
)
)
Expand Down Expand Up @@ -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)
}

Expand All @@ -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("[", ", ", "]")}"
)
)
Expand All @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -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 = {
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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") {
Expand All @@ -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") {
Expand Down

0 comments on commit 93a04e3

Please sign in to comment.