Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(castor): implement translation of Node DidData to W3C DidDocument #182

Merged
merged 50 commits into from
Dec 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b1895c0
ci(prism-node): add ci workflow
Nov 15, 2022
b8d6717
ci(prism-node): rename file to prism-node-client.yml
Nov 15, 2022
7af4a07
feat(castor): update castor to use prism-node stub
Nov 15, 2022
afb2d35
feat(castor): implement DID parser backed by prism-identity 1.4
Nov 16, 2022
76a44c0
feat(castor): update return type of PrismDID.buildLongForm
Nov 17, 2022
225a5b8
test(castor): add public key id validation
Nov 18, 2022
9824316
chore(castor): pr diff cleanup
Nov 21, 2022
78b807b
feat(castor): add service to domain model and implement validation
Nov 23, 2022
aef9376
feat(castor): implement DID parser backed by prism-identity 1.4
Nov 16, 2022
952ca09
feat(prism-agent): update castor with new protobuf model
Nov 17, 2022
1e334db
chore(prism-agent): remove unused commitment secret storage
Nov 17, 2022
c6684ff
test(prism-agent): fix and add test for template validation
Nov 17, 2022
e345f75
feat(prism-agent): sign AtalaOperation with master-key for publishing
Nov 17, 2022
31e1ba1
test(prism-agent): fix tests for publishing created DID
Nov 18, 2022
a07587e
feat(prism-agent): use hex string for operation id presentation
Nov 18, 2022
9a2fe98
test(prism-agent): add DID template validation test
Nov 18, 2022
f3f2989
chore(prism-agent): commented code cleanup
Nov 18, 2022
9389157
feat(prism-agent): integrate castor operation validator into wallet-api
Nov 18, 2022
941105c
chore(prism-agent): cleanup unused castor db layers
Nov 18, 2022
a87416a
feat(prism-agent): make wallet DID publication status trackable
Nov 18, 2022
22966d5
test(prism-agent): make test compile
Nov 18, 2022
9c0d10e
feat(prism-agent): sync DID state from DLT
Nov 21, 2022
c578b0e
revert: "feat(castor): implement DID parser backed by prism-identity …
Nov 21, 2022
044cbdb
chore(prism-agent): pr diff cleanup
Nov 21, 2022
10d336b
fix(prism-agent): fix compilation conflict
Nov 22, 2022
691c235
fix(castor): remove duplicated method
Nov 28, 2022
26d2737
Bump up dependency version
Nov 28, 2022
b568865
Merge branch 'main' into feature/prismagent-with-castor-new-protobuf
Nov 28, 2022
e14f7ae
feat(castor): partially implement resolveDID
Nov 22, 2022
6392be1
fix(castor): fix parsing bug from Node response
Nov 23, 2022
308ff0a
feat(castor): convert domain model to w3c representation
Nov 23, 2022
9982c82
feat(castor): w3c public-key and service translation
Nov 23, 2022
eaa6059
feat(castor): translate jwk publickey
Nov 28, 2022
40b0ea8
feat(castor): complete w3c DIDDocument translation
Nov 28, 2022
7c4d6a3
Merge branch 'main' into feature/prismagent-with-castor-new-protobuf
Nov 29, 2022
ba188f5
Merge branch 'feature/prismagent-with-castor-new-protobuf' into featu…
Nov 29, 2022
b46a4eb
feat(castor): implement unpublished DID resolution
Nov 29, 2022
c45678b
fix(castor): fix unpublished did initial state removal
Nov 29, 2022
d64aa61
feat(castor): transform EC point x y with base64 no padding
Nov 29, 2022
eb425e3
Merge branch 'main' into feature/prismagent-with-castor-new-protobuf
Nov 30, 2022
35a2afa
Merge branch 'feature/prismagent-with-castor-new-protobuf' into featu…
Nov 30, 2022
da94370
feat(castor): resolve DID return metadata
Nov 30, 2022
b6cd43b
pr diff cleanup
Nov 30, 2022
7bb833c
Merge branch 'main' into feature/ATL2189-castor-did-document
Nov 30, 2022
d88e966
pr diff clean up
Nov 30, 2022
297fc79
chore(castor): remove unused method
Nov 30, 2022
6981e0f
Merge branch 'main' into feature/ATL2189-castor-did-document
Nov 30, 2022
6364844
Merge branch 'main' into feature/ATL2189-castor-did-document
Dec 1, 2022
5ae2695
feat(castor): add filtering of revoked keys
Dec 1, 2022
b704393
feat(castor): implement deactivate flag metadata
Dec 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
package io.iohk.atala.castor.core.model

