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/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 11b2078d9f..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 { @@ -334,7 +333,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..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 @@ -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,14 +41,14 @@ 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") { + test("make CreateOperation containing multiple key purposes has only master key in operation") { val didTemplate = ManagedDIDTemplate( Seq( DIDPublicKeyTemplate("auth-0", VerificationRelationship.Authentication, EllipticCurve.SECP256K1), @@ -59,17 +59,45 @@ 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)) && + 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-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)) }, - test("make CreateOperation containing multiple key types") { + 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( DIDPublicKeyTemplate("auth-0", VerificationRelationship.Authentication, EllipticCurve.SECP256K1), @@ -80,34 +108,28 @@ 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 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-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))