From 6129baf1210b68decc4f264bd4a64b4009719956 Mon Sep 17 00:00:00 2001 From: patlo-iog <108713642+patlo-iog@users.noreply.github.com> Date: Tue, 30 May 2023 11:10:15 +0700 Subject: [PATCH] feat(prism-agent): add support for hierarchical deterministic key with 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 --- .../resources/sql/agent/V4__did_hd_key.sql | 36 +++ .../io/iohk/atala/agent/server/Modules.scala | 4 +- .../agent/server/jobs/BackgroundJobs.scala | 2 +- .../controller/DIDRegistrarController.scala | 4 +- .../castor/controller/http/ManagedDID.scala | 10 +- .../atala/agent/walletapi/crypto/Apollo.scala | 8 + .../walletapi/crypto/Prism14Apollo.scala | 28 +++ .../agent/walletapi/model/KeyManagement.scala | 126 ++++++++++ .../agent/walletapi/model/ManagedDID.scala | 25 +- .../model/error/CreateManagedDIDError.scala | 2 - .../model/error/ManagedDIDServiceError.scala | 9 - .../model/error/UpdateManagedDIDError.scala | 2 + .../walletapi/service/ManagedDIDService.scala | 220 ++++++++---------- .../service/handler/DIDUpdateHandler.scala | 131 +++++++++++ .../service/handler/PublicationHandler.scala | 45 ++++ .../sql/JdbcDIDNonSecretStorage.scala | 220 ++++++++++++++++-- .../atala/agent/walletapi/sql/package.scala | 117 +++++++--- .../storage/DIDNonSecretStorage.scala | 24 +- .../agent/walletapi/util/KeyResolver.scala | 38 +++ .../walletapi/util/OperationFactory.scala | 200 +++++++++++----- .../agent/walletapi/util/SeedResolver.scala | 58 +++++ .../agent/walletapi/crypto/ApolloSpec.scala | 100 +++++++- .../service/ManagedDIDServiceSpec.scala | 40 ++-- .../storage/JdbcDIDNonSecretStorageSpec.scala | 61 ++--- .../walletapi/storage/StorageSpecHelper.scala | 18 +- .../walletapi/util/OperationFactorySpec.scala | 131 +++++++++++ .../walletapi/util/SeedResolverSpec.scala | 60 +++++ 27 files changed, 1415 insertions(+), 304 deletions(-) create mode 100644 prism-agent/service/server/src/main/resources/sql/agent/V4__did_hd_key.sql create mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/KeyManagement.scala delete mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/ManagedDIDServiceError.scala create mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/DIDUpdateHandler.scala create mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/PublicationHandler.scala create mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/KeyResolver.scala create mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/SeedResolver.scala create mode 100644 prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/util/OperationFactorySpec.scala create mode 100644 prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/util/SeedResolverSpec.scala diff --git a/prism-agent/service/server/src/main/resources/sql/agent/V4__did_hd_key.sql b/prism-agent/service/server/src/main/resources/sql/agent/V4__did_hd_key.sql new file mode 100644 index 0000000000..f33f9762dd --- /dev/null +++ b/prism-agent/service/server/src/main/resources/sql/agent/V4__did_hd_key.sql @@ -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 +); diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala index 8f25786853..3ea46c2de3 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala @@ -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 { @@ -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] = diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala index cd60dd808e..c2365364ba 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala @@ -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) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/DIDRegistrarController.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/DIDRegistrarController.scala index bf5cc2799f..8f695f8425 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/DIDRegistrarController.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/DIDRegistrarController.scala @@ -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) => @@ -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)) } } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/http/ManagedDID.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/http/ManagedDID.scala index ec2488c433..5ede78eced 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/http/ManagedDID.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/castor/controller/http/ManagedDID.scala @@ -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 @@ -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, diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/crypto/Apollo.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/crypto/Apollo.scala index d06b01387a..f3d1eba98d 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/crypto/Apollo.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/crypto/Apollo.scala @@ -21,6 +21,7 @@ trait ECPrivateKey { def sign(data: Array[Byte]): Try[Array[Byte]] def encode: Array[Byte] def computePublicKey: ECPublicKey + override final def toString(): String = "**********" } trait ECKeyFactory { @@ -28,6 +29,13 @@ trait ECKeyFactory { 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 { diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/crypto/Prism14Apollo.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/crypto/Prism14Apollo.scala index bc92cef660..93fb7b5790 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/crypto/Prism14Apollo.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/crypto/Prism14Apollo.scala @@ -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 { @@ -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 { diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/KeyManagement.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/KeyManagement.scala new file mode 100644 index 0000000000..22872925ad --- /dev/null +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/KeyManagement.scala @@ -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 +) diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/ManagedDID.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/ManagedDID.scala index 8603b92b0f..fd0c6ebdac 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/ManagedDID.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/ManagedDID.scala @@ -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( @@ -27,3 +34,5 @@ final case class DIDUpdateLineage( createdAt: Instant, updatedAt: Instant ) + +final case class ManagedDIDStatePatch(publicationState: PublicationState) diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/CreateManagedDIDError.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/CreateManagedDIDError.scala index b8adf1fe47..57cbca9269 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/CreateManagedDIDError.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/CreateManagedDIDError.scala @@ -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 diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/ManagedDIDServiceError.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/ManagedDIDServiceError.scala deleted file mode 100644 index 4c2f3418bb..0000000000 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/ManagedDIDServiceError.scala +++ /dev/null @@ -1,9 +0,0 @@ -package io.iohk.atala.agent.walletapi.model.error - -import io.iohk.atala.mercury.model.DidId - -sealed trait ManagedDIDServiceError extends Throwable - -object ManagedDIDServiceError { - case class PeerDIDNotFoundError(didId: DidId) extends ManagedDIDServiceError -} diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/UpdateManagedDIDError.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/UpdateManagedDIDError.scala index 69a1aef4fb..9dbef5410b 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/UpdateManagedDIDError.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/UpdateManagedDIDError.scala @@ -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 } diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDService.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDService.scala index 1b6c7b4543..d3bfb0eaa0 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDService.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDService.scala @@ -6,7 +6,9 @@ import io.iohk.atala.agent.walletapi.model.{ ManagedDIDDetail, ManagedDIDState, ManagedDIDTemplate, - UpdateManagedDIDAction + UpdateManagedDIDAction, + ManagedDIDStatePatch, + PublicationState } import io.iohk.atala.agent.walletapi.model.error.{*, given} import io.iohk.atala.agent.walletapi.service.ManagedDIDService.DEFAULT_MASTER_KEY_ID @@ -14,7 +16,6 @@ import io.iohk.atala.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStor import io.iohk.atala.agent.walletapi.util.{ ManagedDIDTemplateValidator, OperationFactory, - UpdateDIDSecret, UpdateManagedDIDActionValidator } import io.iohk.atala.castor.core.model.did.{ @@ -26,7 +27,6 @@ import io.iohk.atala.castor.core.model.did.{ PrismDIDOperation, ScheduleDIDOperationOutcome, ScheduledDIDOperationStatus, - SignedPrismDIDOperation } import io.iohk.atala.castor.core.model.error.DIDOperationError import io.iohk.atala.castor.core.service.DIDService @@ -38,6 +38,9 @@ import io.iohk.atala.mercury.PeerDID import io.iohk.atala.mercury.model.DidId import java.security.{PrivateKey as JavaPrivateKey, PublicKey as JavaPublicKey} +import io.iohk.atala.agent.walletapi.util.KeyResolver +import io.iohk.atala.agent.walletapi.service.handler.{DIDUpdateHandler, PublicationHandler} +import io.iohk.atala.agent.walletapi.util.SeedResolver /** A wrapper around Castor's DIDService providing key-management capability. Analogous to the secretAPI in * indy-wallet-sdk. @@ -47,20 +50,21 @@ final class ManagedDIDService private[walletapi] ( didOpValidator: DIDOperationValidator, private[walletapi] val secretStorage: DIDSecretStorage, private[walletapi] val nonSecretStorage: DIDNonSecretStorage, - apollo: Apollo + apollo: Apollo, + seed: Array[Byte] ) { private val CURVE = EllipticCurve.SECP256K1 private val AGREEMENT_KEY_ID = "agreement" private val AUTHENTICATION_KEY_ID = "authentication" - private val generateCreateOperation = OperationFactory.makeCreateOperation( - DEFAULT_MASTER_KEY_ID, - () => apollo.ecKeyFactory.generateKeyPair(CURVE) - ) + private val keyResolver = KeyResolver(apollo, nonSecretStorage, secretStorage)(seed) - private val generateUpdateOperation = - OperationFactory.makeUpdateOperation(() => apollo.ecKeyFactory.generateKeyPair(CURVE)) + private val publicationHandler = PublicationHandler(didService, keyResolver)(DEFAULT_MASTER_KEY_ID) + private val didUpdateHandler = DIDUpdateHandler(apollo, nonSecretStorage, secretStorage, publicationHandler)(seed) + + private val generateCreateOperationHdKey = + OperationFactory(apollo).makeCreateOperationHdKey(DEFAULT_MASTER_KEY_ID, seed) def syncManagedDIDState: IO[GetManagedDIDError, Unit] = nonSecretStorage .listManagedDID(offset = None, limit = None) @@ -81,8 +85,12 @@ final class ManagedDIDService private[walletapi] ( did: CanonicalPrismDID, keyId: String ): IO[GetKeyError, Option[(JavaPrivateKey, JavaPublicKey)]] = { - secretStorage - .getKey(did, keyId) + nonSecretStorage + .getManagedDIDState(did) + .flatMap { + case None => ZIO.none + case Some(state) => keyResolver.getKey(state.createOperation.did, state.keyMode, keyId) + } .mapBoth( GetKeyError.WalletStorageError.apply, _.map { ecKeyPair => @@ -108,13 +116,16 @@ final class ManagedDIDService private[walletapi] ( details = dids.map { case (did, state) => ManagedDIDDetail(did.asCanonical, state) } } yield details -> totalCount + // TODO: update this method to use the same handler as updateManagedDID def publishStoredDID(did: CanonicalPrismDID): IO[PublishManagedDIDError, ScheduleDIDOperationOutcome] = { - def doPublish(operation: PrismDIDOperation.Create) = { + def doPublish(state: ManagedDIDState) = { for { - signedOperation <- signOperationWithMasterKey[PublishManagedDIDError](operation) - outcome <- submitSignedOperation[PublishManagedDIDError](signedOperation) + signedOperation <- publicationHandler + .signOperationWithMasterKey[PublishManagedDIDError](state, state.createOperation) + outcome <- publicationHandler.submitSignedOperation[PublishManagedDIDError](signedOperation) + publicationState = PublicationState.PublicationPending(outcome.operationId) _ <- nonSecretStorage - .setManagedDIDState(did, ManagedDIDState.PublicationPending(operation, outcome.operationId)) + .updateManagedDID(did, ManagedDIDStatePatch(publicationState)) .mapError(PublishManagedDIDError.WalletStorageError.apply) } yield outcome } @@ -125,42 +136,38 @@ final class ManagedDIDService private[walletapi] ( .getManagedDIDState(did) .mapError(PublishManagedDIDError.WalletStorageError.apply) .someOrFail(PublishManagedDIDError.DIDNotFound(did)) - outcome <- didState match { - case ManagedDIDState.Created(operation) => doPublish(operation) - case ManagedDIDState.PublicationPending(operation, publishOperationId) => - ZIO.succeed(ScheduleDIDOperationOutcome(did, operation, publishOperationId)) - case ManagedDIDState.Published(operation, publishOperationId) => - ZIO.succeed(ScheduleDIDOperationOutcome(did, operation, publishOperationId)) + outcome <- didState.publicationState match { + case PublicationState.Created() => doPublish(didState) + case PublicationState.PublicationPending(publishOperationId) => + ZIO.succeed(ScheduleDIDOperationOutcome(did, didState.createOperation, publishOperationId)) + case PublicationState.Published(publishOperationId) => + ZIO.succeed(ScheduleDIDOperationOutcome(did, didState.createOperation, publishOperationId)) } } yield outcome } + // TODO: update this method to use the same handler as updateManagedDID def createAndStoreDID(didTemplate: ManagedDIDTemplate): IO[CreateManagedDIDError, LongFormPrismDID] = { for { _ <- ZIO .fromEither(ManagedDIDTemplateValidator.validate(didTemplate)) .mapError(CreateManagedDIDError.InvalidArgument.apply) - generated <- generateCreateOperation(didTemplate) - (createOperation, secret) = generated + didIndex <- nonSecretStorage + .getMaxDIDIndex() + .mapBoth( + CreateManagedDIDError.WalletStorageError.apply, + maybeIdx => maybeIdx.map(_ + 1).getOrElse(0) + ) + generated <- generateCreateOperationHdKey(didIndex, didTemplate) + (createOperation, hdKey) = generated longFormDID = PrismDID.buildLongFormFromOperation(createOperation) did = longFormDID.asCanonical _ <- ZIO .fromEither(didOpValidator.validate(createOperation)) .mapError(CreateManagedDIDError.InvalidOperation.apply) + state = ManagedDIDState(createOperation, Some(didIndex), PublicationState.Created()) _ <- nonSecretStorage - .getManagedDIDState(did) - .mapError(CreateManagedDIDError.WalletStorageError.apply) - .filterOrFail(_.isEmpty)(CreateManagedDIDError.DIDAlreadyExists(did)) - _ <- ZIO - .foreachDiscard(secret.keyPairs ++ secret.internalKeyPairs) { case (keyId, keyPair) => - secretStorage.insertKey(did, keyId, keyPair, createOperation.toAtalaOperationHash) - } - .mapError(CreateManagedDIDError.WalletStorageError.apply) - // A DID is considered created after a successful setState - // If some steps above failed, it is not considered created and data that - // are persisted along the way may be garbage collected. - _ <- nonSecretStorage - .setManagedDIDState(did, ManagedDIDState.Created(createOperation)) + .insertManagedDID(did, state, hdKey.keyPaths ++ hdKey.internalKeyPaths) .mapError(CreateManagedDIDError.WalletStorageError.apply) } yield longFormDID } @@ -169,32 +176,6 @@ final class ManagedDIDService private[walletapi] ( did: CanonicalPrismDID, actions: Seq[UpdateManagedDIDAction] ): IO[UpdateManagedDIDError, ScheduleDIDOperationOutcome] = { - def doUpdate(operation: PrismDIDOperation.Update, secret: UpdateDIDSecret) = { - val operationHash = operation.toAtalaOperationHash - for { - signedOperation <- signOperationWithMasterKey[UpdateManagedDIDError](operation) - updateLineage <- Clock.instant.map { now => - DIDUpdateLineage( - operationId = ArraySeq.from(signedOperation.toAtalaOperationId), - operationHash = ArraySeq.from(operation.toAtalaOperationHash), - previousOperationHash = operation.previousOperationHash, - status = ScheduledDIDOperationStatus.Pending, - createdAt = now, - updatedAt = now - ) - } - _ <- ZIO - .foreachDiscard(secret.newKeyPairs) { case (keyId, keyPair) => - secretStorage.insertKey(did, keyId, keyPair, operationHash) - } - .mapError(UpdateManagedDIDError.WalletStorageError.apply) - _ <- nonSecretStorage - .insertDIDUpdateLineage(did, updateLineage) - .mapError(UpdateManagedDIDError.WalletStorageError.apply) - outcome <- submitSignedOperation[UpdateManagedDIDError](signedOperation) - } yield outcome - } - for { _ <- ZIO .fromEither(UpdateManagedDIDActionValidator.validate(actions)) @@ -204,7 +185,9 @@ final class ManagedDIDService private[walletapi] ( .getManagedDIDState(did) .mapError(UpdateManagedDIDError.WalletStorageError.apply) .someOrFail(UpdateManagedDIDError.DIDNotFound(did)) - .collect(UpdateManagedDIDError.DIDNotPublished(did)) { case s: ManagedDIDState.Published => s } + .collect(UpdateManagedDIDError.DIDNotPublished(did)) { + case s @ ManagedDIDState(_, _, PublicationState.Published(_)) => s + } resolvedDID <- didService .resolveDID(did) .mapError(UpdateManagedDIDError.ResolutionError.apply) @@ -215,20 +198,23 @@ final class ManagedDIDService private[walletapi] ( resolvedDID._1, didState.createOperation ) - generated <- generateUpdateOperation(did, previousOperationHash, actions) - (updateOperation, secret) = generated + _ <- getUnconfirmedUpdateOperationByDid[UpdateManagedDIDError](Some(did)) + .filterOrFail(_.isEmpty)(UpdateManagedDIDError.MultipleInflightUpdateNotAllowed(did)) + material <- didUpdateHandler.materialize(didState, previousOperationHash, actions) _ <- ZIO - .fromEither(didOpValidator.validate(updateOperation)) + .fromEither(didOpValidator.validate(material.operation)) .mapError(UpdateManagedDIDError.InvalidOperation.apply) - outcome <- doUpdate(updateOperation, secret) + _ <- material.persist.mapError(UpdateManagedDIDError.WalletStorageError.apply) + outcome <- publicationHandler.submitSignedOperation[UpdateManagedDIDError](material.signedOperation) } yield outcome } + // TODO: refactor this method to use the same handler as updateManagedDID def deactivateManagedDID(did: CanonicalPrismDID): IO[UpdateManagedDIDError, ScheduleDIDOperationOutcome] = { - def doDeactivate(operation: PrismDIDOperation.Deactivate) = { + def doDeactivate(state: ManagedDIDState, operation: PrismDIDOperation.Deactivate) = { for { - signedOperation <- signOperationWithMasterKey[UpdateManagedDIDError](operation) - outcome <- submitSignedOperation[UpdateManagedDIDError](signedOperation) + signedOperation <- publicationHandler.signOperationWithMasterKey[UpdateManagedDIDError](state, operation) + outcome <- publicationHandler.submitSignedOperation[UpdateManagedDIDError](signedOperation) } yield outcome } @@ -238,7 +224,9 @@ final class ManagedDIDService private[walletapi] ( .getManagedDIDState(did) .mapError(UpdateManagedDIDError.WalletStorageError.apply) .someOrFail(UpdateManagedDIDError.DIDNotFound(did)) - .collect(UpdateManagedDIDError.DIDNotPublished(did)) { case s: ManagedDIDState.Published => s } + .collect(UpdateManagedDIDError.DIDNotPublished(did)) { + case s @ ManagedDIDState(_, _, PublicationState.Published(_)) => s + } resolvedDID <- didService .resolveDID(did) .mapError(UpdateManagedDIDError.ResolutionError.apply) @@ -249,11 +237,13 @@ final class ManagedDIDService private[walletapi] ( resolvedDID._1, didState.createOperation ) + _ <- getUnconfirmedUpdateOperationByDid[UpdateManagedDIDError](Some(did)) + .filterOrFail(_.isEmpty)(UpdateManagedDIDError.MultipleInflightUpdateNotAllowed(did)) deactivateOperation = PrismDIDOperation.Deactivate(did, ArraySeq.from(previousOperationHash)) _ <- ZIO .fromEither(didOpValidator.validate(deactivateOperation)) .mapError(UpdateManagedDIDError.InvalidOperation.apply) - outcome <- doDeactivate(deactivateOperation) + outcome <- doDeactivate(didState, deactivateOperation) } yield outcome } @@ -283,6 +273,16 @@ final class ManagedDIDService private[walletapi] ( private def syncUnconfirmedUpdateOperationsByDID[E]( did: Option[PrismDID] )(using c1: Conversion[CommonWalletStorageError, E], c2: Conversion[DIDOperationError, E]): IO[E, Unit] = { + for { + unconfirmedOps <- getUnconfirmedUpdateOperationByDid(did) + _ <- ZIO.foreach(unconfirmedOps)(computeNewDIDLineageStatusAndPersist[E]) + } yield () + } + + private def getUnconfirmedUpdateOperationByDid[E](did: Option[PrismDID])(using + c1: Conversion[CommonWalletStorageError, E], + c2: Conversion[DIDOperationError, E] + ): IO[E, Seq[DIDUpdateLineage]] = { for { awaitingConfirmationOps <- nonSecretStorage .listUpdateLineage(did = did, status = Some(ScheduledDIDOperationStatus.AwaitingConfirmation)) @@ -290,41 +290,9 @@ final class ManagedDIDService private[walletapi] ( pendingOps <- nonSecretStorage .listUpdateLineage(did = did, status = Some(ScheduledDIDOperationStatus.Pending)) .mapError[E](CommonWalletStorageError.apply) - _ <- ZIO.foreach(awaitingConfirmationOps ++ pendingOps)(computeNewDIDLineageStatusAndPersist[E]) - } yield () - } - - private def signOperationWithMasterKey[E](operation: PrismDIDOperation)(using - c1: Conversion[CommonWalletStorageError, E], - c2: Conversion[CommonCryptographyError, E] - ): IO[E, SignedPrismDIDOperation] = { - val did = operation.did - for { - masterKeyPair <- - secretStorage - .getKey(did, DEFAULT_MASTER_KEY_ID) - .mapError[E](CommonWalletStorageError.apply) - .someOrElseZIO( - ZIO.die(Exception("master-key must exists in the wallet for signing DID operation and submit to Node")) - ) - signedOperation <- ZIO - .fromTry(masterKeyPair.privateKey.sign(operation.toAtalaOperation.toByteArray)) - .mapError[E](CommonCryptographyError.apply) - .map(signature => - SignedPrismDIDOperation( - operation = operation, - signature = ArraySeq.from(signature), - signedWithKey = DEFAULT_MASTER_KEY_ID - ) - ) - } yield signedOperation + } yield awaitingConfirmationOps ++ pendingOps } - private def submitSignedOperation[E]( - signedOperation: SignedPrismDIDOperation - )(using c1: Conversion[DIDOperationError, E]): IO[E, ScheduleDIDOperationOutcome] = - didService.scheduleOperation(signedOperation).mapError[E](e => e) - private def computeNewDIDLineageStatusAndPersist[E]( updateLineage: DIDUpdateLineage )(using c1: Conversion[DIDOperationError, E], c2: Conversion[CommonWalletStorageError, E]): IO[E, Unit] = { @@ -351,20 +319,22 @@ final class ManagedDIDService private[walletapi] ( maybeCurrentState <- nonSecretStorage .getManagedDIDState(did) .mapError[E](CommonWalletStorageError.apply) - maybeNewState <- ZIO.foreach(maybeCurrentState)(computeNewDIDStateFromDLT(_).mapError[E](e => e)) - _ <- ZIO.foreach(maybeCurrentState zip maybeNewState) { case (currentState, newState) => + maybeNewPubState <- ZIO + .foreach(maybeCurrentState)(i => computeNewDIDStateFromDLT(i.publicationState)) + .mapError[E](e => e) + _ <- ZIO.foreach(maybeCurrentState zip maybeNewPubState) { case (currentState, newPubState) => nonSecretStorage - .setManagedDIDState(did, newState) + .updateManagedDID(did, ManagedDIDStatePatch(newPubState)) .mapError[E](CommonWalletStorageError.apply) - .when(currentState != newState) + .when(currentState.publicationState != newPubState) } } yield () } /** Reconcile state with DLT and return an updated state */ - private def computeNewDIDStateFromDLT(state: ManagedDIDState): IO[DIDOperationError, ManagedDIDState] = { - state match { - case s @ ManagedDIDState.PublicationPending(operation, publishOperationId) => + private def computeNewDIDStateFromDLT(publicationState: PublicationState): IO[DIDOperationError, PublicationState] = { + publicationState match { + case s @ PublicationState.PublicationPending(publishOperationId) => didService .getScheduledDIDOperationDetail(publishOperationId.toArray) .map { @@ -372,10 +342,10 @@ final class ManagedDIDService private[walletapi] ( result.status match { case ScheduledDIDOperationStatus.Pending => s case ScheduledDIDOperationStatus.AwaitingConfirmation => s - case ScheduledDIDOperationStatus.Confirmed => ManagedDIDState.Published(operation, publishOperationId) - case ScheduledDIDOperationStatus.Rejected => ManagedDIDState.Created(operation) + case ScheduledDIDOperationStatus.Confirmed => PublicationState.Published(publishOperationId) + case ScheduledDIDOperationStatus.Rejected => PublicationState.Created() } - case None => ManagedDIDState.Created(operation) + case None => PublicationState.Created() } case s => ZIO.succeed(s) } @@ -411,10 +381,20 @@ object ManagedDIDService { val reservedKeyIds: Set[String] = Set(DEFAULT_MASTER_KEY_ID) - val layer: URLayer[ - DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & Apollo, + val layer: RLayer[ + DIDOperationValidator & DIDService & DIDSecretStorage & DIDNonSecretStorage & Apollo & SeedResolver, ManagedDIDService - ] = - ZLayer.fromFunction(ManagedDIDService(_, _, _, _, _)) + ] = { + ZLayer.fromZIO { + for { + didService <- ZIO.service[DIDService] + didOpValidator <- ZIO.service[DIDOperationValidator] + secretStorage <- ZIO.service[DIDSecretStorage] + nonSecretStorage <- ZIO.service[DIDNonSecretStorage] + apollo <- ZIO.service[Apollo] + seed <- ZIO.serviceWithZIO[SeedResolver](_.resolve) + } yield ManagedDIDService(didService, didOpValidator, secretStorage, nonSecretStorage, apollo, seed) + } + } } diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/DIDUpdateHandler.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/DIDUpdateHandler.scala new file mode 100644 index 0000000000..8efe2ac2e5 --- /dev/null +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/DIDUpdateHandler.scala @@ -0,0 +1,131 @@ +package io.iohk.atala.agent.walletapi.service.handler + +import zio.* +import io.iohk.atala.agent.walletapi.crypto.Apollo +import io.iohk.atala.agent.walletapi.model.{UpdateDIDRandKey, UpdateDIDHdKey} +import io.iohk.atala.agent.walletapi.model.DIDUpdateLineage +import io.iohk.atala.agent.walletapi.model.error.{*, given} +import io.iohk.atala.agent.walletapi.model.error.UpdateManagedDIDError +import io.iohk.atala.agent.walletapi.model.KeyManagementMode +import io.iohk.atala.agent.walletapi.model.ManagedDIDState +import io.iohk.atala.agent.walletapi.model.UpdateManagedDIDAction +import io.iohk.atala.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage} +import io.iohk.atala.agent.walletapi.util.OperationFactory +import io.iohk.atala.castor.core.model.did.PrismDIDOperation +import io.iohk.atala.castor.core.model.did.PrismDIDOperation.Update +import io.iohk.atala.castor.core.model.did.ScheduledDIDOperationStatus +import io.iohk.atala.castor.core.model.did.SignedPrismDIDOperation +import scala.collection.immutable.ArraySeq + +class DIDUpdateHandler( + apollo: Apollo, + nonSecretStorage: DIDNonSecretStorage, + secretStorage: DIDSecretStorage, + publicationHandler: PublicationHandler +)( + seed: Array[Byte] +) { + def materialize( + state: ManagedDIDState, + previousOperationHash: Array[Byte], + actions: Seq[UpdateManagedDIDAction] + ): IO[UpdateManagedDIDError, DIDUpdateMaterial] = { + val operationFactory = OperationFactory(apollo) + val did = state.createOperation.did + state.keyMode match { + case KeyManagementMode.HD => + for { + keyCounter <- nonSecretStorage + .getHdKeyCounter(did) + .mapError(UpdateManagedDIDError.WalletStorageError.apply) + .someOrFail( + UpdateManagedDIDError.DataIntegrityError("DID is in HD key mode, but its key counter is not found") + ) + result <- operationFactory.makeUpdateOperationHdKey(seed)(did, previousOperationHash, actions, keyCounter) + (operation, hdKey) = result + signedOperation <- publicationHandler.signOperationWithMasterKey[UpdateManagedDIDError](state, operation) + } yield HdKeyUpdateMaterial(secretStorage, nonSecretStorage)(operation, signedOperation, state, hdKey) + case KeyManagementMode.Random => + for { + result <- operationFactory + .makeUpdateOperationRandKey(did, previousOperationHash, actions) + (operation, randKey) = result + signedOperation <- publicationHandler.signOperationWithMasterKey[UpdateManagedDIDError](state, operation) + } yield RandKeyUpdateMaterial(secretStorage, nonSecretStorage)(operation, signedOperation, state, randKey) + } + } +} + +trait DIDUpdateMaterial { + + def operation: PrismDIDOperation.Update + + def signedOperation: SignedPrismDIDOperation + + def state: ManagedDIDState + + def persist: Task[Unit] + + protected final def persistUpdateLineage(nonSecretStorage: DIDNonSecretStorage): Task[Unit] = { + val did = operation.did + for { + updateLineage <- Clock.instant.map { now => + DIDUpdateLineage( + operationId = ArraySeq.from(signedOperation.toAtalaOperationId), + operationHash = ArraySeq.from(operation.toAtalaOperationHash), + previousOperationHash = operation.previousOperationHash, + status = ScheduledDIDOperationStatus.Pending, + createdAt = now, + updatedAt = now + ) + } + _ <- nonSecretStorage.insertDIDUpdateLineage(did, updateLineage) + } yield () + } + +} + +class RandKeyUpdateMaterial(secretStorage: DIDSecretStorage, nonSecretStorage: DIDNonSecretStorage)( + val operation: PrismDIDOperation.Update, + val signedOperation: SignedPrismDIDOperation, + val state: ManagedDIDState, + randKey: UpdateDIDRandKey +) extends DIDUpdateMaterial { + + private def persistKeyMaterial: Task[Unit] = { + val did = operation.did + val operationHash = operation.toAtalaOperationHash + ZIO.foreachDiscard(randKey.newKeyPairs) { case (keyId, keyPair) => + secretStorage.insertKey(did, keyId, keyPair, operationHash) + } + } + + override def persist: Task[Unit] = + for { + _ <- persistKeyMaterial + _ <- persistUpdateLineage(nonSecretStorage) + } yield () +} + +class HdKeyUpdateMaterial(secretStorage: DIDSecretStorage, nonSecretStorage: DIDNonSecretStorage)( + val operation: PrismDIDOperation.Update, + val signedOperation: SignedPrismDIDOperation, + val state: ManagedDIDState, + hdKey: UpdateDIDHdKey +) extends DIDUpdateMaterial { + + private def persistKeyMaterial: Task[Unit] = { + val did = operation.did + val operationHash = operation.toAtalaOperationHash + ZIO.foreachDiscard(hdKey.newKeyPaths) { case (keyId, keyPath) => + nonSecretStorage.insertHdKeyPath(did, keyId, keyPath, operationHash) + } + } + + override def persist: Task[Unit] = + for { + _ <- persistKeyMaterial + _ <- persistUpdateLineage(nonSecretStorage) + } yield () + +} diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/PublicationHandler.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/PublicationHandler.scala new file mode 100644 index 0000000000..4386683211 --- /dev/null +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/handler/PublicationHandler.scala @@ -0,0 +1,45 @@ +package io.iohk.atala.agent.walletapi.service.handler + +import zio.* +import io.iohk.atala.agent.walletapi.model.ManagedDIDState +import io.iohk.atala.castor.core.model.did.PrismDIDOperation +import io.iohk.atala.agent.walletapi.model.error.CommonWalletStorageError +import io.iohk.atala.agent.walletapi.model.error.CommonCryptographyError +import io.iohk.atala.castor.core.model.did.SignedPrismDIDOperation +import io.iohk.atala.agent.walletapi.util.KeyResolver +import scala.collection.immutable.ArraySeq +import io.iohk.atala.castor.core.model.error.DIDOperationError +import io.iohk.atala.castor.core.model.did.ScheduleDIDOperationOutcome +import io.iohk.atala.castor.core.service.DIDService + +class PublicationHandler(didService: DIDService, keyResolver: KeyResolver)(masterKeyId: String) { + def signOperationWithMasterKey[E](state: ManagedDIDState, operation: PrismDIDOperation)(using + c1: Conversion[CommonWalletStorageError, E], + c2: Conversion[CommonCryptographyError, E] + ): IO[E, SignedPrismDIDOperation] = { + for { + masterKeyPair <- + keyResolver + .getKey(state, masterKeyId) + .mapError[E](CommonWalletStorageError.apply) + .someOrElseZIO( + ZIO.die(Exception("master-key must exists in the wallet for signing DID operation and submit to Node")) + ) + signedOperation <- ZIO + .fromTry(masterKeyPair.privateKey.sign(operation.toAtalaOperation.toByteArray)) + .mapError[E](CommonCryptographyError.apply) + .map(signature => + SignedPrismDIDOperation( + operation = operation, + signature = ArraySeq.from(signature), + signedWithKey = masterKeyId + ) + ) + } yield signedOperation + } + + def submitSignedOperation[E]( + signedOperation: SignedPrismDIDOperation + )(using c1: Conversion[DIDOperationError, E]): IO[E, ScheduleDIDOperationOutcome] = + didService.scheduleOperation(signedOperation).mapError[E](e => e) +} diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala index e2b492a932..16b3f6d6b5 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/JdbcDIDNonSecretStorage.scala @@ -3,13 +3,21 @@ package io.iohk.atala.agent.walletapi.sql import doobie.* import doobie.implicits.* import doobie.postgres.implicits.* -import io.iohk.atala.agent.walletapi.model.{DIDUpdateLineage, ManagedDIDState} +import io.iohk.atala.agent.walletapi.model.{DIDUpdateLineage, ManagedDIDState, ManagedDIDStatePatch} import io.iohk.atala.agent.walletapi.storage.DIDNonSecretStorage import io.iohk.atala.castor.core.model.did.{PrismDID, ScheduledDIDOperationStatus} import zio.* import zio.interop.catz.* import java.time.Instant +import io.iohk.atala.agent.walletapi.model.ManagedDIDHdKeyPath +import io.iohk.atala.castor.core.model.did.VerificationRelationship +import io.iohk.atala.castor.core.model.did.InternalKeyPurpose +import io.iohk.atala.agent.walletapi.model.PublicationState +import io.iohk.atala.agent.walletapi.model.HdKeyIndexCounter +import io.iohk.atala.agent.walletapi.model.InternalKeyCounter +import io.iohk.atala.agent.walletapi.model.VerificationRelationshipCounter +import scala.collection.immutable.ArraySeq class JdbcDIDNonSecretStorage(xa: Transactor[Task]) extends DIDNonSecretStorage { @@ -22,11 +30,13 @@ class JdbcDIDNonSecretStorage(xa: Transactor[Task]) extends DIDNonSecretStorage | atala_operation_content, | publish_operation_id, | created_at, - | updated_at + | updated_at, + | key_mode, + | did_index | FROM public.prism_did_wallet_state | WHERE did = $did """.stripMargin - .query[DIDPublicationStateRow] + .query[DIDStateRow] .option cxnIO @@ -34,15 +44,21 @@ class JdbcDIDNonSecretStorage(xa: Transactor[Task]) extends DIDNonSecretStorage .flatMap(_.map(_.toDomain).fold(ZIO.none)(t => ZIO.fromTry(t).asSome)) } - override def setManagedDIDState(did: PrismDID, state: ManagedDIDState): Task[Unit] = { - val cxnIO = (row: DIDPublicationStateRow) => sql""" + override def insertManagedDID( + did: PrismDID, + state: ManagedDIDState, + hdKey: Map[String, ManagedDIDHdKeyPath] + ): Task[Unit] = { + val insertStateIO = (row: DIDStateRow) => sql""" | INSERT INTO public.prism_did_wallet_state( | did, | publication_status, | atala_operation_content, | publish_operation_id, | created_at, - | updated_at + | updated_at, + | key_mode, + | did_index | ) | VALUES ( | ${row.did}, @@ -50,19 +66,181 @@ class JdbcDIDNonSecretStorage(xa: Transactor[Task]) extends DIDNonSecretStorage | ${row.atalaOperationContent}, | ${row.publishOperationId}, | ${row.createdAt}, - | ${row.updatedAt} + | ${row.updatedAt}, + | ${row.keyMode}, + | ${row.didIndex} | ) - | ON CONFLICT (did) DO UPDATE SET - | publication_status = EXCLUDED.publication_status, - | atala_operation_content = EXCLUDED.atala_operation_content, - | publish_operation_id = EXCLUDED.publish_operation_id, - | updated_at = EXCLUDED.updated_at """.stripMargin.update + val operationHash = state.createOperation.toAtalaOperationHash + val hdKeyValues = (now: Instant) => + hdKey.toList.map { case (key, path) => (did, key, path.keyUsage, path.keyIndex, now, operationHash) } + val insertHdKeyIO = + Update[(PrismDID, String, VerificationRelationship | InternalKeyPurpose, Int, Instant, Array[Byte])]( + "INSERT INTO public.prism_did_hd_key(did, key_id, key_usage, key_index, created_at, operation_hash) VALUES (?, ?, ?, ?, ?, ?)" + ) + + val txnIO = (now: Instant) => + for { + _ <- insertStateIO(DIDStateRow.from(did, state, now)).run + _ <- insertHdKeyIO.updateMany(hdKeyValues(now)) + } yield () + + for { + now <- Clock.instant + _ <- txnIO(now).transact(xa) + } yield () + } + + override def updateManagedDID(did: PrismDID, patch: ManagedDIDStatePatch): Task[Unit] = { + val status = PublicationStatusType.from(patch.publicationState) + val publishedOperationId = patch.publicationState match { + case PublicationState.Created() => None + case PublicationState.PublicationPending(operationId) => Some(operationId) + case PublicationState.Published(operationId) => Some(operationId) + } + val cxnIO = (now: Instant) => sql""" + | UPDATE public.prism_did_wallet_state + | SET + | publication_status = $status, + | publish_operation_id = $publishedOperationId, + | updated_at = $now + | WHERE did = $did + """.stripMargin.update + + for { + now <- Clock.instant + _ <- cxnIO(now).run.transact(xa) + } yield () + } + + override def getMaxDIDIndex(): Task[Option[Int]] = { + val cxnIO = + sql""" + | SELECT MAX(did_index) + | FROM public.prism_did_wallet_state + | WHERE did_index IS NOT NULL + """.stripMargin + .query[Option[Int]] + .option + + cxnIO.transact(xa).map(_.flatten) + } + + override def getHdKeyCounter(did: PrismDID): Task[Option[HdKeyIndexCounter]] = { + val status: ScheduledDIDOperationStatus = ScheduledDIDOperationStatus.Confirmed + val cxnIO = + sql""" + | SELECT + | hd.key_usage AS key_usage, + | MAX(hd.key_index) AS key_index + | FROM public.prism_did_hd_key hd + | LEFT JOIN public.prism_did_wallet_state ws ON hd.did = ws.did + | LEFT JOIN public.prism_did_update_lineage ul ON hd.operation_hash = ul.operation_hash + | WHERE + | hd.did = $did + | AND (ul.status = $status OR (ul.status IS NULL AND hd.operation_hash = sha256(ws.atala_operation_content))) + | GROUP BY hd.did, hd.key_usage + """.stripMargin + .query[(VerificationRelationship | InternalKeyPurpose, Int)] + .to[List] + + getManagedDIDState(did) + .map(_.flatMap(_.didIndex)) + .flatMap { + case None => ZIO.none + case Some(didIndex) => + for { + keyUsageIndex <- cxnIO.transact(xa) + keyUsageIndexMap = keyUsageIndex.map { case (k, v) => k -> (v + 1) }.toMap + } yield Some( + HdKeyIndexCounter( + didIndex, + VerificationRelationshipCounter( + authentication = keyUsageIndexMap.getOrElse(VerificationRelationship.Authentication, 0), + assertionMethod = keyUsageIndexMap.getOrElse(VerificationRelationship.AssertionMethod, 0), + keyAgreement = keyUsageIndexMap.getOrElse(VerificationRelationship.KeyAgreement, 0), + capabilityInvocation = keyUsageIndexMap.getOrElse(VerificationRelationship.CapabilityInvocation, 0), + capabilityDelegation = keyUsageIndexMap.getOrElse(VerificationRelationship.CapabilityDelegation, 0), + ), + InternalKeyCounter( + master = keyUsageIndexMap.getOrElse(InternalKeyPurpose.Master, 0), + revocation = keyUsageIndexMap.getOrElse(InternalKeyPurpose.Revocation, 0), + ) + ) + ) + } + } + + override def getHdKeyPath(did: PrismDID, keyId: String): Task[Option[ManagedDIDHdKeyPath]] = { + val status: ScheduledDIDOperationStatus = ScheduledDIDOperationStatus.Confirmed + val cxnIO = + sql""" + | SELECT + | ws.did_index, + | hd.key_usage, + | hd.key_index + | FROM public.prism_did_hd_key hd + | LEFT JOIN public.prism_did_wallet_state ws ON hd.did = ws.did + | LEFT JOIN public.prism_did_update_lineage ul ON hd.operation_hash = ul.operation_hash + | WHERE + | hd.did = $did + | AND hd.key_id = $keyId + | AND (ul.status = $status OR (ul.status IS NULL AND hd.operation_hash = sha256(ws.atala_operation_content))) + """.stripMargin + .query[ManagedDIDHdKeyPath] + .option + + cxnIO.transact(xa) + } + + override def listHdKeyPath(did: PrismDID): Task[Seq[(String, ArraySeq[Byte], ManagedDIDHdKeyPath)]] = { + val cxnIO = + sql""" + | SELECT + | key_id, + | operation_hash, + | key_usage, + | key_index + | FROM public.prism_did_hd_key + | WHERE did = $did + """.stripMargin + .query[(String, ArraySeq[Byte], VerificationRelationship | InternalKeyPurpose, Int)] + .to[List] + + for { + state <- getManagedDIDState(did) + paths <- cxnIO.transact(xa) + } yield state.flatMap(_.didIndex).fold(Nil) { didIndex => + paths.map { (keyId, operationHash, keyUsage, keyIndex) => + (keyId, operationHash, ManagedDIDHdKeyPath(didIndex, keyUsage, keyIndex)) + } + } + } + + override def insertHdKeyPath( + did: PrismDID, + keyId: String, + hdKeyPath: ManagedDIDHdKeyPath, + operationHash: Array[Byte] + ): Task[Unit] = { + val cxnIO = (now: Instant) => sql""" + | INSERT INTO public.prism_did_hd_key(did, key_id, key_usage, key_index, created_at, operation_hash) + | VALUES + | ( + | $did, + | $keyId, + | ${hdKeyPath.keyUsage}, + | ${hdKeyPath.keyIndex}, + | $now, + | $operationHash + | ) + | ON CONFLICT (did, key_id, operation_hash) DO NOTHING + |""".stripMargin.update + for { now <- Clock.instant - row = DIDPublicationStateRow.from(did, state, now) - _ <- cxnIO(row).run.transact(xa) + _ <- cxnIO(now).run.transact(xa) } yield () } @@ -74,7 +252,7 @@ class JdbcDIDNonSecretStorage(xa: Transactor[Task]) extends DIDNonSecretStorage sql""" | SELECT COUNT(*) | FROM public.prism_did_wallet_state - """.stripMargin + """.stripMargin .query[Int] .unique @@ -86,15 +264,17 @@ class JdbcDIDNonSecretStorage(xa: Transactor[Task]) extends DIDNonSecretStorage | atala_operation_content, | publish_operation_id, | created_at, - | updated_at + | updated_at, + | key_mode, + | did_index | FROM public.prism_did_wallet_state | ORDER BY created_at - """.stripMargin + """.stripMargin val withOffsetFr = offset.fold(baseFr)(offsetValue => baseFr ++ fr"OFFSET $offsetValue") val withOffsetAndLimitFr = limit.fold(withOffsetFr)(limitValue => withOffsetFr ++ fr"LIMIT $limitValue") val didsCxnIO = withOffsetAndLimitFr - .query[DIDPublicationStateRow] + .query[DIDStateRow] .to[List] for { @@ -149,7 +329,7 @@ class JdbcDIDNonSecretStorage(xa: Transactor[Task]) extends DIDNonSecretStorage | created_at, | updated_at | FROM public.prism_did_update_lineage - """.stripMargin + """.stripMargin val cxnIO = (baseFr ++ whereFr) .query[DIDUpdateLineage] .to[List] @@ -167,7 +347,7 @@ class JdbcDIDNonSecretStorage(xa: Transactor[Task]) extends DIDNonSecretStorage | status = $status, | updated_at = $now | WHERE operation_id = $operationId - """.stripMargin.update + """.stripMargin.update Clock.instant.flatMap(now => cxnIO(now).run.transact(xa)).unit } diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/package.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/package.scala index 10b4fb7b67..b684e4878f 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/package.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/sql/package.scala @@ -3,7 +3,7 @@ package io.iohk.atala.agent.walletapi import doobie.* import doobie.postgres.implicits.* import doobie.util.invariant.InvalidEnum -import io.iohk.atala.agent.walletapi.model.ManagedDIDState +import io.iohk.atala.agent.walletapi.model.{ManagedDIDState, PublicationState, KeyManagementMode} import io.iohk.atala.castor.core.model.did.{PrismDID, PrismDIDOperation, ScheduledDIDOperationStatus} import io.iohk.atala.castor.core.model.ProtoModelHelper.* import io.iohk.atala.prism.protos.node_models @@ -11,28 +11,72 @@ import io.iohk.atala.prism.protos.node_models import java.time.Instant import scala.util.Try import scala.collection.immutable.ArraySeq +import io.iohk.atala.castor.core.model.did.VerificationRelationship +import io.iohk.atala.castor.core.model.did.InternalKeyPurpose package object sql { - sealed trait DIDWalletStatusType - object DIDWalletStatusType { - case object CREATED extends DIDWalletStatusType - case object PUBLICATION_PENDING extends DIDWalletStatusType - case object PUBLISHED extends DIDWalletStatusType + sealed trait PublicationStatusType + object PublicationStatusType { + case object CREATED extends PublicationStatusType + case object PUBLICATION_PENDING extends PublicationStatusType + case object PUBLISHED extends PublicationStatusType + + def from(status: PublicationState): PublicationStatusType = status match { + case PublicationState.Created() => CREATED + case PublicationState.PublicationPending(_) => PUBLICATION_PENDING + case PublicationState.Published(_) => PUBLISHED + } } - given Meta[DIDWalletStatusType] = pgEnumString( + given Meta[VerificationRelationship | InternalKeyPurpose] = pgEnumString( + "PRISM_DID_KEY_USAGE", + { + case "MASTER" => InternalKeyPurpose.Master + case "ISSUING" => VerificationRelationship.AssertionMethod + case "KEY_AGREEMENT" => VerificationRelationship.KeyAgreement + case "AUTHENTICATION" => VerificationRelationship.Authentication + case "REVOCATION" => InternalKeyPurpose.Revocation + case "CAPABILITY_INVOCATION" => VerificationRelationship.CapabilityInvocation + case "CAPABILITY_DELEGATION" => VerificationRelationship.CapabilityDelegation + case s => throw InvalidEnum[VerificationRelationship | InternalKeyPurpose](s) + }, + { + case InternalKeyPurpose.Master => "MASTER" + case VerificationRelationship.AssertionMethod => "ISSUING" + case VerificationRelationship.KeyAgreement => "KEY_AGREEMENT" + case VerificationRelationship.Authentication => "AUTHENTICATION" + case InternalKeyPurpose.Revocation => "REVOCATION" + case VerificationRelationship.CapabilityInvocation => "CAPABILITY_INVOCATION" + case VerificationRelationship.CapabilityDelegation => "CAPABILITY_DELEGATION" + } + ) + + given Meta[KeyManagementMode] = pgEnumString( + "PRISM_DID_KEY_MODE", + { + case "HD" => KeyManagementMode.HD + case "RANDOM" => KeyManagementMode.Random + case s => throw InvalidEnum[KeyManagementMode](s) + }, + { + case KeyManagementMode.HD => "HD" + case KeyManagementMode.Random => "RANDOM" + } + ) + + given Meta[PublicationStatusType] = pgEnumString( "PRISM_DID_WALLET_STATUS", { - case "CREATED" => DIDWalletStatusType.CREATED - case "PUBLICATION_PENDING" => DIDWalletStatusType.PUBLICATION_PENDING - case "PUBLISHED" => DIDWalletStatusType.PUBLISHED - case s => throw InvalidEnum[DIDWalletStatusType](s) + case "CREATED" => PublicationStatusType.CREATED + case "PUBLICATION_PENDING" => PublicationStatusType.PUBLICATION_PENDING + case "PUBLISHED" => PublicationStatusType.PUBLISHED + case s => throw InvalidEnum[PublicationStatusType](s) }, { - case DIDWalletStatusType.CREATED => "CREATED" - case DIDWalletStatusType.PUBLICATION_PENDING => "PUBLICATION_PENDING" - case DIDWalletStatusType.PUBLISHED => "PUBLISHED" + case PublicationStatusType.CREATED => "CREATED" + case PublicationStatusType.PUBLICATION_PENDING => "PUBLICATION_PENDING" + case PublicationStatusType.PUBLISHED => "PUBLISHED" } ) @@ -59,31 +103,38 @@ package object sql { given arraySeqByteGet: Get[ArraySeq[Byte]] = Get[Array[Byte]].map(ArraySeq.from) given arraySeqBytePut: Put[ArraySeq[Byte]] = Put[Array[Byte]].contramap(_.toArray) - final case class DIDPublicationStateRow( + final case class DIDStateRow( did: PrismDID, - publicationStatus: DIDWalletStatusType, + publicationStatus: PublicationStatusType, atalaOperationContent: Array[Byte], publishOperationId: Option[Array[Byte]], createdAt: Instant, - updatedAt: Instant + updatedAt: Instant, + keyMode: KeyManagementMode, + didIndex: Option[Int] ) { def toDomain: Try[ManagedDIDState] = { publicationStatus match { - case DIDWalletStatusType.CREATED => createDIDOperation.map(ManagedDIDState.Created.apply) - case DIDWalletStatusType.PUBLICATION_PENDING => + case PublicationStatusType.CREATED => + createDIDOperation.map(op => ManagedDIDState(op, didIndex, PublicationState.Created())) + case PublicationStatusType.PUBLICATION_PENDING => for { createDIDOperation <- createDIDOperation operationId <- publishOperationId .toRight(RuntimeException(s"DID publication operation id does not exists for PUBLICATION_PENDING status")) .toTry - } yield ManagedDIDState.PublicationPending(createDIDOperation, ArraySeq.from(operationId)) - case DIDWalletStatusType.PUBLISHED => + } yield ManagedDIDState( + createDIDOperation, + didIndex, + PublicationState.PublicationPending(ArraySeq.from(operationId)) + ) + case PublicationStatusType.PUBLISHED => for { createDIDOperation <- createDIDOperation operationId <- publishOperationId .toRight(RuntimeException(s"DID publication operation id does not exists for PUBLISHED status")) .toTry - } yield ManagedDIDState.Published(createDIDOperation, ArraySeq.from(operationId)) + } yield ManagedDIDState(createDIDOperation, didIndex, PublicationState.Published(ArraySeq.from(operationId))) } } @@ -102,22 +153,24 @@ package object sql { } } - object DIDPublicationStateRow { - def from(did: PrismDID, state: ManagedDIDState, now: Instant): DIDPublicationStateRow = { - import DIDWalletStatusType.* - val (status, createOperation, publishedOperationId) = state match { - case ManagedDIDState.Created(operation) => (CREATED, operation, None) - case ManagedDIDState.PublicationPending(operation, operationId) => - (PUBLICATION_PENDING, operation, Some(operationId)) - case ManagedDIDState.Published(operation, operationId) => (PUBLISHED, operation, Some(operationId)) + object DIDStateRow { + def from(did: PrismDID, state: ManagedDIDState, now: Instant): DIDStateRow = { + val createOperation = state.createOperation + val status = PublicationStatusType.from(state.publicationState) + val publishedOperationId = state.publicationState match { + case PublicationState.Created() => None + case PublicationState.PublicationPending(operationId) => Some(operationId.toArray) + case PublicationState.Published(operationId) => Some(operationId.toArray) } - DIDPublicationStateRow( + DIDStateRow( did = did, publicationStatus = status, atalaOperationContent = createOperation.toAtalaOperation.toByteArray, publishOperationId = publishedOperationId.map(_.toArray), createdAt = now, - updatedAt = now + updatedAt = now, + keyMode = state.keyMode, + didIndex = state.didIndex ) } } diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDNonSecretStorage.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDNonSecretStorage.scala index 6b2cdadd9b..103792c5c7 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDNonSecretStorage.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDNonSecretStorage.scala @@ -1,14 +1,34 @@ package io.iohk.atala.agent.walletapi.storage -import io.iohk.atala.agent.walletapi.model.{DIDUpdateLineage, ManagedDIDState} +import io.iohk.atala.agent.walletapi.model.{DIDUpdateLineage, ManagedDIDState, ManagedDIDStatePatch} import io.iohk.atala.castor.core.model.did.{PrismDID, ScheduledDIDOperationStatus} import zio.* +import io.iohk.atala.agent.walletapi.model.ManagedDIDHdKeyPath +import io.iohk.atala.agent.walletapi.model.HdKeyIndexCounter +import scala.collection.immutable.ArraySeq private[walletapi] trait DIDNonSecretStorage { def getManagedDIDState(did: PrismDID): Task[Option[ManagedDIDState]] - def setManagedDIDState(did: PrismDID, state: ManagedDIDState): Task[Unit] + def insertManagedDID(did: PrismDID, state: ManagedDIDState, hdKey: Map[String, ManagedDIDHdKeyPath]): Task[Unit] + + def updateManagedDID(did: PrismDID, patch: ManagedDIDStatePatch): Task[Unit] + + def getMaxDIDIndex(): Task[Option[Int]] + + def getHdKeyCounter(did: PrismDID): Task[Option[HdKeyIndexCounter]] + + def getHdKeyPath(did: PrismDID, keyId: String): Task[Option[ManagedDIDHdKeyPath]] + + def insertHdKeyPath( + did: PrismDID, + keyId: String, + hdKeyPath: ManagedDIDHdKeyPath, + operationHash: Array[Byte] + ): Task[Unit] + + def listHdKeyPath(did: PrismDID): Task[Seq[(String, ArraySeq[Byte], ManagedDIDHdKeyPath)]] /** Return a list of Managed DID as well as a count of all filtered items */ def listManagedDID(offset: Option[Int], limit: Option[Int]): Task[(Seq[(PrismDID, ManagedDIDState)], Int)] diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/KeyResolver.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/KeyResolver.scala new file mode 100644 index 0000000000..af67e5b0ed --- /dev/null +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/KeyResolver.scala @@ -0,0 +1,38 @@ +package io.iohk.atala.agent.walletapi.util + +import io.iohk.atala.agent.walletapi.model.ManagedDIDState +import zio.* +import io.iohk.atala.agent.walletapi.crypto.ECKeyPair +import io.iohk.atala.agent.walletapi.storage.{DIDNonSecretStorage, DIDSecretStorage} +import io.iohk.atala.agent.walletapi.model.KeyManagementMode +import io.iohk.atala.castor.core.model.did.PrismDID +import io.iohk.atala.agent.walletapi.crypto.Apollo +import io.iohk.atala.castor.core.model.did.EllipticCurve + +class KeyResolver(apollo: Apollo, nonSecretStorage: DIDNonSecretStorage, secretStorage: DIDSecretStorage)( + seed: Array[Byte] +) { + def getKey(state: ManagedDIDState, keyId: String): Task[Option[ECKeyPair]] = { + val did = state.createOperation.did + getKey(did, state.keyMode, keyId) + } + + def getKey(did: PrismDID, keyMode: KeyManagementMode, keyId: String): Task[Option[ECKeyPair]] = { + keyMode match { + case KeyManagementMode.HD => resolveHdKey(did, keyId) + case KeyManagementMode.Random => secretStorage.getKey(did, keyId) + } + } + + private def resolveHdKey(did: PrismDID, keyId: String): Task[Option[ECKeyPair]] = { + nonSecretStorage + .getHdKeyPath(did, keyId) + .flatMap { + case None => ZIO.none + case Some(path) => + apollo.ecKeyFactory + .deriveKeyPair(EllipticCurve.SECP256K1, seed)(path.derivationPath: _*) + .asSome + } + } +} diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/OperationFactory.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/OperationFactory.scala index 7436ec8007..d5bd44bca6 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/OperationFactory.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/OperationFactory.scala @@ -1,7 +1,15 @@ package io.iohk.atala.agent.walletapi.util import io.iohk.atala.agent.walletapi.crypto.{ECKeyPair, ECPublicKey} -import io.iohk.atala.agent.walletapi.model.{DIDPublicKeyTemplate, ManagedDIDTemplate, UpdateManagedDIDAction} +import io.iohk.atala.agent.walletapi.model.{ + DIDPublicKeyTemplate, + ManagedDIDTemplate, + UpdateManagedDIDAction, + CreateDIDHdKey, + CreateDIDRandKey, + UpdateDIDHdKey, + UpdateDIDRandKey +} import io.iohk.atala.agent.walletapi.model.error.{CreateManagedDIDError, UpdateManagedDIDError} import io.iohk.atala.castor.core.model.did.{ CanonicalPrismDID, @@ -16,86 +24,157 @@ import io.iohk.atala.shared.models.Base64UrlString import zio.* import scala.collection.immutable.ArraySeq +import io.iohk.atala.agent.walletapi.model.HdKeyIndexCounter +import io.iohk.atala.agent.walletapi.crypto.Apollo +import io.iohk.atala.castor.core.model.did.EllipticCurve +import io.iohk.atala.agent.walletapi.model.ManagedDIDHdKeyPath -private[walletapi] final case class CreateDIDSecret( - keyPairs: Map[String, ECKeyPair], - internalKeyPairs: Map[String, ECKeyPair] +private[util] final case class KeyDerivationOutcome[PK]( + publicKey: PK, + path: ManagedDIDHdKeyPath, + nextCounter: HdKeyIndexCounter ) -private[walletapi] final case class UpdateDIDSecret(newKeyPairs: Map[String, ECKeyPair]) - -object OperationFactory { +class OperationFactory(apollo: Apollo) { - def makeCreateOperation( + /** Generates a key pair and a public key from a DID template + * + * @param masterKeyId + * The key id of the master key + * @param seed + * The seed to use for the key generation + * @param didTemplate + * The DID template + * @param didIndex + * The index of the DID to be used for the key derivation + */ + def makeCreateOperationHdKey( masterKeyId: String, - keyGenerator: () => Task[ECKeyPair] + seed: Array[Byte] )( + didIndex: Int, didTemplate: ManagedDIDTemplate - ): IO[CreateManagedDIDError, (PrismDIDOperation.Create, CreateDIDSecret)] = { - for { - keys <- ZIO - .foreach(didTemplate.publicKeys)(generateKeyPairAndPublicKey(keyGenerator)) - .mapError(CreateManagedDIDError.KeyGenerationError.apply) - masterKey <- generateKeyPairAndInternalPublicKey(keyGenerator)(masterKeyId, InternalKeyPurpose.Master) - .mapError( - CreateManagedDIDError.KeyGenerationError.apply - ) + ): IO[CreateManagedDIDError, (PrismDIDOperation.Create, CreateDIDHdKey)] = { + val initKeysWithCounter = (Vector.empty[(PublicKey, ManagedDIDHdKeyPath)], HdKeyIndexCounter.zero(didIndex)) + val result = for { + keysWithCounter <- ZIO.foldLeft(didTemplate.publicKeys)(initKeysWithCounter) { + case ((keys, keyCounter), template) => + derivePublicKey(seed)(template, keyCounter) + .map { outcome => + val newKeys = keys :+ (outcome.publicKey, outcome.path) + (newKeys, outcome.nextCounter) + } + } + masterKeyOutcome <- deriveInternalPublicKey(seed)( + masterKeyId, + InternalKeyPurpose.Master, + keysWithCounter._2 + ) operation = PrismDIDOperation.Create( - publicKeys = keys.map(_._2) ++ Seq(masterKey._2), + publicKeys = keysWithCounter._1.map(_._1) ++ Seq(masterKeyOutcome.publicKey), services = didTemplate.services, context = Seq() // TODO: expose context in the API ) - secret = CreateDIDSecret( - keyPairs = keys.map { case (keyPair, publicKey) => publicKey.id -> keyPair }.toMap, - internalKeyPairs = Map(masterKey._2.id -> masterKey._1) + hdKeys = CreateDIDHdKey( + keyPaths = keysWithCounter._1.map { case (publicKey, path) => publicKey.id -> path }.toMap, + internalKeyPaths = Map(masterKeyOutcome.publicKey.id -> masterKeyOutcome.path), ) - } yield operation -> secret + } yield operation -> hdKeys + + result.mapError(CreateManagedDIDError.KeyGenerationError.apply) } - def makeUpdateOperation( - keyGenerator: () => Task[ECKeyPair] - )( + def makeCreateOperationRandKey( + masterKeyId: String + )(didTemplate: ManagedDIDTemplate): IO[CreateManagedDIDError, (PrismDIDOperation.Create, CreateDIDRandKey)] = { + for { + randomSeed <- apollo.ecKeyFactory.randomBip32Seed().mapError(CreateManagedDIDError.KeyGenerationError.apply) + operationWithHdKey <- makeCreateOperationHdKey(masterKeyId, randomSeed)(0, didTemplate) + (operation, hdKeys) = operationWithHdKey + keyPairs <- ZIO.foreach(hdKeys.keyPaths) { case (id, path) => + deriveSecp256k1KeyPair(randomSeed, path).mapBoth(CreateManagedDIDError.KeyGenerationError.apply, id -> _) + } + internalKeyPairs <- ZIO.foreach(hdKeys.internalKeyPaths) { case (id, path) => + deriveSecp256k1KeyPair(randomSeed, path).mapBoth(CreateManagedDIDError.KeyGenerationError.apply, id -> _) + } + } yield operation -> CreateDIDRandKey( + keyPairs = keyPairs, + internalKeyPairs = internalKeyPairs + ) + } + + def makeUpdateOperationHdKey(seed: Array[Byte])( did: CanonicalPrismDID, previousOperationHash: Array[Byte], - actions: Seq[UpdateManagedDIDAction] - ): IO[UpdateManagedDIDError, (PrismDIDOperation.Update, UpdateDIDSecret)] = { - val actionsWithSecret = actions.map { - case a @ UpdateManagedDIDAction.AddKey(template) => - a -> generateKeyPairAndPublicKey(keyGenerator)(template) - .mapError(UpdateManagedDIDError.KeyGenerationError.apply) - .asSome - case a => a -> ZIO.none + actions: Seq[UpdateManagedDIDAction], + fromKeyCounter: HdKeyIndexCounter + ): IO[UpdateManagedDIDError, (PrismDIDOperation.Update, UpdateDIDHdKey)] = { + val initKeysWithCounter = + (Vector.empty[(UpdateManagedDIDAction, Option[(PublicKey, ManagedDIDHdKeyPath)])], fromKeyCounter) + val actionsWithKeyMaterial = ZIO.foldLeft(actions)(initKeysWithCounter) { case ((acc, keyCounter), action) => + val derivation = action match { + case UpdateManagedDIDAction.AddKey(template) => + derivePublicKey(seed)(template, keyCounter).mapError(UpdateManagedDIDError.KeyGenerationError.apply).asSome + case _ => ZIO.none + } + derivation.map { + case Some(outcome) => (acc :+ (action -> Some((outcome.publicKey, outcome.path))), outcome.nextCounter) + case None => (acc :+ (action -> None), keyCounter) + } } for { - actionsWithSecret <- ZIO - .foreach(actionsWithSecret) { case (action, secret) => secret.map(action -> _) } - transformedActions <- ZIO.foreach(actionsWithSecret)(transformUpdateAction) - keys = actionsWithSecret.collect { case (UpdateManagedDIDAction.AddKey(_), Some(secret)) => secret } + actionsWithKeyMaterial <- actionsWithKeyMaterial + (actionWithHdKey, keyCounter) = actionsWithKeyMaterial + transformedActions <- ZIO.foreach(actionWithHdKey) { case (action, keyMaterial) => + transformUpdateAction(action, keyMaterial.map(_._1)) + } + keys = actionWithHdKey.collect { case (UpdateManagedDIDAction.AddKey(_), Some(secret)) => secret } operation = PrismDIDOperation.Update( did = did, previousOperationHash = ArraySeq.from(previousOperationHash), actions = transformedActions ) - secret = UpdateDIDSecret( + hdKeys = UpdateDIDHdKey( // NOTE: Prism DID specification currently doesn't allow updating existing key with the same key-id. // Duplicated key-id in AddKey action can be ignored as the specification will reject the whole update operation. // If the specification supports updating existing key, the key that will be stored in the wallet // MUST be aligned with the spec (e.g. keep first / keep last in the action list) - newKeyPairs = keys.map { case (keyPair, publicKey) => publicKey.id -> keyPair }.toMap + newKeyPaths = keys.map { case (publicKey, path) => publicKey.id -> path }.toMap, + counter = keyCounter ) - } yield operation -> secret + } yield operation -> hdKeys + } + + def makeUpdateOperationRandKey( + did: CanonicalPrismDID, + previousOperationHash: Array[Byte], + actions: Seq[UpdateManagedDIDAction] + ): IO[UpdateManagedDIDError, (PrismDIDOperation.Update, UpdateDIDRandKey)] = { + for { + randomSeed <- apollo.ecKeyFactory.randomBip32Seed().mapError(UpdateManagedDIDError.KeyGenerationError.apply) + operationWithHdKey <- makeUpdateOperationHdKey(randomSeed)( + did, + previousOperationHash, + actions, + HdKeyIndexCounter.zero(0) + ) + (operation, hdKeys) = operationWithHdKey + keyPairs <- ZIO.foreach(hdKeys.newKeyPaths) { case (id, path) => + deriveSecp256k1KeyPair(randomSeed, path).mapBoth(UpdateManagedDIDError.KeyGenerationError.apply, id -> _) + } + } yield operation -> UpdateDIDRandKey(newKeyPairs = keyPairs) } private def transformUpdateAction( updateAction: UpdateManagedDIDAction, - secret: Option[(ECKeyPair, PublicKey)] + publicKey: Option[PublicKey] ): UIO[UpdateDIDAction] = { updateAction match { case UpdateManagedDIDAction.AddKey(_) => - secret match { - case Some((_, publicKey)) => ZIO.succeed(UpdateDIDAction.AddKey(publicKey)) - case None => + publicKey match { + case Some(publicKey) => ZIO.succeed(UpdateDIDAction.AddKey(publicKey)) + case None => // should be impossible otherwise it's a defect ZIO.dieMessage("addKey update DID action must have a generated a key-pair") } @@ -107,23 +186,32 @@ object OperationFactory { } } - private def generateKeyPairAndPublicKey(keyGenerator: () => Task[ECKeyPair])( - template: DIDPublicKeyTemplate - ): Task[(ECKeyPair, PublicKey)] = { + private def derivePublicKey(seed: Array[Byte])( + template: DIDPublicKeyTemplate, + keyCounter: HdKeyIndexCounter + ): Task[KeyDerivationOutcome[PublicKey]] = { + val purpose = template.purpose + val keyPath = keyCounter.path(purpose) for { - keyPair <- keyGenerator() - publicKey = PublicKey(template.id, template.purpose, toPublicKeyData(keyPair.publicKey)) - } yield (keyPair, publicKey) + keyPair <- deriveSecp256k1KeyPair(seed, keyPath) + publicKey = PublicKey(template.id, purpose, toPublicKeyData(keyPair.publicKey)) + } yield KeyDerivationOutcome(publicKey, keyPath, keyCounter.next(purpose)) } - private def generateKeyPairAndInternalPublicKey(keyGenerator: () => Task[ECKeyPair])( + private def deriveInternalPublicKey(seed: Array[Byte])( id: String, - purpose: InternalKeyPurpose - ): Task[(ECKeyPair, InternalPublicKey)] = { + purpose: InternalKeyPurpose, + keyCounter: HdKeyIndexCounter + ): Task[KeyDerivationOutcome[InternalPublicKey]] = { + val keyPath = keyCounter.path(purpose) for { - keyPair <- keyGenerator() + keyPair <- deriveSecp256k1KeyPair(seed, keyPath) internalPublicKey = InternalPublicKey(id, purpose, toPublicKeyData(keyPair.publicKey)) - } yield (keyPair, internalPublicKey) + } yield KeyDerivationOutcome(internalPublicKey, keyPath, keyCounter.next(purpose)) + } + + private def deriveSecp256k1KeyPair(seed: Array[Byte], path: ManagedDIDHdKeyPath): Task[ECKeyPair] = { + apollo.ecKeyFactory.deriveKeyPair(EllipticCurve.SECP256K1, seed)(path.derivationPath: _*) } private def toPublicKeyData(publicKey: ECPublicKey): PublicKeyData = PublicKeyData.ECCompressedKeyData( diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/SeedResolver.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/SeedResolver.scala new file mode 100644 index 0000000000..4e942e16f4 --- /dev/null +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/util/SeedResolver.scala @@ -0,0 +1,58 @@ +package io.iohk.atala.agent.walletapi.util + +import zio.* +import io.iohk.atala.shared.models.HexString +import io.iohk.atala.agent.walletapi.crypto.Apollo + +trait SeedResolver { + def resolve: Task[Array[Byte]] +} + +object SeedResolver { + def layer(seedOverrideHex: Option[String] = None): URLayer[Apollo, SeedResolver] = + ZLayer.fromFunction(SeedResolverImpl(_, seedOverrideHex)) +} + +private class SeedResolverImpl(apollo: Apollo, seedOverrideHex: Option[String]) extends SeedResolver { + override def resolve: Task[Array[Byte]] = { + val seedOverride = + for { + _ <- ZIO.logInfo("Resolving a wallet seed using seed-override") + maybeSeed <- seedOverrideHex + .fold(ZIO.none) { hex => + ZIO.fromTry(HexString.fromString(hex)).map(_.toByteArray).asSome + } + .tapError(e => ZIO.logError("Failed to parse seed-override")) + _ <- ZIO.logInfo("seed-override is not found. Fallback to the next resolver").when(maybeSeed.isEmpty) + } yield maybeSeed + + val seedEnv = + for { + _ <- ZIO.logInfo("Resolving a wallet seed using WALLET_SEED environemnt variable") + maybeSeed <- System + .env("WALLET_SEED") + .flatMap { + case Some(hex) => ZIO.fromTry(HexString.fromString(hex)).map(_.toByteArray).asSome + case None => ZIO.none + } + .tapError(e => ZIO.logError("Failed to parse WALLET_SEED")) + _ <- ZIO.logInfo("WALLET_SEED environment is not found. Fallback to the next resolver").when(maybeSeed.isEmpty) + } yield maybeSeed + + val seedRand = + for { + _ <- ZIO.logInfo("Generating a new wallet seed") + seed <- apollo.ecKeyFactory.randomBip32Seed() + } yield seed + + seedOverride + .flatMap { + case Some(seed) => ZIO.some(seed) + case None => seedEnv + } + .flatMap { + case Some(seed) => ZIO.succeed(seed) + case None => seedRand + } + } +} diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/crypto/ApolloSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/crypto/ApolloSpec.scala index 72c2085ce6..d79f645d40 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/crypto/ApolloSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/crypto/ApolloSpec.scala @@ -13,6 +13,7 @@ object ApolloSpec extends ZIOSpecDefault { publicKeySpec, privateKeySpec, ecKeyFactorySpec, + ecKeyFactoryBip32Spec ) suite("Apollo - Prism14 implementation")(tests: _*).provideLayer(Apollo.prism14Layer) } @@ -218,7 +219,104 @@ object ApolloSpec extends ZIOSpecDefault { pk1 = apollo.ecKeyFactory.publicKeyFromEncoded(EllipticCurve.SECP256K1, bytes).get pk2 = apollo.ecKeyFactory.publicKeyFromEncoded(EllipticCurve.SECP256K1, bytes2).get } yield assert(pk1)(equalTo(pk2)) - }, + } ) + // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vectors + private val ecKeyFactoryBip32Spec = { + def assertHDKey(seedHex: String)(pathStr: String, expectedPrivateKeyHex: String) = { + val path = pathStr + .drop(1) + .split("/") + .filter(_.nonEmpty) + .map { s => + if (s.endsWith("'")) DerivationPath.Hardened(s.dropRight(1).toInt) + else DerivationPath.Normal(s.toInt) + } + .toSeq + test(pathStr) { + val seed = HexString.fromStringUnsafe(seedHex).toByteArray + for { + apollo <- ZIO.service[Apollo] + keyPair <- apollo.ecKeyFactory.deriveKeyPair(EllipticCurve.SECP256K1, seed)(path: _*) + } yield assert(keyPair.privateKey.encode)( + equalTo(HexString.fromStringUnsafe(expectedPrivateKeyHex).toByteArray) + ) + } + } + + val testVector1 = assertHDKey("000102030405060708090a0b0c0d0e0f") + val testVector2 = assertHDKey( + "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542" + ) + val testVector3 = assertHDKey( + "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be" + ) + + suite("ECKeyFactory - BIP32")( + suite("Test vector 1")( + testVector1( + "m", + "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35" + ), + testVector1( + "m/0'", + "edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea" + ), + testVector1( + "m/0'/1", + "3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368" + ), + testVector1( + "m/0'/1/2'", + "cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca" + ), + testVector1( + "m/0'/1/2'/2", + "0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4" + ), + testVector1( + "m/0'/1/2'/2/1000000000", + "471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8" + ), + ), + suite("Test vector 2")( + testVector2( + "m", + "4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e" + ), + testVector2( + "m/0", + "abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e" + ), + testVector2( + "m/0/2147483647'", + "877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93" + ), + testVector2( + "m/0/2147483647'/1", + "704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7" + ), + testVector2( + "m/0/2147483647'/1/2147483646'", + "f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d" + ), + testVector2( + "m/0/2147483647'/1/2147483646'/2", + "bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23" + ), + ), + suite("Test vector 3")( + testVector3( + "m", + "00ddb80b067e0d4993197fe10f2657a844a384589847602d56f0c629c81aae32" + ), + testVector3( + "m/0'", + "491f7a2eebc7b57028e0d3faa0acda02e75c33b03c48fb288c41e2ea44e1daef" + ) + ) + ) + } + } diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala index e86f8a00d8..8fed45a315 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDServiceSpec.scala @@ -1,7 +1,7 @@ package io.iohk.atala.agent.walletapi.service import io.iohk.atala.agent.walletapi.model.error.{CreateManagedDIDError, PublishManagedDIDError} -import io.iohk.atala.agent.walletapi.model.{DIDPublicKeyTemplate, ManagedDIDState, ManagedDIDTemplate} +import io.iohk.atala.agent.walletapi.model.{DIDPublicKeyTemplate, ManagedDIDState, ManagedDIDTemplate, PublicationState} import io.iohk.atala.castor.core.model.did.{ DIDData, DIDMetadata, @@ -33,6 +33,7 @@ import io.iohk.atala.castor.core.model.did.InternalKeyPurpose import io.iohk.atala.agent.walletapi.model.error.UpdateManagedDIDError import io.iohk.atala.agent.walletapi.model.UpdateManagedDIDAction import io.iohk.atala.agent.walletapi.crypto.Apollo +import io.iohk.atala.agent.walletapi.util.SeedResolver object ManagedDIDServiceSpec extends ZIOSpecDefault, PostgresTestContainerSupport, ApolloSpecHelper { @@ -76,7 +77,10 @@ object ManagedDIDServiceSpec extends ZIOSpecDefault, PostgresTestContainerSuppor pgContainerLayer >+> (transactorLayer ++ apolloLayer) >+> (JdbcDIDSecretStorage.layer ++ JdbcDIDNonSecretStorage.layer) private def managedDIDServiceLayer = - (DIDOperationValidator.layer() ++ testDIDServiceLayer ++ apolloLayer) >+> ManagedDIDService.layer + (DIDOperationValidator.layer() ++ + testDIDServiceLayer ++ + apolloLayer ++ + SeedResolver.layer()) >+> ManagedDIDService.layer private def generateDIDTemplate( publicKeys: Seq[DIDPublicKeyTemplate] = Nil, @@ -120,7 +124,7 @@ object ManagedDIDServiceSpec extends ZIOSpecDefault, PostgresTestContainerSuppor deactivateManagedDIDSpec ) @@ TestAspect.before(DBTestUtils.runMigrationAgentDB) - testSuite.provideLayer(jdbcStorageLayer >+> managedDIDServiceLayer) + testSuite.provideLayer(jdbcStorageLayer >+> managedDIDServiceLayer).provide(Runtime.removeDefaultLoggers) } private val publishStoredDIDSpec = @@ -132,7 +136,7 @@ object ManagedDIDServiceSpec extends ZIOSpecDefault, PostgresTestContainerSuppor testDIDSvc <- ZIO.service[TestDIDService] did <- svc.createAndStoreDID(template).map(_.asCanonical) createOp <- svc.nonSecretStorage.getManagedDIDState(did).collect(()) { - case Some(ManagedDIDState.Created(op)) => op + case Some(ManagedDIDState(op, _, PublicationState.Created())) => op } opsBefore <- testDIDSvc.getPublishedOperations _ <- svc.publishStoredDID(did) @@ -158,11 +162,11 @@ object ManagedDIDServiceSpec extends ZIOSpecDefault, PostgresTestContainerSuppor for { svc <- ZIO.service[ManagedDIDService] did <- svc.createAndStoreDID(template).map(_.asCanonical) - stateBefore <- svc.nonSecretStorage.getManagedDIDState(did) + stateBefore <- svc.nonSecretStorage.getManagedDIDState(did).map(_.map(_.publicationState)) _ <- svc.publishStoredDID(did) - stateAfter <- svc.nonSecretStorage.getManagedDIDState(did) - } yield assert(stateBefore)(isSome(isSubtype[ManagedDIDState.Created](anything))) - && assert(stateAfter)(isSome(isSubtype[ManagedDIDState.PublicationPending](anything))) + stateAfter <- svc.nonSecretStorage.getManagedDIDState(did).map(_.map(_.publicationState)) + } yield assert(stateBefore)(isSome(isSubtype[PublicationState.Created](anything))) + && assert(stateAfter)(isSome(isSubtype[PublicationState.PublicationPending](anything))) }, test("do not re-publish when publishing already published DID") { val template = generateDIDTemplate() @@ -197,8 +201,8 @@ object ManagedDIDServiceSpec extends ZIOSpecDefault, PostgresTestContainerSuppor for { svc <- ZIO.service[ManagedDIDService] did <- svc.createAndStoreDID(template).map(_.asCanonical) - keyPairs <- svc.secretStorage.listKeys(did) - } yield assert(keyPairs.map(_._1))(hasSameElements(Seq("key1", "key2", ManagedDIDService.DEFAULT_MASTER_KEY_ID))) + keyPaths <- svc.nonSecretStorage.listHdKeyPath(did) + } yield assert(keyPaths.map(_._1))(hasSameElements(Seq("key1", "key2", ManagedDIDService.DEFAULT_MASTER_KEY_ID))) }, test("created DID have corresponding public keys in CreateOperation") { val template = generateDIDTemplate( @@ -212,7 +216,9 @@ object ManagedDIDServiceSpec extends ZIOSpecDefault, PostgresTestContainerSuppor svc <- ZIO.service[ManagedDIDService] did <- svc.createAndStoreDID(template).map(_.asCanonical) state <- svc.nonSecretStorage.getManagedDIDState(did) - createOperation <- ZIO.fromOption(state.collect { case ManagedDIDState.Created(operation) => operation }) + createOperation <- ZIO.fromOption(state.collect { + case ManagedDIDState(operation, _, PublicationState.Created()) => operation + }) publicKeys = createOperation.publicKeys.collect { case pk: PublicKey => pk } } yield assert(publicKeys.map(i => i.id -> i.purpose))( hasSameElements( @@ -229,7 +235,9 @@ object ManagedDIDServiceSpec extends ZIOSpecDefault, PostgresTestContainerSuppor svc <- ZIO.service[ManagedDIDService] did <- svc.createAndStoreDID(generateDIDTemplate()).map(_.asCanonical) state <- svc.nonSecretStorage.getManagedDIDState(did) - createOperation <- ZIO.fromOption(state.collect { case ManagedDIDState.Created(operation) => operation }) + createOperation <- ZIO.fromOption(state.collect { + case ManagedDIDState(operation, _, PublicationState.Created()) => operation + }) internalKeys = createOperation.publicKeys.collect { case pk: InternalPublicKey => pk } } yield assert(internalKeys.map(_.purpose))(contains(InternalKeyPurpose.Master)) }, @@ -310,8 +318,8 @@ object ManagedDIDServiceSpec extends ZIOSpecDefault, PostgresTestContainerSuppor UpdateManagedDIDAction.AddKey(DIDPublicKeyTemplate(id, VerificationRelationship.Authentication)) ) _ <- svc.updateManagedDID(did, actions) - keyPairs <- svc.secretStorage.listKeys(did) - } yield assert(keyPairs.map(_._1))( + keyPaths <- svc.nonSecretStorage.listHdKeyPath(did) + } yield assert(keyPaths.map(_._1))( hasSameElements(Seq(ManagedDIDService.DEFAULT_MASTER_KEY_ID, "key-1", "key-2")) ) }, @@ -327,8 +335,8 @@ object ManagedDIDServiceSpec extends ZIOSpecDefault, PostgresTestContainerSuppor ) _ <- svc.updateManagedDID(did, actions) // 1st update _ <- svc.updateManagedDID(did, actions.take(1)) // 2nd update: key-1 is added twice - keyPairs <- svc.secretStorage.listKeys(did) - } yield assert(keyPairs.map(_._1))( + keyPaths <- svc.nonSecretStorage.listHdKeyPath(did) + } yield assert(keyPaths.map(_._1))( hasSameElements(Seq(ManagedDIDService.DEFAULT_MASTER_KEY_ID, "key-1", "key-1", "key-2")) ) }, diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcDIDNonSecretStorageSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcDIDNonSecretStorageSpec.scala index 011410f010..1d24592be5 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcDIDNonSecretStorageSpec.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/JdbcDIDNonSecretStorageSpec.scala @@ -13,6 +13,7 @@ import io.iohk.atala.agent.walletapi.crypto.ApolloSpecHelper import io.iohk.atala.castor.core.model.did.ScheduledDIDOperationStatus import io.iohk.atala.castor.core.model.did.PrismDIDOperation import org.postgresql.util.PSQLException +import io.iohk.atala.agent.walletapi.model.PublicationState object JdbcDIDNonSecretStorageSpec extends ZIOSpecDefault, @@ -24,7 +25,11 @@ object JdbcDIDNonSecretStorageSpec ZIO.foreach(operation) { op => for { storage <- ZIO.service[DIDNonSecretStorage] - _ <- storage.setManagedDIDState(op.did, ManagedDIDState.Created(op)) + _ <- storage.insertManagedDID( + op.did, + ManagedDIDState(op, None, PublicationState.Created()), + Map.empty + ) _ <- TestClock.adjust(delay) } yield () } @@ -33,7 +38,6 @@ object JdbcDIDNonSecretStorageSpec val testSuite = suite("JdbcDIDNonSecretStorageSpec")( listDIDStateSpec, - setDIDStateSpec, getDIDStateSpec, listDIDLineageSpec, setDIDLineageStatusSpec @@ -59,8 +63,16 @@ object JdbcDIDNonSecretStorageSpec did2 = PrismDID.buildCanonicalFromSuffix("1" * 64).toOption.get createOperation1 <- generateCreateOperation(Seq("key-1")).map(_._1) createOperation2 <- generateCreateOperation(Seq("key-1")).map(_._1) - _ <- storage.setManagedDIDState(did1, ManagedDIDState.Created(createOperation1)) - _ <- storage.setManagedDIDState(did2, ManagedDIDState.Created(createOperation2)) + _ <- storage.insertManagedDID( + did1, + ManagedDIDState(createOperation1, None, PublicationState.Created()), + Map.empty + ) + _ <- storage.insertManagedDID( + did2, + ManagedDIDState(createOperation2, None, PublicationState.Created()), + Map.empty + ) states <- storage.listManagedDID(None, None).map(_._1) } yield assert(states.map(_._1))(hasSameElements(Seq(did1, did2))) }, @@ -132,22 +144,6 @@ object JdbcDIDNonSecretStorageSpec } ) - private val setDIDStateSpec = suite("setManagedDIDState")( - test("replace state if set for the same did") { - for { - storage <- ZIO.service[DIDNonSecretStorage] - createOperation1 <- generateCreateOperation(Seq("key-1")).map(_._1) - createOperation2 <- generateCreateOperation(Seq("key-1")).map(_._1) - _ <- storage.setManagedDIDState(didExample, ManagedDIDState.Created(createOperation1)) - state1 <- storage.getManagedDIDState(didExample) - _ <- storage.setManagedDIDState(didExample, ManagedDIDState.Created(createOperation2)) - state2 <- storage.getManagedDIDState(didExample) - } yield assert(state1.get.createOperation)(equalTo(createOperation1)) - && assert(state2.get.createOperation)(equalTo(createOperation2)) - && assert(createOperation1)(not(equalTo(createOperation2))) - } - ) - private val getDIDStateSpec = suite("getManagedDIDState")( test("return None of state is not found") { for { @@ -158,19 +154,28 @@ object JdbcDIDNonSecretStorageSpec test("return the same state that was set for all variants") { for { storage <- ZIO.service[DIDNonSecretStorage] - createOperation <- generateCreateOperation(Seq("key-1")).map(_._1) - states = Seq( - ManagedDIDState.Created(createOperation), - ManagedDIDState.PublicationPending(createOperation, ArraySeq.fill(32)(0)), - ManagedDIDState.Published(createOperation, ArraySeq.fill(32)(1)), + inputs = Seq[(Option[Int], PublicationState)]( + (None, PublicationState.Created()), + (None, PublicationState.PublicationPending(ArraySeq.fill(32)(0))), + (None, PublicationState.Published(ArraySeq.fill(32)(1))), + (Some(1), PublicationState.Created()), + (Some(2), PublicationState.PublicationPending(ArraySeq.fill(32)(0))), + (Some(3), PublicationState.Published(ArraySeq.fill(32)(1))), ) + states <- ZIO.foreach(inputs) { case (didIndex, publicationState) => + val operation = didIndex match { + case Some(idx) => generateCreateOperationHdKey(Seq("key-1"), idx).map(_._1) + case None => generateCreateOperation(Seq("key-1")).map(_._1) + } + operation.map(o => ManagedDIDState(o, None, publicationState)) + } readStates <- ZIO.foreach(states) { state => for { - _ <- storage.setManagedDIDState(didExample, state) - readState <- storage.getManagedDIDState(didExample) + _ <- storage.insertManagedDID(state.createOperation.did, state, Map.empty) + readState <- storage.getManagedDIDState(state.createOperation.did) } yield readState } - } yield assert(readStates.flatten)(equalTo(states)) + } yield assert(readStates.flatten)(hasSameElements(states)) } ) diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/StorageSpecHelper.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/StorageSpecHelper.scala index 2de9799835..115d1cb309 100644 --- a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/StorageSpecHelper.scala +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/storage/StorageSpecHelper.scala @@ -14,6 +14,7 @@ import io.iohk.atala.agent.walletapi.model.DIDPublicKeyTemplate import io.iohk.atala.castor.core.model.did.VerificationRelationship import zio.* import io.iohk.atala.agent.walletapi.model.ManagedDIDState +import io.iohk.atala.agent.walletapi.model.PublicationState trait StorageSpecHelper extends ApolloSpecHelper { protected val didExample = PrismDID.buildLongFormFromOperation(PrismDIDOperation.Create(Nil, Nil, Nil)) @@ -34,7 +35,16 @@ trait StorageSpecHelper extends ApolloSpecHelper { protected def generateKeyPair() = apollo.ecKeyFactory.generateKeyPair(EllipticCurve.SECP256K1) protected def generateCreateOperation(keyIds: Seq[String]) = - OperationFactory.makeCreateOperation("master0", generateKeyPair)( + OperationFactory(apollo).makeCreateOperationRandKey("master0")( + ManagedDIDTemplate( + publicKeys = keyIds.map(DIDPublicKeyTemplate(_, VerificationRelationship.Authentication)), + services = Nil + ) + ) + + protected def generateCreateOperationHdKey(keyIds: Seq[String], didIndex: Int) = + OperationFactory(apollo).makeCreateOperationHdKey("master0", Array.fill(64)(0))( + didIndex, ManagedDIDTemplate( publicKeys = keyIds.map(DIDPublicKeyTemplate(_, VerificationRelationship.Authentication)), services = Nil @@ -49,7 +59,11 @@ trait StorageSpecHelper extends ApolloSpecHelper { (createOperation, secrets) = generated did = createOperation.did keyPairs = secrets.keyPairs.toSeq - _ <- nonSecretStorage.setManagedDIDState(did, ManagedDIDState.Created(createOperation)) + _ <- nonSecretStorage.insertManagedDID( + did, + ManagedDIDState(createOperation, None, PublicationState.Created()), + Map.empty + ) _ <- ZIO.foreach(keyPairs) { case (keyId, keyPair) => secretStorage.insertKey(did, keyId, keyPair, createOperation.toAtalaOperationHash) } diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/util/OperationFactorySpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/util/OperationFactorySpec.scala new file mode 100644 index 0000000000..baadb65dbe --- /dev/null +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/util/OperationFactorySpec.scala @@ -0,0 +1,131 @@ +package io.iohk.atala.agent.walletapi.util + +import zio.* +import zio.test.* +import zio.test.Assertion.* +import io.iohk.atala.agent.walletapi.crypto.ApolloSpecHelper +import io.iohk.atala.shared.models.HexString +import io.iohk.atala.agent.walletapi.model.ManagedDIDTemplate +import io.iohk.atala.castor.core.model.did.InternalPublicKey +import io.iohk.atala.castor.core.model.did.InternalKeyPurpose +import io.iohk.atala.agent.walletapi.model.DIDPublicKeyTemplate +import io.iohk.atala.castor.core.model.did.VerificationRelationship +import io.iohk.atala.castor.core.model.did.PrismDID +import io.iohk.atala.agent.walletapi.model.HdKeyIndexCounter +import io.iohk.atala.agent.walletapi.model.UpdateManagedDIDAction +import io.iohk.atala.castor.core.model.did.PrismDIDOperation +import io.iohk.atala.agent.walletapi.model.VerificationRelationshipCounter +import io.iohk.atala.castor.core.model.did.UpdateDIDAction + +object OperationFactorySpec extends ZIOSpecDefault, ApolloSpecHelper { + + private val didExample = PrismDID.buildLongFormFromOperation(PrismDIDOperation.Create(Nil, Nil, Nil)).asCanonical + + private val previousOperationHash = didExample.stateHash.toByteArray + + private val seed = HexString + .fromStringUnsafe( + "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542" + ) + .toByteArray + + private val operationFactory = OperationFactory(apollo) + + override def spec = + suite("OperationFactory")(makeCreateOpeartionHdKeySpec, makeUpdateOperationHdKeySpec) + + private val makeCreateOpeartionHdKeySpec = suite("makeCreateOpeartionHdKeySpec ")( + test("make CrateOperation from same seed is deterministic") { + val didTemplate = ManagedDIDTemplate(Nil, Nil) + for { + result1 <- operationFactory.makeCreateOperationHdKey("master0", seed)(0, didTemplate) + (op1, hdKey1) = result1 + result2 <- operationFactory.makeCreateOperationHdKey("master0", seed)(0, didTemplate) + (op2, hdKey2) = result2 + } yield assert(op1)(equalTo(op2)) && + assert(hdKey1)(equalTo(hdKey2)) + }, + test("make CreateOperation must contain 1 master key") { + val didTemplate = ManagedDIDTemplate(Nil, Nil) + for { + result <- operationFactory.makeCreateOperationHdKey("master-0", seed)(0, didTemplate) + (op, hdKey) = result + pk = op.publicKeys.head.asInstanceOf[InternalPublicKey] + } yield assert(op.publicKeys)(hasSize(equalTo(1))) && + assert(pk.id)(equalTo("master-0")) && + assert(pk.purpose)(equalTo(InternalKeyPurpose.Master)) + }, + test("make CreateOperation containing multiple keys") { + val didTemplate = ManagedDIDTemplate( + Seq( + DIDPublicKeyTemplate("auth-0", VerificationRelationship.Authentication), + DIDPublicKeyTemplate("auth-1", VerificationRelationship.Authentication), + DIDPublicKeyTemplate("issue-0", VerificationRelationship.AssertionMethod), + ), + Nil + ) + for { + result <- operationFactory.makeCreateOperationHdKey("master-0", seed)(0, didTemplate) + (op, hdKey) = result + } yield assert(op.publicKeys.length)(equalTo(4)) && + assert(hdKey.internalKeyPaths.size)(equalTo(1)) && + assert(hdKey.keyPaths.size)(equalTo(3)) && + assert(hdKey.internalKeyPaths.get("master-0").get.keyIndex)(equalTo(0)) && + assert(hdKey.keyPaths.get("auth-0").get.keyIndex)(equalTo(0)) && + assert(hdKey.keyPaths.get("auth-1").get.keyIndex)(equalTo(1)) && + assert(hdKey.keyPaths.get("issue-0").get.keyIndex)(equalTo(0)) + } + ) + + private val makeUpdateOperationHdKeySpec = suite("makeUpdateOperationHdKeySpec ")( + test("make UpdateOperation from same seed is deterministic") { + val counter = HdKeyIndexCounter.zero(0) + val actions = + Seq(UpdateManagedDIDAction.AddKey(DIDPublicKeyTemplate("issue-42", VerificationRelationship.AssertionMethod))) + for { + result1 <- operationFactory.makeUpdateOperationHdKey(seed)(didExample, previousOperationHash, actions, counter) + (op1, hdKey1) = result1 + result2 <- operationFactory.makeUpdateOperationHdKey(seed)(didExample, previousOperationHash, actions, counter) + (op2, hdKey2) = result2 + } yield assert(op1)(equalTo(op2)) && assert(hdKey1)(equalTo(hdKey2)) + }, + test("make UpdateOperation correctly construct operation and increment counter") { + val counter = HdKeyIndexCounter + .zero(42) + .copy( + verificationRelationship = VerificationRelationshipCounter.zero.copy( + authentication = 3, + assertionMethod = 1, + ) + ) + val actions = Seq( + UpdateManagedDIDAction.AddKey(DIDPublicKeyTemplate("auth-42", VerificationRelationship.Authentication)), + UpdateManagedDIDAction.AddKey(DIDPublicKeyTemplate("issue-42", VerificationRelationship.AssertionMethod)), + ) + for { + result <- operationFactory.makeUpdateOperationHdKey(seed)(didExample, previousOperationHash, actions, counter) + (op, hdKey) = result + } yield { + // counter is correct + assert(hdKey.counter.didIndex)(equalTo(42)) && + assert(hdKey.counter.verificationRelationship.authentication)(equalTo(4)) && + assert(hdKey.counter.verificationRelationship.assertionMethod)(equalTo(2)) && + assert(hdKey.counter.verificationRelationship.capabilityDelegation)(equalTo(0)) && + assert(hdKey.counter.verificationRelationship.capabilityDelegation)(equalTo(0)) && + assert(hdKey.counter.verificationRelationship.keyAgreement)(equalTo(0)) && + assert(hdKey.counter.internalKey.master)(equalTo(0)) && + assert(hdKey.counter.internalKey.revocation)(equalTo(0)) && + // path is correct + assert(hdKey.newKeyPaths.size)(equalTo(2)) && + assert(hdKey.newKeyPaths.get("auth-42").get.keyIndex)(equalTo(3)) && + assert(hdKey.newKeyPaths.get("issue-42").get.keyIndex)(equalTo(1)) && + // operation is correct + assert(op.actions)(hasSize(equalTo(2))) && + assert(op.actions.collect { case UpdateDIDAction.AddKey(pk) => pk.id })( + hasSameElements(Seq("auth-42", "issue-42")) + ) + } + } + ) + +} diff --git a/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/util/SeedResolverSpec.scala b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/util/SeedResolverSpec.scala new file mode 100644 index 0000000000..305885e9d5 --- /dev/null +++ b/prism-agent/service/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/util/SeedResolverSpec.scala @@ -0,0 +1,60 @@ +package io.iohk.atala.agent.walletapi.util + +import zio.* +import zio.test.* +import zio.test.Assertion.* +import io.iohk.atala.agent.walletapi.crypto.ApolloSpecHelper + +object SeedResolverSpec extends ZIOSpecDefault, ApolloSpecHelper { + + override def spec = suite("SeedResolverSpec ")( + resolveSpec + ).provide(Runtime.removeDefaultLoggers) + + private val resolveSpec = suite("resolve")( + test("generate new seed if not set in env") { + val result = + for { + resolver <- ZIO.service[SeedResolver] + seed1 <- resolver.resolve + seed2 <- resolver.resolve + seed3 <- resolver.resolve + } yield assert(Set(seed1, seed2, seed3))(hasSize(equalTo(3))) + result.provide(SeedResolver.layer(), apolloLayer) + }, + test("read seed from env if set") { + val result = for { + _ <- TestSystem.putEnv("WALLET_SEED", "00" * 32) + seed <- ZIO.serviceWithZIO[SeedResolver](_.resolve) + } yield assert(seed)(equalTo(Array.fill(32)(0))) + result.provide(SeedResolver.layer(), apolloLayer) + }, + test("fail if seed from env in invalid") { + val result = for { + _ <- TestSystem.putEnv("WALLET_SEED", "xyz") + exit <- ZIO.serviceWithZIO[SeedResolver](_.resolve).exit + } yield assert(exit)(fails(anything)) + result.provide(SeedResolver.layer(), apolloLayer) + }, + test("read seed override if set") { + val result = for { + seed <- ZIO.serviceWithZIO[SeedResolver](_.resolve) + } yield assert(seed)(equalTo((Array.fill(32)(0)))) + result.provide(SeedResolver.layer(Some("00" * 32)), apolloLayer) + }, + test("fail if seed override is invalid") { + val result = for { + exit <- ZIO.serviceWithZIO[SeedResolver](_.resolve).exit + } yield assert(exit)(fails(anything)) + result.provide(SeedResolver.layer(Some("xyz")), apolloLayer) + }, + test("seed override take precedence over WALLET_SEED variable") { + val result = for { + _ <- TestSystem.putEnv("WALLET_SEED", "00" * 32) + seed <- ZIO.serviceWithZIO[SeedResolver](_.resolve) + } yield assert(seed)(equalTo((Array.fill(32)(1)))) + result.provide(SeedResolver.layer(Some("01" * 32)), apolloLayer) + } + ) + +}