Skip to content

Commit

Permalink
feat(prism-agent): add support for hierarchical deterministic key wit…
Browse files Browse the repository at this point in the history
…h seed (#534)

* add key derivation

* HD key counter

* derive keys in create-operation

* add random seed

* sanity check

* add sql migration script

* sql schema for did with different key mode

* make wallet api compile

* make server compile

* make tests compile

* fix failing tests

* maxErrors = 5

* Remove unused file

* rand key reuse random seed derivation

* [wip] create new did and increment did index

* [wip] persist derivation path

* fix missing column

* make update did state more strict

* resolve key for different key management mode

* wip

* disallow multiple in-flight update

* Construct update ops with hd key

* hd key timestamp

* Read ekuycount from storage

* key rotation material

* operation handler module

* handle DIDUpdate material

* make update flow use DIDUpdateMaterial

* hd key path table add operation_hash col

* insert hd key path method

* enable key rotation on managed DID

* make walletAPI tests compile

* recover failed tests

* random seed to make tests pass

* wallet seed resolver

* fix test compilation

* SeedResolver tests

* pr cleanup

* operation factory tests
  • Loading branch information
patlo-iog authored May 30, 2023
1 parent fe5920b commit 6129baf
Show file tree
Hide file tree
Showing 27 changed files with 1,415 additions and 304 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
CREATE TYPE public.prism_did_key_mode AS ENUM(
'RANDOM',
'HD'
);

ALTER TABLE public.prism_did_wallet_state
ADD COLUMN "key_mode" PRISM_DID_KEY_MODE,
ADD COLUMN "did_index" INT UNIQUE;

UPDATE public.prism_did_wallet_state
SET "key_mode" = 'RANDOM'
WHERE "key_mode" IS NULL;

ALTER TABLE public.prism_did_wallet_state
ALTER COLUMN "key_mode" SET NOT NULL;

CREATE TYPE public.prism_did_key_usage AS ENUM(
'MASTER',
'ISSUING',
'KEY_AGREEMENT',
'AUTHENTICATION',
'REVOCATION',
'CAPABILITY_INVOCATION',
'CAPABILITY_DELEGATION'
);

CREATE TABLE public.prism_did_hd_key(
"did" TEXT NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"key_id" TEXT NOT NULL,
"key_usage" PRISM_DID_KEY_USAGE,
"key_index" INT,
"operation_hash" BYTEA NOT NULL,
UNIQUE ("did", "key_id", "operation_hash"),
CONSTRAINT fk_did FOREIGN KEY ("did") REFERENCES public.prism_did_wallet_state("did") ON DELETE RESTRICT
);
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import io.iohk.atala.castor.controller.{
}
import io.iohk.atala.agent.walletapi.crypto.Apollo
import io.iohk.atala.system.controller.{SystemController, SystemServerEndpoints}
import io.iohk.atala.agent.walletapi.util.SeedResolver

object Modules {

Expand Down Expand Up @@ -442,7 +443,8 @@ object AppModule {
val manageDIDServiceLayer: TaskLayer[ManagedDIDService] = {
val secretStorageLayer = (RepoModule.agentTransactorLayer ++ apolloLayer) >>> JdbcDIDSecretStorage.layer
val nonSecretStorageLayer = RepoModule.agentTransactorLayer >>> JdbcDIDNonSecretStorage.layer
(didOpValidatorLayer ++ didServiceLayer ++ secretStorageLayer ++ nonSecretStorageLayer ++ apolloLayer) >>> ManagedDIDService.layer
val seedResolverLayer = apolloLayer >>> SeedResolver.layer()
(didOpValidatorLayer ++ didServiceLayer ++ secretStorageLayer ++ nonSecretStorageLayer ++ apolloLayer ++ seedResolverLayer) >>> ManagedDIDService.layer
}

val credentialServiceLayer: RLayer[DidOps & DidAgent & JwtDidResolver, CredentialService] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ object BackgroundJobs {
.mapError(e => RuntimeException(s"Error occurred while getting did from wallet: ${e.toString}"))
.someOrFail(RuntimeException(s"Issuer DID does not exist in the wallet: $did"))
.flatMap {
case s: ManagedDIDState.Published => ZIO.succeed(s)
case s @ ManagedDIDState(_, _, PublicationState.Published(_)) => ZIO.succeed(s)
case s => ZIO.cond(allowUnpublishedIssuingDID, s, RuntimeException(s"Issuer DID must be published: $did"))
}
longFormPrismDID = PrismDID.buildLongFormFromOperation(didState.createOperation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ object DIDRegistrarController {
given Conversion[CreateManagedDIDError, ErrorResponse] = {
case CreateManagedDIDError.InvalidArgument(msg) =>
ErrorResponse.unprocessableEntity(detail = Some(msg))
case CreateManagedDIDError.DIDAlreadyExists(did) =>
ErrorResponse.internalServerError(detail = Some(s"DID already exists: $did"))
case CreateManagedDIDError.KeyGenerationError(e) =>
ErrorResponse.internalServerError(detail = Some(e.toString))
case CreateManagedDIDError.WalletStorageError(e) =>
Expand Down Expand Up @@ -79,6 +77,8 @@ object DIDRegistrarController {
ErrorResponse.conflict(detail = Some(s"DID already deactivated: $did"))
case UpdateManagedDIDError.InvalidArgument(msg) =>
ErrorResponse.badRequest(detail = Some(msg))
case UpdateManagedDIDError.MultipleInflightUpdateNotAllowed(did) =>
ErrorResponse.conflict(detail = Some(s"Multiple in-flight update operations are not allowed: $did"))
case e => ErrorResponse.internalServerError(detail = Some(e.toString))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.iohk.atala.agent.walletapi.model as walletDomain
import io.iohk.atala.agent.walletapi.model.DIDPublicKeyTemplate
import io.iohk.atala.agent.walletapi.model.ManagedDIDDetail
import io.iohk.atala.agent.walletapi.model.ManagedDIDState
import io.iohk.atala.agent.walletapi.model.PublicationState
import io.iohk.atala.api.http.Annotation
import io.iohk.atala.castor.core.model.did as castorDomain
import io.iohk.atala.castor.core.model.did.PrismDID
Expand Down Expand Up @@ -54,11 +55,12 @@ object ManagedDID {
given schema: Schema[ManagedDID] = Schema.derived

given Conversion[ManagedDIDDetail, ManagedDID] = { didDetail =>
val (longFormDID, status) = didDetail.state match {
case ManagedDIDState.Created(operation) => Some(PrismDID.buildLongFormFromOperation(operation)) -> "CREATED"
case ManagedDIDState.PublicationPending(operation, _) =>
val operation = didDetail.state.createOperation
val (longFormDID, status) = didDetail.state.publicationState match {
case PublicationState.Created() => Some(PrismDID.buildLongFormFromOperation(operation)) -> "CREATED"
case PublicationState.PublicationPending(_) =>
Some(PrismDID.buildLongFormFromOperation(operation)) -> "PUBLICATION_PENDING"
case ManagedDIDState.Published(_, _) => None -> "PUBLISHED"
case PublicationState.Published(_) => None -> "PUBLISHED"
}
ManagedDID(
did = didDetail.did.toString,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,21 @@ trait ECPrivateKey {
def sign(data: Array[Byte]): Try[Array[Byte]]
def encode: Array[Byte]
def computePublicKey: ECPublicKey
override final def toString(): String = "**********"
}

trait ECKeyFactory {
def publicKeyFromCoordinate(curve: EllipticCurve, x: BigInt, y: BigInt): Try[ECPublicKey]
def publicKeyFromEncoded(curve: EllipticCurve, bytes: Array[Byte]): Try[ECPublicKey]
def privateKeyFromEncoded(curve: EllipticCurve, bytes: Array[Byte]): Try[ECPrivateKey]
def generateKeyPair(curve: EllipticCurve): Task[ECKeyPair]
def deriveKeyPair(curve: EllipticCurve, seed: Array[Byte])(path: DerivationPath*): Task[ECKeyPair]
def randomBip32Seed(): Task[Array[Byte]]
}

enum DerivationPath {
case Normal(i: Int) extends DerivationPath
case Hardened(i: Int) extends DerivationPath
}

trait Apollo {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import java.security.spec.{ECPrivateKeySpec, ECPublicKeySpec}
import io.iohk.atala.agent.walletapi.util.Prism14CompatUtil.*
import io.iohk.atala.prism.crypto.EC
import zio.*
import io.iohk.atala.prism.crypto.derivation.KeyDerivation
import io.iohk.atala.prism.crypto.derivation.DerivationAxis

final case class Prism14ECPublicKey(publicKey: io.iohk.atala.prism.crypto.keys.ECPublicKey) extends ECPublicKey {

Expand Down Expand Up @@ -121,6 +123,32 @@ object Prism14ECKeyFactory extends ECKeyFactory {
}
}

override def deriveKeyPair(curve: EllipticCurve, seed: Array[Byte])(path: DerivationPath*): Task[ECKeyPair] = {
curve match {
case EllipticCurve.SECP256K1 =>
ZIO.attempt {
val extendedKey = path
.foldLeft(KeyDerivation.INSTANCE.derivationRoot(seed)) { case (extendedKey, p) =>
val axis = p match {
case DerivationPath.Hardened(i) => DerivationAxis.hardened(i)
case DerivationPath.Normal(i) => DerivationAxis.normal(i)
}
extendedKey.derive(axis)
}
val prism14KeyPair = extendedKey.keyPair()
ECKeyPair(
Prism14ECPublicKey(prism14KeyPair.getPublicKey()),
Prism14ECPrivateKey(prism14KeyPair.getPrivateKey())
)
}
}
}

override def randomBip32Seed(): Task[Array[Byte]] = ZIO.attempt {
val mnemonic = KeyDerivation.INSTANCE.randomMnemonicCode()
KeyDerivation.INSTANCE.binarySeed(mnemonic, "")
}

}

object Prism14Apollo extends Apollo {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package io.iohk.atala.agent.walletapi.model

import io.iohk.atala.castor.core.model.did.VerificationRelationship
import io.iohk.atala.castor.core.model.did.InternalKeyPurpose
import io.iohk.atala.agent.walletapi.crypto.DerivationPath
import io.iohk.atala.agent.walletapi.crypto.ECKeyPair

enum KeyManagementMode {
case Random extends KeyManagementMode
case HD extends KeyManagementMode
}

final case class VerificationRelationshipCounter(
authentication: Int,
assertionMethod: Int,
keyAgreement: Int,
capabilityInvocation: Int,
capabilityDelegation: Int,
) {
def next(keyUsage: VerificationRelationship): VerificationRelationshipCounter = {
keyUsage match {
case VerificationRelationship.Authentication => copy(authentication = authentication + 1)
case VerificationRelationship.AssertionMethod => copy(assertionMethod = assertionMethod + 1)
case VerificationRelationship.KeyAgreement => copy(keyAgreement = keyAgreement + 1)
case VerificationRelationship.CapabilityInvocation => copy(capabilityInvocation = capabilityInvocation + 1)
case VerificationRelationship.CapabilityDelegation => copy(capabilityDelegation = capabilityDelegation + 1)
}
}
}

object VerificationRelationshipCounter {
def zero: VerificationRelationshipCounter = VerificationRelationshipCounter(0, 0, 0, 0, 0)
}

final case class InternalKeyCounter(
master: Int,
revocation: Int
) {
def next(keyUsage: InternalKeyPurpose): InternalKeyCounter = {
keyUsage match {
case InternalKeyPurpose.Master => copy(master = master + 1)
case InternalKeyPurpose.Revocation => copy(revocation = revocation + 1)
}
}
}

object InternalKeyCounter {
def zero: InternalKeyCounter = InternalKeyCounter(0, 0)
}

/** Key counter of a single DID */
final case class HdKeyIndexCounter(
didIndex: Int,
verificationRelationship: VerificationRelationshipCounter,
internalKey: InternalKeyCounter
) {
def next(keyUsage: VerificationRelationship | InternalKeyPurpose): HdKeyIndexCounter = {
keyUsage match {
case i: VerificationRelationship => copy(verificationRelationship = verificationRelationship.next(i))
case i: InternalKeyPurpose => copy(internalKey = internalKey.next(i))
}
}

def path(keyUsage: VerificationRelationship | InternalKeyPurpose): ManagedDIDHdKeyPath = {
val keyIndex = keyUsage match {
case VerificationRelationship.AssertionMethod => verificationRelationship.assertionMethod
case VerificationRelationship.KeyAgreement => verificationRelationship.keyAgreement
case VerificationRelationship.CapabilityInvocation => verificationRelationship.capabilityInvocation
case VerificationRelationship.CapabilityDelegation => verificationRelationship.capabilityDelegation
case VerificationRelationship.Authentication => verificationRelationship.authentication
case InternalKeyPurpose.Master => internalKey.master
case InternalKeyPurpose.Revocation => internalKey.revocation
}
ManagedDIDHdKeyPath(didIndex, keyUsage, keyIndex)
}
}

object HdKeyIndexCounter {
def zero(didIndex: Int): HdKeyIndexCounter =
HdKeyIndexCounter(didIndex, VerificationRelationshipCounter.zero, InternalKeyCounter.zero)
}

final case class ManagedDIDHdKeyPath(
didIndex: Int,
keyUsage: VerificationRelationship | InternalKeyPurpose,
keyIndex: Int
) {
def derivationPath: Seq[DerivationPath] =
Seq(
DerivationPath.Hardened(0x1d),
DerivationPath.Hardened(didIndex),
DerivationPath.Hardened(keyUsageIndex),
DerivationPath.Hardened(keyIndex)
)

def keyUsageIndex: Int = mapKeyUsageIndex(keyUsage)

private def mapKeyUsageIndex(keyUsage: VerificationRelationship | InternalKeyPurpose): Int = {
keyUsage match {
case InternalKeyPurpose.Master => 1
case VerificationRelationship.AssertionMethod => 2
case VerificationRelationship.KeyAgreement => 3
case VerificationRelationship.Authentication => 4
case InternalKeyPurpose.Revocation => 5
case VerificationRelationship.CapabilityInvocation => 6
case VerificationRelationship.CapabilityDelegation => 7
}
}
}

private[walletapi] final case class CreateDIDRandKey(
keyPairs: Map[String, ECKeyPair],
internalKeyPairs: Map[String, ECKeyPair]
)

private[walletapi] final case class UpdateDIDRandKey(newKeyPairs: Map[String, ECKeyPair])

private[walletapi] final case class CreateDIDHdKey(
keyPaths: Map[String, ManagedDIDHdKeyPath],
internalKeyPaths: Map[String, ManagedDIDHdKeyPath],
)

private[walletapi] final case class UpdateDIDHdKey(
newKeyPaths: Map[String, ManagedDIDHdKeyPath],
counter: HdKeyIndexCounter
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,23 @@ import scala.collection.immutable.ArraySeq

final case class ManagedDIDDetail(did: CanonicalPrismDID, state: ManagedDIDState)

sealed trait ManagedDIDState {
def createOperation: PrismDIDOperation.Create
final case class ManagedDIDState(
createOperation: PrismDIDOperation.Create,
didIndex: Option[Int],
publicationState: PublicationState
) {
def keyMode: KeyManagementMode = didIndex match {
case Some(_) => KeyManagementMode.HD
case None => KeyManagementMode.Random
}
}

object ManagedDIDState {
final case class Created(createOperation: PrismDIDOperation.Create) extends ManagedDIDState
final case class PublicationPending(createOperation: PrismDIDOperation.Create, publishOperationId: ArraySeq[Byte])
extends ManagedDIDState
final case class Published(createOperation: PrismDIDOperation.Create, publishOperationId: ArraySeq[Byte])
extends ManagedDIDState
sealed trait PublicationState

object PublicationState {
final case class Created() extends PublicationState
final case class PublicationPending(publishOperationId: ArraySeq[Byte]) extends PublicationState
final case class Published(publishOperationId: ArraySeq[Byte]) extends PublicationState
}

final case class DIDUpdateLineage(
Expand All @@ -27,3 +34,5 @@ final case class DIDUpdateLineage(
createdAt: Instant,
updatedAt: Instant
)

final case class ManagedDIDStatePatch(publicationState: PublicationState)
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package io.iohk.atala.agent.walletapi.model.error

import io.iohk.atala.castor.core.model.did.PrismDID
import io.iohk.atala.castor.core.model.error as castor

sealed trait CreateManagedDIDError extends Throwable

object CreateManagedDIDError {
final case class InvalidArgument(msg: String) extends CreateManagedDIDError
final case class DIDAlreadyExists(did: PrismDID) extends CreateManagedDIDError
final case class KeyGenerationError(cause: Throwable) extends CreateManagedDIDError
final case class WalletStorageError(cause: Throwable) extends CreateManagedDIDError
final case class InvalidOperation(cause: castor.OperationValidationError) extends CreateManagedDIDError
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ object UpdateManagedDIDError {
final case class InvalidOperation(cause: castor.OperationValidationError) extends UpdateManagedDIDError
final case class ResolutionError(cause: castor.DIDResolutionError) extends UpdateManagedDIDError
final case class CryptographyError(cause: Throwable) extends UpdateManagedDIDError
final case class MultipleInflightUpdateNotAllowed(did: CanonicalPrismDID) extends UpdateManagedDIDError
final case class DataIntegrityError(msg: String) extends UpdateManagedDIDError
}
Loading

0 comments on commit 6129baf

Please sign in to comment.