import java.net.URI
import java.time.Instant
import com.google.protobuf.ByteString
import io.iohk.atala.castor.core.model.did.{
CanonicalPrismDID,
DIDData,
EllipticCurve,
InternalKeyPurpose,
InternalPublicKey,
LongFormPrismDID,
PrismDID,
PrismDIDOperation,
PublicKey,
PublicKeyData,
ScheduledDIDOperationDetail,
ScheduledDIDOperationStatus,
VerificationRelationship
}
import io.iohk.atala.prism.crypto.EC
import io.iohk.atala.prism.protos.common_models.OperationStatus
import io.iohk.atala.prism.protos.node_models.KeyUsage
import io.iohk.atala.prism.protos.node_models.PublicKey.KeyData
import io.iohk.atala.shared.models.HexStrings.*
import io.iohk.atala.shared.models.Base64UrlStrings.*
import io.iohk.atala.shared.utils.Traverse.*
import io.iohk.atala.prism.protos.{KeyUsage, common_models, node_api, node_models}
import io.iohk.atala.prism.protos.{common_models, node_api, node_models}
import zio.*

import scala.util.Try

Expand All @@ -34,6 +44,7 @@ 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)
)
)
Expand Down Expand Up @@ -105,4 +116,109 @@ private[castor] trait ProtoModelHelper {
}
}

extension (didData: node_models.DIDData) {
def toDomain: Either[String, DIDData] = {
for {
canonicalDID <- PrismDID.buildCanonicalFromSuffix(didData.id)
allKeys <- didData.publicKeys.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 }
)
}

