diff --git a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/ProtoModelHelper.scala b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/ProtoModelHelper.scala index 1b4c37ef88..0a98a0365c 100644 --- a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/ProtoModelHelper.scala +++ b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/ProtoModelHelper.scala @@ -16,6 +16,8 @@ import io.iohk.atala.castor.core.model.did.{ PublicKeyData, ScheduledDIDOperationDetail, ScheduledDIDOperationStatus, + Service, + ServiceType, VerificationRelationship } import io.iohk.atala.prism.crypto.EC @@ -44,8 +46,8 @@ private[castor] trait ProtoModelHelper { value = node_models.CreateDIDOperation( didData = Some( node_models.CreateDIDOperation.DIDCreationData( - // TODO: add Service when it is added to Prism DID method spec (ATL-2203) - publicKeys = createDIDOp.publicKeys.map(_.toProto) ++ createDIDOp.internalKeys.map(_.toProto) + publicKeys = createDIDOp.publicKeys.map(_.toProto) ++ createDIDOp.internalKeys.map(_.toProto), + services = createDIDOp.services.map(_.toProto) ) ) ) @@ -63,6 +65,7 @@ private[castor] trait ProtoModelHelper { case VerificationRelationship.AssertionMethod => node_models.KeyUsage.ISSUING_KEY case VerificationRelationship.KeyAgreement => node_models.KeyUsage.COMMUNICATION_KEY case VerificationRelationship.CapabilityInvocation => ??? + case VerificationRelationship.CapabilityDelegation => ??? }, addedOn = None, revokedOn = None, @@ -101,6 +104,16 @@ private[castor] trait ProtoModelHelper { } } + extension (service: Service) { + def toProto: node_models.Service = node_models.Service( + id = service.id, + `type` = service.`type`.name, + serviceEndpoint = service.serviceEndpoint.map(_.toString), + addedOn = None, + deletedOn = None + ) + } + extension (resp: node_api.GetOperationInfoResponse) { def toDomain: Either[String, Option[ScheduledDIDOperationDetail]] = { val status = resp.operationStatus match { @@ -121,38 +134,61 @@ private[castor] trait ProtoModelHelper { for { canonicalDID <- PrismDID.buildCanonicalFromSuffix(didData.id) allKeys <- didData.publicKeys.traverse(_.toDomain) + services <- didData.services.traverse(_.toDomain) } yield DIDData( id = canonicalDID, publicKeys = allKeys.collect { case key: PublicKey => key }, - services = Nil, // TODO: add Service when it is added to Prism DID method spec (ATL-2203) - internalKeys = allKeys.collect { case key: InternalPublicKey => key } + internalKeys = allKeys.collect { case key: InternalPublicKey => key }, + services = services ) } - /** Return DIDData with keys and services removed by checking revocation time against current time */ + /** Return DIDData with keys and services removed by checking revocation time against the current time */ def filterRevokedKeysAndServices: UIO[node_models.DIDData] = { - // TODO: filter Service when it is added to Prism DID method spec (ATL-2203) Clock.instant.map { now => didData .withPublicKeys(didData.publicKeys.filter { publicKey => - val maybeRevokeTime = publicKey.revokedOn - .flatMap(_.timestampInfo) - .flatMap(_.blockTimestamp) - .map(ts => Instant.ofEpochSecond(ts.seconds).plusNanos(ts.nanos)) - maybeRevokeTime.forall(revokeTime => revokeTime isBefore now) + publicKey.revokedOn.flatMap(_.toInstant).forall(revokeTime => revokeTime isBefore now) + }) + .withServices(didData.services.filter { service => + service.deletedOn.flatMap(_.toInstant).forall(revokeTime => revokeTime isBefore now) }) } } } + extension (ledgerData: node_models.LedgerData) { + def toInstant: Option[Instant] = ledgerData.timestampInfo + .flatMap(_.blockTimestamp) + .map(ts => Instant.ofEpochSecond(ts.seconds).plusNanos(ts.nanos)) + } + extension (operation: node_models.CreateDIDOperation) { def toDomain: Either[String, PrismDIDOperation.Create] = { for { allKeys <- operation.didData.map(_.publicKeys.traverse(_.toDomain)).getOrElse(Right(Nil)) + services <- operation.didData.map(_.services.traverse(_.toDomain)).getOrElse(Right(Nil)) } yield PrismDIDOperation.Create( publicKeys = allKeys.collect { case key: PublicKey => key }, internalKeys = allKeys.collect { case key: InternalPublicKey => key }, - services = Nil // TODO: add Service when it is added to Prism DID method spec (ATL-2203) + services = services + ) + } + } + + extension (service: node_models.Service) { + def toDomain: Either[String, Service] = { + for { + uris <- service.serviceEndpoint.traverse(s => + Try(URI.create(s)).toEither.left.map(_ => s"unable to parse serviceEndpoint $s as URI") + ) + serviceType <- ServiceType + .parseString(service.`type`) + .toRight(s"unable to parse ${service.`type`} as service type") + } yield Service( + id = service.id, + `type` = serviceType, + serviceEndpoint = uris ) } } diff --git a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/Service.scala b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/Service.scala index c9d05b8b50..184200c458 100644 --- a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/Service.scala +++ b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/Service.scala @@ -5,5 +5,5 @@ import java.net.URI final case class Service( id: String, `type`: ServiceType, - serviceEndpoint: URI + serviceEndpoint: Seq[URI] ) diff --git a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/VerificationRelationship.scala b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/VerificationRelationship.scala index 96298aa795..797ee0d51b 100644 --- a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/VerificationRelationship.scala +++ b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/VerificationRelationship.scala @@ -5,6 +5,7 @@ enum VerificationRelationship(val name: String) { case AssertionMethod extends VerificationRelationship("assertionMethod") case KeyAgreement extends VerificationRelationship("keyAgreement") case CapabilityInvocation extends VerificationRelationship("capabilityInvocation") + case CapabilityDelegation extends VerificationRelationship("capabilityDelegation") } object VerificationRelationship { diff --git a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/w3c/DIDDocumentRepr.scala b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/w3c/DIDDocumentRepr.scala index 775a34e7e0..bf7a31fa16 100644 --- a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/w3c/DIDDocumentRepr.scala +++ b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/w3c/DIDDocumentRepr.scala @@ -24,7 +24,7 @@ final case class PublicKeyRepr( final case class ServiceRepr( id: String, `type`: String, - serviceEndpoint: String + serviceEndpoint: Seq[String] ) final case class PublicKeyJwk(kty: "EC", crv: String, x: String, y: String) diff --git a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/w3c/W3CModelHelper.scala b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/w3c/W3CModelHelper.scala index 5255bed5aa..16fe3e42ff 100644 --- a/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/w3c/W3CModelHelper.scala +++ b/castor/lib/core/src/main/scala/io/iohk/atala/castor/core/model/did/w3c/W3CModelHelper.scala @@ -20,7 +20,7 @@ private[castor] trait W3CModelHelper { extension (didData: DIDData) { def toW3C: DIDDocumentRepr = { - val keyWithPurpose = didData.publicKeys.map(k => k.purpose -> k.toW3C(didData.id)) + val keyWithPurpose = didData.publicKeys.map(k => k.purpose -> k.toW3C(didData.id, didData.id)) DIDDocumentRepr( id = didData.id.toString, controller = didData.id.toString, @@ -29,22 +29,22 @@ private[castor] trait W3CModelHelper { assertionMethod = keyWithPurpose.collect { case (VerificationRelationship.AssertionMethod, k) => k }, keyAgreement = keyWithPurpose.collect { case (VerificationRelationship.KeyAgreement, k) => k }, capabilityInvocation = keyWithPurpose.collect { case (VerificationRelationship.CapabilityInvocation, k) => k }, - service = didData.services.map(_.toW3C) + service = didData.services.map(_.toW3C(didData.id)) ) } } extension (service: Service) { - def toW3C: ServiceRepr = ServiceRepr( - id = service.id, + def toW3C(did: CanonicalPrismDID): ServiceRepr = ServiceRepr( + id = s"${did.toString}#${service.id}", `type` = service.`type`.name, - serviceEndpoint = service.serviceEndpoint.toString + serviceEndpoint = service.serviceEndpoint.map(_.toString) ) } extension (publicKey: PublicKey) { - def toW3C(controller: CanonicalPrismDID): PublicKeyRepr = PublicKeyRepr( - id = publicKey.id, + def toW3C(did: CanonicalPrismDID, controller: CanonicalPrismDID): PublicKeyRepr = PublicKeyRepr( + id = s"${did.toString}#${publicKey.id}", `type` = "EcdsaSecp256k1VerificationKey2019", controller = controller.toString, publicKeyJwk = publicKey.publicKeyData match { 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 b8ed653df8..9d609e414e 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 @@ -27,6 +27,7 @@ class DIDOperationValidator(config: Config) { _ <- validateMaxServiceAccess(operation) _ <- validateUniquePublicKeyId(operation) _ <- validateUniqueServiceId(operation) + _ <- validateUniqueServiceUri(operation) _ <- validateKeyIdRegex(operation) } yield () } @@ -78,4 +79,18 @@ class DIDOperationValidator(config: Config) { } } + private def validateUniqueServiceUri(operation: PrismDIDOperation): Either[DIDOperationError, Unit] = { + operation match { + case PrismDIDOperation.Create(_, _, services) => + services + .find { service => + val uris = service.serviceEndpoint.map(_.toString) + uris.length != uris.distinct.length + } + .toLeft(()) + .left + .map(s => DIDOperationError.InvalidArgument(s"service ${s.id} does not have unique serviceEndpoint URIs")) + } + } + } diff --git a/castor/lib/core/src/test/scala/io/iohk/atala/castor/core/model/ProtoModelHelperSpec.scala b/castor/lib/core/src/test/scala/io/iohk/atala/castor/core/model/ProtoModelHelperSpec.scala index bbac4439f9..75e02b8819 100644 --- a/castor/lib/core/src/test/scala/io/iohk/atala/castor/core/model/ProtoModelHelperSpec.scala +++ b/castor/lib/core/src/test/scala/io/iohk/atala/castor/core/model/ProtoModelHelperSpec.scala @@ -80,6 +80,68 @@ object ProtoModelHelperSpec extends ZIOSpecDefault { ) validKeysId <- didData.filterRevokedKeysAndServices.map(_.publicKeys.map(_.id)) } yield assert(validKeysId)(isEmpty) + }, + test("not filter services if deletedOn is empty") { + val didData = node_models.DIDData( + id = "123", + services = Seq( + node_models.Service(id = "service1"), + node_models.Service(id = "service2"), + node_models.Service(id = "service3") + ) + ) + assertZIO(didData.filterRevokedKeysAndServices.map(_.services.map(_.id)))( + hasSameElements(Seq("service1", "service2", "service3")) + ) + }, + test("not filter services if deletedOn timestamp has not passed") { + for { + now <- Clock.instant + revokeTime = now.minusSeconds(5) + ledgerData = node_models.LedgerData(timestampInfo = + Some(node_models.TimestampInfo(blockTimestamp = Some(revokeTime.toTimestamp))) + ) + didData = node_models.DIDData( + id = "123", + services = Seq( + node_models.Service(id = "key1"), + node_models.Service(id = "key2", deletedOn = Some(ledgerData)), + node_models.Service(id = "key3", deletedOn = Some(ledgerData)) + ) + ) + validKeysId <- didData.filterRevokedKeysAndServices.map(_.services.map(_.id)) + } yield assert(validKeysId)(hasSameElements(Seq("key1", "key2", "key3"))) + }, + test("filter services if deletedOn timestamp has passed") { + for { + now <- Clock.instant + revokeTime = now.plusSeconds(5) + ledgerData = node_models.LedgerData(timestampInfo = + Some(node_models.TimestampInfo(blockTimestamp = Some(revokeTime.toTimestamp))) + ) + didData = node_models.DIDData( + id = "123", + services = Seq( + node_models.Service(id = "key1"), + node_models.Service(id = "key2", deletedOn = Some(ledgerData)), + node_models.Service(id = "key3", deletedOn = Some(ledgerData)) + ) + ) + validKeysId <- didData.filterRevokedKeysAndServices.map(_.services.map(_.id)) + } yield assert(validKeysId)(hasSameElements(Seq("key1"))) + }, + test("filter services if deletedOn timestamp is exactly now") { + for { + now <- Clock.instant + ledgerData = node_models.LedgerData(timestampInfo = + Some(node_models.TimestampInfo(blockTimestamp = Some(now.toTimestamp))) + ) + didData = node_models.DIDData( + id = "123", + services = Seq(node_models.Service(id = "key1", deletedOn = Some(ledgerData))) + ) + validKeysId <- didData.filterRevokedKeysAndServices.map(_.services.map(_.id)) + } yield assert(validKeysId)(isEmpty) } ) 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 b534f54ca8..1c9a18be9f 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 @@ -93,7 +93,7 @@ object DIDOperationValidatorSpec extends ZIOSpecDefault { Service( id = s"service$i", `type` = ServiceType.MediatorService, - serviceEndpoint = URI.create("http://example.com") + serviceEndpoint = Seq(URI.create("http://example.com")) ) ) val op = createPrismDIDOperation(services = services) @@ -106,7 +106,7 @@ object DIDOperationValidatorSpec extends ZIOSpecDefault { Service( id = s"service0", `type` = ServiceType.MediatorService, - serviceEndpoint = URI.create("http://example.com") + serviceEndpoint = Seq(URI.create("http://example.com")) ) ) val op = createPrismDIDOperation(services = services) @@ -131,6 +131,19 @@ object DIDOperationValidatorSpec extends ZIOSpecDefault { assert(DIDOperationValidator(Config(50, 50)).validate(op))( isLeft(isSubtype[DIDOperationError.InvalidArgument](anything)) ) + }, + test("reject CreateOperation on non-unique serviceEndpoint URI") { + val services = Seq( + Service( + id = s"service0", + `type` = ServiceType.MediatorService, + serviceEndpoint = Seq(URI.create("http://example.com"), URI.create("http://example.com")) + ) + ) + val op = createPrismDIDOperation(services = services) + assert(DIDOperationValidator(Config(50, 50)).validate(op))( + isLeft(isSubtype[DIDOperationError.InvalidArgument](anything)) + ) } ) } diff --git a/castor/lib/project/Dependencies.scala b/castor/lib/project/Dependencies.scala index d34876e90e..d95a434b59 100644 --- a/castor/lib/project/Dependencies.scala +++ b/castor/lib/project/Dependencies.scala @@ -5,7 +5,7 @@ object Dependencies { val zio = "2.0.4" val doobie = "1.0.0-RC2" val zioCatsInterop = "3.3.0" - val prismNodeClient = "0.1.0" + val prismNodeClient = "0.2.0" val prismSdk = "v1.4.1" // scala-steward:off val shared = "0.2.0" val flyway = "9.8.3"