Skip to content

Commit

Permalink
feat(castor): upgrade castor with service in protobuf definition (#224)
Browse files Browse the repository at this point in the history
* feat(castor): add service protobuf model conversion

* test(castor): add test on service filtering

* feat(castor): update service endpoint validation

* feat(castor): bump prism-node client to 0.2.0
  • Loading branch information
patlo-iog authored Dec 9, 2022
1 parent 86773f0 commit 8223740
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
)
)
)
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ import java.net.URI
final case class Service(
id: String,
`type`: ServiceType,
serviceEndpoint: URI
serviceEndpoint: Seq[URI]
)
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class DIDOperationValidator(config: Config) {
_ <- validateMaxServiceAccess(operation)
_ <- validateUniquePublicKeyId(operation)
_ <- validateUniqueServiceId(operation)
_ <- validateUniqueServiceUri(operation)
_ <- validateKeyIdRegex(operation)
} yield ()
}
Expand Down Expand Up @@ -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"))
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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))
)
}
)
}
Expand Down
2 changes: 1 addition & 1 deletion castor/lib/project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 8223740

Please sign in to comment.