From 830d32fc5e74f09362c894f14d52afeef8185b47 Mon Sep 17 00:00:00 2001 From: Yurii Shynbuiev Date: Mon, 16 Mar 2026 14:53:27 +0700 Subject: [PATCH 1/3] feat: normalize master key ID to "master" for deterministic PRISM DID Change DEFAULT_MASTER_KEY_ID from "master0" to "master" per the deterministic DID creation spec. The cloud-agent already uses CompressedECKeyData for secp256k1, so only the key ID normalization is needed for cross-platform DID determinism. Closes hyperledger-identus/cloud-agent#1732 Signed-off-by: Yurii Shynbuiev Co-Authored-By: Claude Opus 4.6 Signed-off-by: Yurii Shynbuiev --- .../walletapi/service/ManagedDIDService.scala | 2 +- .../service/ManagedDIDServiceSpec.scala | 2 +- .../walletapi/storage/StorageSpecHelper.scala | 2 +- .../walletapi/util/OperationFactorySpec.scala | 16 ++++++++-------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDService.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDService.scala index aa98ccaefc..b5bdad56e4 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDService.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDService.scala @@ -70,6 +70,6 @@ trait ManagedDIDService { } object ManagedDIDService { - val DEFAULT_MASTER_KEY_ID: String = "master0" + val DEFAULT_MASTER_KEY_ID: String = "master" val reservedKeyIds: Set[String] = Set(DEFAULT_MASTER_KEY_ID) } diff --git a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala index 11b2078d9f..2c1c7d7898 100644 --- a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala +++ b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala @@ -334,7 +334,7 @@ object ManagedDIDServiceSpec // this template will fail during validation for reserved key id val template = generateDIDTemplate( publicKeys = Seq( - DIDPublicKeyTemplate("master0", VerificationRelationship.Authentication, EllipticCurve.SECP256K1) + DIDPublicKeyTemplate("master", VerificationRelationship.Authentication, EllipticCurve.SECP256K1) ) ) val result = ZIO.serviceWithZIO[ManagedDIDService](_.createAndStoreDID(template)) diff --git a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/StorageSpecHelper.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/StorageSpecHelper.scala index 15f348ca1d..bfae751fdf 100644 --- a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/StorageSpecHelper.scala +++ b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/storage/StorageSpecHelper.scala @@ -44,7 +44,7 @@ trait StorageSpecHelper extends ApolloSpecHelper { protected def generateKeyPair() = apollo.secp256k1.generateKeyPair protected def generateCreateOperation(keyIds: Seq[String], didIndex: Int) = - OperationFactory(apollo).makeCreateOperation(KeyId("master0"), Array.fill(64)(0))( + OperationFactory(apollo).makeCreateOperation(KeyId("master"), Array.fill(64)(0))( didIndex, ManagedDIDTemplate( publicKeys = diff --git a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactorySpec.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactorySpec.scala index 6f7a3fe70f..9dac4f4a78 100644 --- a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactorySpec.scala +++ b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactorySpec.scala @@ -31,9 +31,9 @@ object OperationFactorySpec extends ZIOSpecDefault, ApolloSpecHelper { test("make CrateOperation from same seed is deterministic") { val didTemplate = ManagedDIDTemplate(publicKeys = Nil, services = Nil, contexts = Nil) for { - result1 <- operationFactory.makeCreateOperation(KeyId("master0"), seed)(0, didTemplate) + result1 <- operationFactory.makeCreateOperation(KeyId("master"), seed)(0, didTemplate) (op1, hdKey1) = result1 - result2 <- operationFactory.makeCreateOperation(KeyId("master0"), seed)(0, didTemplate) + result2 <- operationFactory.makeCreateOperation(KeyId("master"), seed)(0, didTemplate) (op2, hdKey2) = result2 } yield assert(op1)(equalTo(op2)) && assert(hdKey1)(equalTo(hdKey2)) @@ -41,11 +41,11 @@ object OperationFactorySpec extends ZIOSpecDefault, ApolloSpecHelper { test("make CreateOperation must contain 1 master key") { val didTemplate = ManagedDIDTemplate(publicKeys = Nil, services = Nil, contexts = Nil) for { - result <- operationFactory.makeCreateOperation(KeyId("master-0"), seed)(0, didTemplate) + result <- operationFactory.makeCreateOperation(KeyId("master"), 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.id)(equalTo("master")) && assert(pk.purpose)(equalTo(InternalKeyPurpose.Master)) }, test("make CreateOperation containing multiple key purposes") { @@ -59,12 +59,12 @@ object OperationFactorySpec extends ZIOSpecDefault, ApolloSpecHelper { contexts = Nil ) for { - result <- operationFactory.makeCreateOperation(KeyId("master-0"), seed)(0, didTemplate) + result <- operationFactory.makeCreateOperation(KeyId("master"), seed)(0, didTemplate) (op, keys) = result } yield assert(op.publicKeys.length)(equalTo(4)) && assert(keys.hdKeys.size)(equalTo(4)) && assert(keys.randKeys)(isEmpty) && - assert(keys.hdKeys.get("master-0").get.keyIndex)(equalTo(0)) && + assert(keys.hdKeys.get("master").get.keyIndex)(equalTo(0)) && assert(keys.hdKeys.get("auth-0").get.keyIndex)(equalTo(0)) && assert(keys.hdKeys.get("auth-1").get.keyIndex)(equalTo(1)) && assert(keys.hdKeys.get("issue-0").get.keyIndex)(equalTo(0)) @@ -80,7 +80,7 @@ object OperationFactorySpec extends ZIOSpecDefault, ApolloSpecHelper { contexts = Nil ) for { - result <- operationFactory.makeCreateOperation(KeyId("master-0"), seed)(0, didTemplate) + result <- operationFactory.makeCreateOperation(KeyId("master"), seed)(0, didTemplate) (op, keys) = result publicKeyData = op.publicKeys.map { case PublicKey(id, _, publicKeyData) => id -> publicKeyData @@ -107,7 +107,7 @@ object OperationFactorySpec extends ZIOSpecDefault, ApolloSpecHelper { ) && assert(keys.hdKeys.size)(equalTo(2)) && assert(keys.randKeys.size)(equalTo(2)) && - assert(keys.hdKeys.get("master-0").get.keyIndex)(equalTo(0)) && + assert(keys.hdKeys.get("master").get.keyIndex)(equalTo(0)) && assert(keys.hdKeys.get("auth-0").get.keyIndex)(equalTo(0)) && assert(keys.randKeys.get("auth-1").get.keyPair)(isSubtype[Ed25519KeyPair](anything)) && assert(keys.randKeys.get("comm-0").get.keyPair)(isSubtype[X25519KeyPair](anything)) From 6b1df4a2cd3c230354ff508e737b5fc3070063f7 Mon Sep 17 00:00:00 2001 From: Yurii Shynbuiev Date: Mon, 16 Mar 2026 17:13:52 +0700 Subject: [PATCH 2/3] refactor: CreateDID operation contains only master key CreateDIDOperation now includes only the master key with CompressedECKeyData. All other keys, services, and contexts must be added via subsequent UpdateDIDOperation. Keys are still derived and stored during createAndStoreDID for use in subsequent UpdateDID operations. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Yurii Shynbuiev --- .../walletapi/util/OperationFactory.scala | 12 +++---- .../service/ManagedDIDServiceSpec.scala | 19 +++++------ .../walletapi/util/OperationFactorySpec.scala | 33 +++++++++---------- 3 files changed, 31 insertions(+), 33 deletions(-) diff --git a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactory.scala b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactory.scala index 20bc24d976..a689242a3a 100644 --- a/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactory.scala +++ b/cloud-agent/service/wallet-api/src/main/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactory.scala @@ -67,13 +67,13 @@ class OperationFactory(apollo: Apollo) { .map(outcome => (keys :+ outcome, outcome.nextCounter)) } (derivedInternalKeys, _) = derivedInternalKeysWithCounter + // CreateDIDOperation only contains the master key with CompressedECKeyData. + // All other keys (authentication, issuance), services, and context + // must be added via subsequent UpdateDIDOperation. operation = PrismDIDOperation.Create( - publicKeys = hdKeysWithCounter._1.map(_._1) ++ - randKeys.map(_.publicKey) ++ - Seq(masterKeyOutcome.publicKey) ++ - derivedInternalKeys.map(_.publicKey), - services = didTemplate.services, - context = didTemplate.contexts + publicKeys = Seq(masterKeyOutcome.publicKey), + services = Seq.empty, + context = Seq.empty ) keys = CreateDIDKey( hdKeys = hdKeysWithCounter._1.map(i => i.publicKey.id.value -> i.path).toMap ++ diff --git a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala index 2c1c7d7898..7a1293ec0e 100644 --- a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala +++ b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/service/ManagedDIDServiceSpec.scala @@ -293,7 +293,7 @@ object ManagedDIDServiceSpec assert(key3KeyPair)(isSubtype[Ed25519KeyPair](anything)) && assert(key4KeyPair)(isSubtype[X25519KeyPair](anything)) }, - test("created DID have corresponding public keys in CreateOperation") { + test("created DID CreateOperation contains only master key regardless of template") { val template = generateDIDTemplate( publicKeys = Seq( DIDPublicKeyTemplate("key1", VerificationRelationship.Authentication, EllipticCurve.SECP256K1), @@ -308,16 +308,15 @@ object ManagedDIDServiceSpec createOperation <- ZIO.fromOption(state.collect { case ManagedDIDState(operation, _, PublicationState.Created()) => operation }) + // CreateDIDOperation only contains master key; other keys go in subsequent UpdateDIDOperation publicKeys = createOperation.publicKeys.collect { case pk: PublicKey => pk } - } yield assert(publicKeys.map(i => i.id -> i.purpose))( - hasSameElements( - Seq( - "key1" -> VerificationRelationship.Authentication, - "key2" -> VerificationRelationship.KeyAgreement, - "key3" -> VerificationRelationship.AssertionMethod - ) - ) - ) + internalKeys = createOperation.publicKeys.collect { case pk: InternalPublicKey => pk } + } yield assert(publicKeys)(isEmpty) && + assert(internalKeys)(hasSize(equalTo(1))) && + assert(internalKeys.head.id)(equalTo("master")) && + assert(internalKeys.head.purpose)(equalTo(InternalKeyPurpose.Master)) && + assert(createOperation.services)(isEmpty) && + assert(createOperation.context)(isEmpty) }, test("created DID contain at least 1 master key in CreateOperation") { for { diff --git a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactorySpec.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactorySpec.scala index 9dac4f4a78..9815d94e78 100644 --- a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactorySpec.scala +++ b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactorySpec.scala @@ -48,7 +48,7 @@ object OperationFactorySpec extends ZIOSpecDefault, ApolloSpecHelper { assert(pk.id)(equalTo("master")) && assert(pk.purpose)(equalTo(InternalKeyPurpose.Master)) }, - test("make CreateOperation containing multiple key purposes") { + test("make CreateOperation containing multiple key purposes has only master key in operation") { val didTemplate = ManagedDIDTemplate( Seq( DIDPublicKeyTemplate("auth-0", VerificationRelationship.Authentication, EllipticCurve.SECP256K1), @@ -61,7 +61,12 @@ object OperationFactorySpec extends ZIOSpecDefault, ApolloSpecHelper { for { result <- operationFactory.makeCreateOperation(KeyId("master"), seed)(0, didTemplate) (op, keys) = result - } yield assert(op.publicKeys.length)(equalTo(4)) && + pk = op.publicKeys.head.asInstanceOf[InternalPublicKey] + } yield // CreateDIDOperation only contains master key + assert(op.publicKeys)(hasSize(equalTo(1))) && + assert(pk.id)(equalTo("master")) && + assert(pk.purpose)(equalTo(InternalKeyPurpose.Master)) && + // But all keys are still derived and stored for subsequent UpdateDIDOperation assert(keys.hdKeys.size)(equalTo(4)) && assert(keys.randKeys)(isEmpty) && assert(keys.hdKeys.get("master").get.keyIndex)(equalTo(0)) && @@ -69,7 +74,7 @@ object OperationFactorySpec extends ZIOSpecDefault, ApolloSpecHelper { assert(keys.hdKeys.get("auth-1").get.keyIndex)(equalTo(1)) && assert(keys.hdKeys.get("issue-0").get.keyIndex)(equalTo(0)) }, - test("make CreateOperation containing multiple key types") { + test("make CreateOperation with multiple key types has only master key in operation") { val didTemplate = ManagedDIDTemplate( Seq( DIDPublicKeyTemplate("auth-0", VerificationRelationship.Authentication, EllipticCurve.SECP256K1), @@ -86,25 +91,19 @@ object OperationFactorySpec extends ZIOSpecDefault, ApolloSpecHelper { case PublicKey(id, _, publicKeyData) => id -> publicKeyData case InternalPublicKey(id, _, publicKeyData) => id -> publicKeyData }.toMap - } yield assert(publicKeyData.size)(equalTo(4)) && - assert(publicKeyData.get(KeyId("auth-0")).get)( + masterPk = op.publicKeys.head.asInstanceOf[InternalPublicKey] + } yield // CreateDIDOperation only contains master key with CompressedECKeyData + assert(op.publicKeys)(hasSize(equalTo(1))) && + assert(masterPk.id)(equalTo("master")) && + assert(publicKeyData.get(KeyId("master")).get)( isSubtype[PublicKeyData.ECCompressedKeyData]( hasField[PublicKeyData.ECCompressedKeyData, Int]("data", _.data.toByteArray.length, equalTo(33)) && hasField("crv", _.crv, equalTo(EllipticCurve.SECP256K1)) ) ) && - assert(publicKeyData.get(KeyId("auth-1")).get)( - isSubtype[PublicKeyData.ECCompressedKeyData]( - hasField[PublicKeyData.ECCompressedKeyData, Int]("data", _.data.toByteArray.length, equalTo(32)) && - hasField("crv", _.crv, equalTo(EllipticCurve.ED25519)) - ) - ) && - assert(publicKeyData.get(KeyId("comm-0")).get)( - isSubtype[PublicKeyData.ECCompressedKeyData]( - hasField[PublicKeyData.ECCompressedKeyData, Int]("data", _.data.toByteArray.length, equalTo(32)) && - hasField("crv", _.crv, equalTo(EllipticCurve.X25519)) - ) - ) && + assert(op.services)(isEmpty) && + assert(op.context)(isEmpty) && + // But all keys are still derived and stored for subsequent UpdateDIDOperation assert(keys.hdKeys.size)(equalTo(2)) && assert(keys.randKeys.size)(equalTo(2)) && assert(keys.hdKeys.get("master").get.keyIndex)(equalTo(0)) && From 86db4f3bb9e9480ce1c2502cbb85a554535d3b41 Mon Sep 17 00:00:00 2001 From: Yurii Shynbuiev Date: Mon, 16 Mar 2026 17:51:33 +0700 Subject: [PATCH 3/3] test: add spec test vector for deterministic DID creation Verify that raw spec seed produces: - Compressed pubkey: 023f7c75c9e5fba08fea1640d6faa3f8dc0151261d2b56026d46ddcbe1fc5a5bbb - Canonical DID: did:prism:35fbaf7f8a68e927feb89dc897f4edc24ca8d7510261829e4834d931e947e6ca Co-Authored-By: Claude Opus 4.6 Signed-off-by: Yurii Shynbuiev --- .../walletapi/util/OperationFactorySpec.scala | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactorySpec.scala b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactorySpec.scala index 9815d94e78..cc71c0dc80 100644 --- a/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactorySpec.scala +++ b/cloud-agent/service/wallet-api/src/test/scala/org/hyperledger/identus/agent/walletapi/util/OperationFactorySpec.scala @@ -74,6 +74,29 @@ object OperationFactorySpec extends ZIOSpecDefault, ApolloSpecHelper { assert(keys.hdKeys.get("auth-1").get.keyIndex)(equalTo(1)) && assert(keys.hdKeys.get("issue-0").get.keyIndex)(equalTo(0)) }, + test("spec test vector: raw seed produces expected canonical DID") { + // Spec test vector from: + // https://github.com/input-output-hk/prism-did-method-spec/blob/main/extensions/deterministic-prism-did-generation-proposal.md#examples--test-vector + val specSeed = HexString + .fromStringUnsafe( + "3b32a5049f2b4e3af31ec5c1ae75fada1ad2eb8be5accf56ada343ad89eeb083208e538b3b97836e3bd7048c131421bf5bea9e3a1d25812a2d831e2bab89e058" + ) + .toByteArray + val didTemplate = ManagedDIDTemplate(publicKeys = Nil, services = Nil, contexts = Nil) + for { + result <- operationFactory.makeCreateOperation(KeyId("master"), specSeed)(0, didTemplate) + (op, _) = result + did = op.did + } yield { + // Verify the compressed public key matches the spec test vector + val masterKeyData = op.publicKeys.head.asInstanceOf[InternalPublicKey].publicKeyData + .asInstanceOf[PublicKeyData.ECCompressedKeyData] + val compressedPubKeyHex = HexString.fromByteArray(masterKeyData.data.toByteArray).toString + assert(compressedPubKeyHex)(equalTo("023f7c75c9e5fba08fea1640d6faa3f8dc0151261d2b56026d46ddcbe1fc5a5bbb")) && + // Verify the canonical DID matches the spec test vector exactly + assert(did.toString)(equalTo("did:prism:35fbaf7f8a68e927feb89dc897f4edc24ca8d7510261829e4834d931e947e6ca")) + } + }, test("make CreateOperation with multiple key types has only master key in operation") { val didTemplate = ManagedDIDTemplate( Seq(