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

refactor(ATL-6924): add support for other keys #834

Merged
merged 5 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
4 changes: 1 addition & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,6 @@ lazy val Dependencies = new {

// We have to exclude bouncycastle since for some reason bitcoinj depends on bouncycastle jdk15to18
// (i.e. JDK 1.5 to 1.8), but we are using JDK 11
val prismCrypto =
"io.iohk.atala" % "prism-crypto-jvm" % versions.prismSdk
val prismIdentity =
"io.iohk.atala" % "prism-identity-jvm" % versions.prismSdk

Expand Down Expand Up @@ -154,7 +152,7 @@ lazy val Dependencies = new {
val sttpDependencies = Seq(sttpCore, sttpCE2)
val tofuDependencies = Seq(tofu, tofuLogging, tofuDerevoTagless)
val prismDependencies =
Seq(prismCrypto, prismIdentity)
Seq(prismIdentity)
val scalapbDependencies = Seq(
"com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf",
"com.thesamet.scalapb" %% "scalapb-runtime-grpc" % scalapb.compiler.Version.scalapbVersion
Expand Down
101 changes: 101 additions & 0 deletions node/src/main/scala/io/iohk/atala/prism/node/crypto/CryptoUtils.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package io.iohk.atala.prism.node.crypto

import io.iohk.atala.prism.node.models.ProtocolConstants
import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util
import org.bouncycastle.jce.interfaces.ECPublicKey

import java.security.{KeyFactory, MessageDigest, PublicKey, Security, Signature}
import org.bouncycastle.jce.{ECNamedCurveTable, ECPointUtil}
import org.bouncycastle.jce.provider.BouncyCastleProvider

import java.security.spec.{ECPoint, ECPublicKeySpec}

object CryptoUtils {
trait SecpPublicKey {
private[crypto] def publicKey: PublicKey
def curveName: String = ProtocolConstants.secpCurveName
def compressed: Array[Byte] = publicKey
.asInstanceOf[ECPublicKey]
.getQ
.getEncoded(true)
def x: Array[Byte] = publicKey.asInstanceOf[ECPublicKey].getQ.getAffineXCoord.getEncoded
def y: Array[Byte] = publicKey.asInstanceOf[ECPublicKey].getQ.getAffineYCoord.getEncoded
}

// We define the constructor to SecpKeys private so that the only way to generate
// these keys is by using the methods unsafeToPublicKeyFromByteCoordinates and
// unsafeToPublicKeyFromCompressed.
private object SecpPublicKey {
private class SecpPublicKeyImpl(pubKey: PublicKey) extends SecpPublicKey {
override private[crypto] def publicKey: PublicKey = pubKey
}

def fromPublicKey(key: PublicKey): SecpPublicKey = new SecpPublicKeyImpl(key)
}

private val provider = new BouncyCastleProvider()
private val PUBLIC_KEY_COORDINATE_BYTE_SIZE: Int = 32

Security.addProvider(provider)

def hash(bArray: Array[Byte]): Vector[Byte] = {
Copy link

Choose a reason for hiding this comment

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

Would it make sense to specify the type of hash (sha256) in the name of the function? it does not matter that much in this codebase, but if we've been working on something we plan to extend, I'd prefer to name it more specific.

MessageDigest
.getInstance("SHA-256")
.digest(bArray)
.toVector
}

def bytesToHex(bytes: Vector[Byte]): String = {
bytes.map(byte => f"${byte & 0xff}%02x").mkString
}

def hexedHash(bArray: Array[Byte]): String = {
Copy link

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sounds good, let me create the type too so we remove Vector

bytesToHex(hash(bArray))
}

def checkECDSASignature(msg: Array[Byte], sig: Array[Byte], pubKey: SecpPublicKey): Boolean = {
val ecdsaVerify = Signature.getInstance("SHA256withECDSA", provider)
ecdsaVerify.initVerify(pubKey.publicKey)
ecdsaVerify.update(msg.toArray)
ecdsaVerify.verify(sig.toArray)
}

def unsafeToSecpPublicKeyFromByteCoordinates(x: Array[Byte], y: Array[Byte]): SecpPublicKey = {
def trimLeadingZeroes(arr: Array[Byte], c: String): Array[Byte] = {
val trimmed = arr.dropWhile(_ == 0.toByte)
require(
trimmed.length <= PUBLIC_KEY_COORDINATE_BYTE_SIZE,
s"Expected $c coordinate byte length to be less than or equal ${PUBLIC_KEY_COORDINATE_BYTE_SIZE}, but got ${trimmed.length} bytes"
)
trimmed
}

val xTrimmed = trimLeadingZeroes(x, "x")
val yTrimmed = trimLeadingZeroes(y, "y")
val xInteger = BigInt(1, xTrimmed)
val yInteger = BigInt(1, yTrimmed)
unsafeToSecpPublicKeyFromBigIntegerCoordinates(xInteger, yInteger)
}

private def unsafeToSecpPublicKeyFromBigIntegerCoordinates(x: BigInt, y: BigInt): SecpPublicKey = {
val params = ECNamedCurveTable.getParameterSpec("secp256k1")
val fact = KeyFactory.getInstance("ECDSA", provider)
val curve = params.getCurve
val ellipticCurve = EC5Util.convertCurve(curve, params.getSeed)
val point = new ECPoint(x.bigInteger, y.bigInteger)
val params2 = EC5Util.convertSpec(ellipticCurve, params)
val keySpec = new ECPublicKeySpec(point, params2)
SecpPublicKey.fromPublicKey(fact.generatePublic(keySpec))
}

def unsafeToSecpPublicKeyFromCompressed(com: Vector[Byte]): SecpPublicKey = {
val params = ECNamedCurveTable.getParameterSpec("secp256k1")
val fact = KeyFactory.getInstance("ECDSA", provider)
val curve = params.getCurve
val ellipticCurve = EC5Util.convertCurve(curve, params.getSeed)
val point = ECPointUtil.decodePoint(ellipticCurve, com.toArray)
val params2 = EC5Util.convertSpec(ellipticCurve, params)
val keySpec = new ECPublicKeySpec(point, params2)
SecpPublicKey.fromPublicKey(fact.generatePublic(keySpec))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import com.google.protobuf.ByteString
import io.iohk.atala.prism.protos.models.TimestampInfo
import io.iohk.atala.prism.crypto.EC.{INSTANCE => EC}
import io.iohk.atala.prism.crypto.keys.ECPublicKey
import io.iohk.atala.prism.crypto.ECConfig.{INSTANCE => ECConfig}
import io.iohk.atala.prism.node.models.{DidSuffix, Ledger}
import io.iohk.atala.prism.node.models.{DidSuffix, Ledger, PublicKeyData}
import io.iohk.atala.prism.protos.common_models
import io.iohk.atala.prism.node.models
import io.iohk.atala.prism.node.models.KeyUsage._
Expand Down Expand Up @@ -56,7 +55,7 @@ object ProtoCodecs {
didDataState.keys.map(key =>
toProtoPublicKey(
key.keyId,
toECKeyData(key.key),
toCompressedECKeyData(key.key),
toProtoKeyUsage(key.keyUsage),
toLedgerData(key.addedOn),
key.revokedOn map toLedgerData
Expand All @@ -83,28 +82,26 @@ object ProtoCodecs {

def toProtoPublicKey(
id: String,
ecKeyData: node_models.ECKeyData,
compressedEcKeyData: node_models.CompressedECKeyData,
Comment on lines -86 to +85
Copy link
Contributor Author

Choose a reason for hiding this comment

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

as we now assume we always retrieve a compressed key, we request that the model to convert is a compressed one

Copy link

Choose a reason for hiding this comment

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

does it apply to new key types and old key type as well? would that require a change in spec then?

Copy link
Contributor

Choose a reason for hiding this comment

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

IMO I wouldn't require change on the specs. Would just put in the spec that the compressed version is recommended.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this method is applied on data retrieved from the node DB, we store keys in compressed format, meaning that all keys we retrieve will be compressed indistinctly of how the user submitted it. So, no need for spec changes for this part

We do need to tell in the spec that Ed25519 and X25519 keys can only be sent in compressed format (which I understand is fine, please correct if I am wrong @FabioPinheiro ). Secp keys can be sent in any of the formats and the node will work fine

keyUsage: node_models.KeyUsage,
addedOn: node_models.LedgerData,
revokedOn: Option[node_models.LedgerData]
): node_models.PublicKey = {
val withoutRevKey = node_models
.PublicKey()
.withId(id)
.withEcKeyData(ecKeyData)
.withCompressedEcKeyData(compressedEcKeyData)
.withUsage(keyUsage)
.withAddedOn(addedOn)

revokedOn.fold(withoutRevKey)(revTime => withoutRevKey.withRevokedOn(revTime))
}

def toECKeyData(key: ECPublicKey): node_models.ECKeyData = {
val point = key.getCurvePoint
Comment on lines -101 to -102
Copy link
Contributor Author

Choose a reason for hiding this comment

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

before, the code assumed we had an ECPublicKey which could be decomposed into x and y coordinates. Now we assume it is always a compressed key

def toCompressedECKeyData(key: PublicKeyData): node_models.CompressedECKeyData = {
node_models
.ECKeyData()
.withCurve(ECConfig.getCURVE_NAME)
.withX(ByteString.copyFrom(point.getX.bytes()))
.withY(ByteString.copyFrom(point.getY.bytes()))
.CompressedECKeyData()
.withCurve(key.curveName)
.withData(ByteString.copyFrom(key.compressedKey.toArray))
}

def toProtoKeyUsage(keyUsage: models.KeyUsage): node_models.KeyUsage = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ object ProtocolConstants {
val contextStringCharLimit: Int =
Try(globalConfig.getInt("contextStringCharLimit")).toOption.getOrElse(defaultContextStringCharLength)

val supportedEllipticCurves: Seq[String] = List("secp256k1", "Ed25519", "X25519")
val secpCurveName = "secp256k1"
val ed25519CurveName = "Ed25519"
val x25519CurveName = "X25519"

val supportedEllipticCurves: Seq[String] = List(secpCurveName, ed25519CurveName, x25519CurveName)

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import derevo.derive
import enumeratum.EnumEntry.UpperSnakecase
import enumeratum._
import io.iohk.atala.prism.crypto.Sha256Digest
import io.iohk.atala.prism.crypto.keys.ECPublicKey
import io.iohk.atala.prism.protos.models.TimestampInfo
import io.iohk.atala.prism.protos.node_models
import tofu.logging.derivation.loggable
Expand Down Expand Up @@ -46,7 +45,7 @@ package object models {
didSuffix: DidSuffix,
keyId: String,
keyUsage: KeyUsage,
key: ECPublicKey
key: PublicKeyData
)

case class DIDService(
Expand Down Expand Up @@ -117,13 +116,18 @@ package object models {
ProtocolVersionInfo(ProtocolVersion.InitialProtocolVersion, None, 0)
}

case class PublicKeyData(
curveName: String,
compressedKey: Vector[Byte]
)

object nodeState {

case class DIDPublicKeyState(
didSuffix: DidSuffix,
keyId: String,
keyUsage: KeyUsage,
key: ECPublicKey,
key: PublicKeyData,
addedOn: LedgerData,
revokedOn: Option[LedgerData]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import doobie.free.connection.ConnectionIO
import doobie.implicits._
import doobie.postgres.sqlstate
import io.iohk.atala.prism.crypto.{Sha256, Sha256Digest}
import io.iohk.atala.prism.node.crypto.CryptoUtils
import io.iohk.atala.prism.node.models.DidSuffix
import io.iohk.atala.prism.node.models.KeyUsage.MasterKey
import io.iohk.atala.prism.node.models.nodeState.LedgerData
import io.iohk.atala.prism.node.models.{DIDPublicKey, DIDService, ProtocolConstants}
import io.iohk.atala.prism.node.operations.StateError.{EntityExists, InvalidKeyUsed, UnknownKey}
import io.iohk.atala.prism.node.operations.StateError.{EntityExists, IllegalSecp256k1Key, InvalidKeyUsed, UnknownKey}
import io.iohk.atala.prism.node.operations.path._
import io.iohk.atala.prism.node.repositories.daos.{DIDDataDAO, PublicKeysDAO, ServicesDAO, ContextDAO}
import io.iohk.atala.prism.node.repositories.daos.{ContextDAO, DIDDataDAO, PublicKeysDAO, ServicesDAO}
import io.iohk.atala.prism.protos.{node_models => proto}

import scala.util.Try

case class CreateDIDOperation(
id: DidSuffix,
keys: List[DIDPublicKey],
Expand All @@ -30,14 +33,21 @@ case class CreateDIDOperation(
): EitherT[ConnectionIO, StateError, CorrectnessData] = {
val keyOpt = keys.find(_.keyId == keyId)
for {
_ <- EitherT.fromEither[ConnectionIO] {
key <- EitherT.fromEither[ConnectionIO] {
keyOpt
.filter(_.keyUsage == MasterKey)
.toRight(InvalidKeyUsed("master key"))
}
secpKey <- EitherT.fromEither[ConnectionIO] {
val tryKey = Try {
CryptoUtils.unsafeToSecpPublicKeyFromCompressed(key.key.compressedKey)
}
tryKey.toOption
.toRight(IllegalSecp256k1Key(key.keyId))
}
data <- EitherT.fromEither[ConnectionIO] {
keyOpt
.map(didKey => CorrectnessData(didKey.key, None))
.map(_ => CorrectnessData(secpKey, None))
.toRight(UnknownKey(id, keyId): StateError)
}
} yield data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ package io.iohk.atala.prism.node.operations
import cats.data.EitherT
import doobie.free.connection.{ConnectionIO, unit}
import io.iohk.atala.prism.crypto.{Sha256, Sha256Digest}
import io.iohk.atala.prism.node.crypto.CryptoUtils
import io.iohk.atala.prism.node.models.DidSuffix
import io.iohk.atala.prism.node.models.nodeState.DIDPublicKeyState
import io.iohk.atala.prism.node.models.{KeyUsage, nodeState}
import io.iohk.atala.prism.node.operations.StateError.IllegalSecp256k1Key
import io.iohk.atala.prism.node.operations.path.{Path, ValueAtPath}
import io.iohk.atala.prism.node.repositories.daos.{DIDDataDAO, PublicKeysDAO, ServicesDAO, ContextDAO}
import io.iohk.atala.prism.node.repositories.daos.{ContextDAO, DIDDataDAO, PublicKeysDAO, ServicesDAO}
import io.iohk.atala.prism.protos.node_models.AtalaOperation

import scala.util.Try

case class DeactivateDIDOperation(
didSuffix: DidSuffix,
previousOperation: Sha256Digest,
Expand All @@ -36,7 +40,7 @@ case class DeactivateDIDOperation(
)
)
}
key <- EitherT[ConnectionIO, StateError, DIDPublicKeyState] {
keyData <- EitherT[ConnectionIO, StateError, DIDPublicKeyState] {
PublicKeysDAO
.find(didSuffix, keyId)
.map(_.toRight(StateError.UnknownKey(didSuffix, keyId)))
Expand All @@ -53,7 +57,14 @@ case class DeactivateDIDOperation(
StateError.KeyAlreadyRevoked()
)
}.map(_.key)
} yield CorrectnessData(key, Some(lastOperation))
secpKey <- EitherT.fromEither[ConnectionIO] {
val tryKey = Try {
CryptoUtils.unsafeToSecpPublicKeyFromCompressed(keyData.compressedKey)
}
tryKey.toOption
.toRight(IllegalSecp256k1Key(keyId): StateError)
}
} yield CorrectnessData(secpKey, Some(lastOperation))
}

override protected def applyStateImpl(c: ApplyOperationConfig): EitherT[ConnectionIO, StateError, Unit] = {
Expand Down
Loading
Loading