/** Return DIDData with keys and services removed by checking revocation time against 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)
})
}
}
}

extension (operation: node_models.CreateDIDOperation) {
def toDomain: Either[String, PrismDIDOperation.Create] = {
for {
allKeys <- operation.didData.map(_.publicKeys.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)
)
}
}

extension (publicKey: node_models.PublicKey) {
def toDomain: Either[String, PublicKey | InternalPublicKey] = {
val purpose: Either[String, VerificationRelationship | InternalKeyPurpose] = publicKey.usage match {
case node_models.KeyUsage.UNKNOWN_KEY => Left(s"unsupported use of KeyUsage.UNKNOWN_KEY on key ${publicKey.id}")
case node_models.KeyUsage.MASTER_KEY => Right(InternalKeyPurpose.Master)
case node_models.KeyUsage.ISSUING_KEY => Right(VerificationRelationship.AssertionMethod)
case node_models.KeyUsage.COMMUNICATION_KEY => Right(VerificationRelationship.KeyAgreement)
case node_models.KeyUsage.AUTHENTICATION_KEY => Right(VerificationRelationship.Authentication)
case node_models.KeyUsage.REVOCATION_KEY =>
??? // TODO: define the corresponding KeyUsage in Prism DID (ATL-2213)
case node_models.KeyUsage.Unrecognized(unrecognizedValue) =>
Left(s"unrecognized KeyUsage: $unrecognizedValue on key ${publicKey.id}")
}

for {
purpose <- purpose
keyData <- publicKey.keyData.toDomain
} yield purpose match {
case purpose: VerificationRelationship =>
PublicKey(
id = publicKey.id,
purpose = purpose,
publicKeyData = keyData
)
case purpose: InternalKeyPurpose =>
publicKey.keyData.ecKeyData
InternalPublicKey(
id = publicKey.id,
purpose = purpose,
publicKeyData = keyData
)
}
}
}

extension (publicKeyData: node_models.PublicKey.KeyData) {
def toDomain: Either[String, PublicKeyData] = {
publicKeyData match {
case KeyData.Empty => Left(s"unable to convert KeyData.Emtpy to PublicKeyData")
case KeyData.EcKeyData(ecKeyData) =>
for {
curve <- EllipticCurve
.parseString(ecKeyData.curve)
.toRight(s"unsupported elliptic curve ${ecKeyData.curve}")
} yield PublicKeyData.ECKeyData(
crv = curve,
x = Base64UrlString.fromByteArray(ecKeyData.x.toByteArray),
y = Base64UrlString.fromByteArray(ecKeyData.y.toByteArray)
)
case KeyData.CompressedEcKeyData(ecKeyData) =>
val ecPublicKey = EC.INSTANCE.toPublicKeyFromCompressed(ecKeyData.data.toByteArray)
for {
curve <- EllipticCurve
.parseString(ecKeyData.curve)
.toRight(s"unsupported elliptic curve ${ecKeyData.curve}")
} yield PublicKeyData.ECKeyData(
crv = curve,
x = Base64UrlString.fromByteArray(ecPublicKey.getCurvePoint.getX.bytes()),
y = Base64UrlString.fromByteArray(ecPublicKey.getCurvePoint.getY.bytes())
)
}
}
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
package io.iohk.atala.castor.core.model.did

import io.iohk.atala.castor.core.model.did.w3c.DIDDocumentRepr

import scala.collection.immutable.ArraySeq

final case class DIDData(
id: CanonicalPrismDID,
publicKeys: Seq[PublicKey],
services: Seq[Service],
internalKeys: Seq[InternalPublicKey]
)

final case class DIDMetadata(
lastOperationHash: ArraySeq[Byte],
deactivated: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,21 @@ object PrismDID extends ProtoModelHelper {
.map(_.getMessage)
.map(_ => CanonicalPrismDID(HexString.fromByteArray(stateHash)))

def buildLongFormFromOperation(createOperation: PrismDIDOperation.Create): LongFormPrismDID = {
val createDIDOperation = createOperation.toProto
val atalaOperation = node_models.AtalaOperation(createDIDOperation)
buildLongFormFromAtalaOperation(atalaOperation).toOption.get
}
def buildCanonicalFromSuffix(suffix: String): Either[String, CanonicalPrismDID] =
HexString
.fromString(suffix)
.toEither
.left
.map(e => s"unable to parse suffix as hex string: ${e.getMessage}")
.flatMap(suffix => buildCanonical(suffix.toByteArray))

def buildLongFormFromOperation(createOperation: PrismDIDOperation.Create): LongFormPrismDID = LongFormPrismDID(
createOperation
)

def buildLongFormFromAtalaOperation(atalaOperation: node_models.AtalaOperation): Either[String, LongFormPrismDID] =
atalaOperation.operation match {
case Operation.CreateDid(_) => Right(LongFormPrismDID(atalaOperation))
case Operation.CreateDid(op) => op.toDomain.map(LongFormPrismDID.apply)
case operation =>
Left(s"Provided initial state of long form Prism DID is ${operation.value}, CreateDid Atala operation expected")
}
Expand Down Expand Up @@ -102,7 +108,8 @@ final case class CanonicalPrismDID private[did] (stateHash: HexString) extends P
override val suffix: DIDMethodSpecificId = DIDMethodSpecificId.fromString(stateHash.toString).get
}

final case class LongFormPrismDID private[did] (atalaOperation: node_models.AtalaOperation) extends PrismDID {
final case class LongFormPrismDID private[did] (createOperation: PrismDIDOperation.Create) extends PrismDID {

override val stateHash: HexString = {
val encodedState = atalaOperation.toByteArray
HexString.fromByteArray(Sha256.compute(encodedState).getValue)
Expand All @@ -112,4 +119,6 @@ final case class LongFormPrismDID private[did] (atalaOperation: node_models.Atal
val encodedState = Base64UrlString.fromByteArray(atalaOperation.toByteArray).toStringNoPadding
DIDMethodSpecificId.fromString(s"${stateHash.toString}:${encodedState}").get
}

def atalaOperation: node_models.AtalaOperation = createOperation.toAtalaOperation
}

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.iohk.atala.castor.core.model.did.w3c

import io.iohk.atala.castor.core.model.did.{DID, DIDUrl}

/** A projection of DIDDocument data model to W3C compliant DID representation */
final case class DIDDocumentRepr(
id: String,
controller: String,
verificationMethod: Seq[PublicKeyRepr],
authentication: Seq[PublicKeyRepr],
assertionMethod: Seq[PublicKeyRepr],
keyAgreement: Seq[PublicKeyRepr],
capabilityInvocation: Seq[PublicKeyRepr],
service: Seq[ServiceRepr]
)

final case class PublicKeyRepr(
id: String,
`type`: "EcdsaSecp256k1VerificationKey2019",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow :), how does it works, this type in double quotes?

Copy link
Contributor Author

@patlo-iog patlo-iog Dec 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new in scala 3 :). Enforce that type must not only be string, but string of value EcdsaSecp256k1VerificationKey2019

controller: String,
publicKeyJwk: PublicKeyJwk
)

final case class ServiceRepr(
id: String,
`type`: String,
serviceEndpoint: String
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@patlo-iog, just one comment regarding serviceEndpoint to be a String. Strictly in the spec can be also a map or a set. We are seeing a probable interoperability issue in DIDComm because it requieres serviceEndpoint to be a set with an URI. So, it should be nice to have in mind to extend the type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rodolfomiranda . It will be trivial to extend these types in scala 3 so if we ever need to project some document where it provides a set of URIs then we can add String | Seq[String]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm falling in love with Scala :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOL

)

final case class PublicKeyJwk(kty: "EC", crv: String, x: String, y: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.iohk.atala.castor.core.model.did.w3c

import java.time.Instant

// errors are based on https://www.w3.org/TR/did-spec-registries/#error
enum DIDResolutionErrorRepr(val value: String) {
case InvalidDID extends DIDResolutionErrorRepr("invalidDid")
case InvalidDIDUrl extends DIDResolutionErrorRepr("invalidDidUrl")
case NotFound extends DIDResolutionErrorRepr("notFound")
case RepresentationNotSupported extends DIDResolutionErrorRepr("representationNotSupported")
case InternalError extends DIDResolutionErrorRepr("internalError")
case InvalidPublicKeyLength extends DIDResolutionErrorRepr("invalidPublicKeyLength")
case InvalidPublicKeyType extends DIDResolutionErrorRepr("invalidPublicKeyType")
case UnsupportedPublicKeyType extends DIDResolutionErrorRepr("unsupportedPublicKeyType")
}

final case class DIDDocumentMetadataRepr(deactivated: Boolean)
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.iohk.atala.castor.core.model.did.w3c

import io.iohk.atala.castor.core.model.did.{
CanonicalPrismDID,
DIDData,
DIDMetadata,
PublicKey,
PublicKeyData,
Service,
VerificationRelationship
}

object W3CModelHelper extends W3CModelHelper

private[castor] trait W3CModelHelper {

extension (didMetadata: DIDMetadata) {
def toW3C: DIDDocumentMetadataRepr = DIDDocumentMetadataRepr(deactivated = didMetadata.deactivated)
}

extension (didData: DIDData) {
def toW3C: DIDDocumentRepr = {
val keyWithPurpose = didData.publicKeys.map(k => k.purpose -> k.toW3C(didData.id))
DIDDocumentRepr(
id = didData.id.toString,
controller = didData.id.toString,
verificationMethod = Nil,
authentication = keyWithPurpose.collect { case (VerificationRelationship.Authentication, k) => k },
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)
)
}
}

extension (service: Service) {
def toW3C: ServiceRepr = ServiceRepr(
id = service.id,
`type` = service.`type`.name,
serviceEndpoint = service.serviceEndpoint.toString
)
}

extension (publicKey: PublicKey) {
def toW3C(controller: CanonicalPrismDID): PublicKeyRepr = PublicKeyRepr(
id = publicKey.id,
`type` = "EcdsaSecp256k1VerificationKey2019",
controller = controller.toString,
publicKeyJwk = publicKey.publicKeyData match {
case PublicKeyData.ECKeyData(crv, x, y) =>
PublicKeyJwk(
kty = "EC",
crv = crv.name,
x = x.toStringNoPadding,
y = y.toStringNoPadding
)
}
)
}

}
Loading