diff --git a/cmd/hid-noded/cmd/generate_ssi.go b/cmd/hid-noded/cmd/generate_ssi.go new file mode 100644 index 0000000..102808b --- /dev/null +++ b/cmd/hid-noded/cmd/generate_ssi.go @@ -0,0 +1,261 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/hypersign-protocol/hid-node/app" + "github.com/hypersign-protocol/hid-node/x/ssi/types" + "github.com/multiformats/go-multibase" + "github.com/spf13/cobra" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const fromFlag = "from" +const didAliasFlag = "did-alias" +const keyringBackendFlag = "keyring-backend" +const didNamespaceFlag = "did-namespace" + +func generateSSICmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "ssi-tools", + Short: "commands to experiment around Self Sovereign Identity (SSI) documents", + } + + cmd.AddCommand(generateDidCmd()) + cmd.AddCommand(showDidByAliasCmd()) + cmd.AddCommand(listAllDidAliasesCmd()) + + return cmd +} + +func listAllDidAliasesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list-did-aliases", + Short: "List all DID Document alias names", + RunE: func(cmd *cobra.Command, _ []string) error { + didAliasConfig, err := types.GetDidAliasConfig(cmd) + if err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + result := []map[string]string{} + + if _, err := os.Stat(didAliasConfig.DidAliasDir); err != nil { + if os.IsNotExist(err) { + fmt.Fprintf(cmd.ErrOrStderr(), "%v\n\n", []string{}) + return nil + } + } + didJsonFiles, err := os.ReadDir(didAliasConfig.DidAliasDir) + if err != nil { + return err + } + + // Consider only those files whose extensions are '.json' + for _, didJsonFile := range didJsonFiles { + isDidJsonFile := !didJsonFile.IsDir() && (strings.Split(didJsonFile.Name(), ".")[1] == "json") + if isDidJsonFile { + unit := map[string]string{} + didDocBytes, err := os.ReadFile(filepath.Join(didAliasConfig.DidAliasDir, didJsonFile.Name())) + if err != nil { + return err + } + + var didDoc types.Did + err = clientCtx.Codec.UnmarshalJSON(didDocBytes, &didDoc) + if err != nil { + // Ignore any files which are not able to parse into type.Did + continue + } + + unit["did"] = didDoc.Id + unit["alias"] = strings.Split(didJsonFile.Name(), ".")[0] + result = append(result, unit) + } else { + continue + } + } + + // Indent Map + resultBytes, err := json.MarshalIndent(result, "", " ") + if err != nil { + return err + } + + _, err = fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", string(resultBytes)) + return err + }, + } + return cmd +} + +func showDidByAliasCmd() *cobra.Command { + exampleString := "hid-noded ssi-tools show-did-by-alias didsample3" + + cmd := &cobra.Command{ + Use: "show-did-by-alias [alias-name]", + Args: cobra.ExactArgs(1), + Example: exampleString, + Short: "Retrieve the Did Document by a alias name", + RunE: func(cmd *cobra.Command, args []string) error { + didAliasConfig, err := types.GetDidAliasConfig(cmd) + if err != nil { + return err + } + + aliasName := args[0] + aliasFile := aliasName + ".json" + + if _, err := os.Stat(didAliasConfig.DidAliasDir); err != nil { + if os.IsNotExist(err) { + fmt.Fprintf(cmd.ErrOrStderr(), "DID Document alias '%v' does not exist\n", aliasName) + return nil + } + } + + didDocBytes, err := os.ReadFile(filepath.Join(didAliasConfig.DidAliasDir, aliasFile)) + if err != nil { + fmt.Fprintf(cmd.ErrOrStderr(), "DID Document alias '%v' does not exist\n", aliasName) + return nil + } + + _, err = fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", string(didDocBytes)) + return err + }, + } + + return cmd +} + +func generateDidCmd() *cobra.Command { + exampleString1 := "hid-noded ssi-tools generate-did --from hid1kspgn6f5hmurulx4645ch6rf0kt90jpv5ydykp --keyring-backend test --did-alias example1" + exampleString2 := "hid-noded ssi-tools generate-did --from node1 --keyring-backend test --did-alias example2" + exampleString3 := "hid-noded ssi-tools generate-did --from node1 --keyring-backend test --did-alias example3 --did-namespace devnet" + + cmd := &cobra.Command{ + Use: "generate-did", + Short: "Generates a DID Document", + Example: exampleString1 + "\n" + exampleString2 + "\n" + exampleString3, + RunE: func(cmd *cobra.Command, _ []string) error { + // Get the flags + account, err := cmd.Flags().GetString(fromFlag) + if err != nil { + return err + } + if account == "" { + return fmt.Errorf("no value provided for --from flag") + } + + didAlias, err := cmd.Flags().GetString(didAliasFlag) + if err != nil { + return err + } + if didAlias == "" { + return fmt.Errorf("no value provided for --did-alias flag") + } + + keyringBackend, err := cmd.Flags().GetString(keyringBackendFlag) + if err != nil { + return err + } + if keyringBackend == "" { + return fmt.Errorf("no value provided for --keyring-backend flag") + } + + didNamespace, err := cmd.Flags().GetString(didNamespaceFlag) + if err != nil { + return err + } + + // Get Public Key from keyring account + var kr keyring.Keyring + appName := "hid-noded-keyring" + didAliasConfig, err := types.GetDidAliasConfig(cmd) + if err != nil { + return err + } + + switch keyringBackend { + case "test": + kr, err = keyring.New(appName, "test", didAliasConfig.HidNodeConfigDir, nil) + if err != nil { + return err + } + default: + return fmt.Errorf("unsupported keyring-backend : %v", keyringBackend) + } + + // Handle both key name as well as key address + var userKeyInfo keyring.Info + var errAccountFetch error + + userKeyInfo, errAccountFetch = kr.Key(account) + if errAccountFetch != nil { + if accountAddr, err := sdk.AccAddressFromBech32(account); err != nil { + return err + } else { + userKeyInfo, errAccountFetch = kr.KeyByAddress(accountAddr) + if errAccountFetch != nil { + return errAccountFetch + } + } + } + + pubKeyBytes := userKeyInfo.GetPubKey().Bytes() + pubKeyMultibase, err := multibase.Encode(multibase.Base58BTC, pubKeyBytes) + if err != nil { + return err + } + userBlockchainAddress := sdk.MustBech32ifyAddressBytes( + app.AccountAddressPrefix, + userKeyInfo.GetAddress().Bytes(), + ) + + // Generate a DID document with both publicKeyMultibase and blockchainAccountId + didDoc := generateDidDoc(didNamespace, pubKeyMultibase, userBlockchainAddress) + + // Construct the JSON and store it in $HOME/.hid-node/generated-ssi-docs + if _, err := os.Stat(didAliasConfig.DidAliasDir); err != nil { + if os.IsNotExist(err) { + if err := os.Mkdir(didAliasConfig.DidAliasDir, os.ModePerm); err != nil { + return err + } + } else { + return err + } + } + + didJsonBytes, err := json.MarshalIndent(didDoc, "", " ") + if err != nil { + return err + } + didJsonFilename := didAlias + ".json" + didJsonPath := filepath.Join(didAliasConfig.DidAliasDir, didJsonFilename) + if err := os.WriteFile(didJsonPath, didJsonBytes, 0644); err != nil { + return err + } + + _, err = fmt.Fprintf(cmd.ErrOrStderr(), "DID Document alias '%v' (didId: %v) has been successfully generated at %v\n", didAlias, didDoc.Id, didJsonPath) + + return err + }, + } + + cmd.Flags().String(fromFlag, "", "name of account while will sign the DID Document") + cmd.Flags().String(didAliasFlag, "", "alias of the generated DID Document which can be referred to while registering on-chain") + cmd.Flags().String(keyringBackendFlag, "", "supported keyring backend: (test)") + cmd.Flags().String(didNamespaceFlag, "", "namespace of DID Document Id") + return cmd +} diff --git a/cmd/hid-noded/cmd/generate_ssi_utils.go b/cmd/hid-noded/cmd/generate_ssi_utils.go new file mode 100644 index 0000000..e07d44b --- /dev/null +++ b/cmd/hid-noded/cmd/generate_ssi_utils.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "github.com/hypersign-protocol/hid-node/x/ssi/types" +) + +func formDidId(didNamespace string, publicKeyMultibase string) string { + if didNamespace != "" { + return types.DocumentIdentifierDid + ":" + types.DidMethod + ":" + didNamespace + ":" + publicKeyMultibase + } else { + return types.DocumentIdentifierDid + ":" + types.DidMethod + ":" + publicKeyMultibase + } +} + +func generateDidDoc(didNamespace string, publicKeyMultibase string, userAddress string) *types.Did { + didId := formDidId(didNamespace, publicKeyMultibase) + + return &types.Did{ + Id: didId, + Controller: []string{didId}, + VerificationMethod: []*types.VerificationMethod{ + { + Id: didId + "#k1", + Type: types.EcdsaSecp256k1VerificationKey2019, + Controller: didId, + PublicKeyMultibase: publicKeyMultibase, + BlockchainAccountId: types.CosmosCAIP10Prefix + ":jagrat:" + userAddress, + }, + }, + } +} diff --git a/cmd/hid-noded/cmd/root.go b/cmd/hid-noded/cmd/root.go index fadc673..3aec51e 100644 --- a/cmd/hid-noded/cmd/root.go +++ b/cmd/hid-noded/cmd/root.go @@ -114,6 +114,7 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) { ) rootCmd.AddCommand(server.RosettaCommand(encodingConfig.InterfaceRegistry, encodingConfig.Codec)) + rootCmd.AddCommand(generateSSICmd()) } func addModuleInitFlags(startCmd *cobra.Command) { diff --git a/localnetsetup.sh b/localnetsetup.sh old mode 100644 new mode 100755 index 25db901..e4bed8a --- a/localnetsetup.sh +++ b/localnetsetup.sh @@ -38,7 +38,7 @@ cat $HOME/.hid-node/config/genesis.json | jq '.app_state["gov"]["deposit_params" cat $HOME/.hid-node/config/genesis.json | jq '.app_state["gov"]["voting_params"]["voting_period"]="50s"' > $HOME/.hid-node/config/tmp_genesis.json && mv $HOME/.hid-node/config/tmp_genesis.json $HOME/.hid-node/config/genesis.json # update ssi genesis -cat $HOME/.hid-node/config/genesis.json | jq '.app_state["ssi"]["chain_namespace"]="testnet"' > $HOME/.hid-node/config/tmp_genesis.json && mv $HOME/.hid-node/config/tmp_genesis.json $HOME/.hid-node/config/genesis.json +cat $HOME/.hid-node/config/genesis.json | jq '.app_state["ssi"]["chain_namespace"]="devnet"' > $HOME/.hid-node/config/tmp_genesis.json && mv $HOME/.hid-node/config/tmp_genesis.json $HOME/.hid-node/config/genesis.json # update mint genesis cat $HOME/.hid-node/config/genesis.json | jq '.app_state["mint"]["params"]["mint_denom"]="uhid"' > $HOME/.hid-node/config/tmp_genesis.json && mv $HOME/.hid-node/config/tmp_genesis.json $HOME/.hid-node/config/genesis.json @@ -59,6 +59,12 @@ sed -i -E 's|allow_duplicate_ip = false|allow_duplicate_ip = true|g' $HOME/.hid- sed -i -E 's|addr_book_strict = true|addr_book_strict = false|g' $HOME/.hid-node/config/config.toml sed -i -E 's|cors_allowed_origins = \[\]|cors_allowed_origins = \[\"\*\"\]|g' $HOME/.hid-node/config/config.toml -echo -e "\nConfiguarations set, you are ready to run hid-noded now!" +echo -e "\nConfiguration set up is done, you are ready to run hid-noded now!" + +echo -e "\nPlease note the important chain configurations below:" + +echo -e "\nRPC server address: http://localhost:26657" +echo -e "API server address: http://localhost:1317" +echo -e "DID Namespace: devnet" echo -e "\nEnter the command 'hid-noded start' to start a single node blockchain." diff --git a/tests/e2e/ssi_tests/e2e_tests.py b/tests/e2e/ssi_tests/e2e_tests.py index e065f61..968e87a 100644 --- a/tests/e2e/ssi_tests/e2e_tests.py +++ b/tests/e2e/ssi_tests/e2e_tests.py @@ -495,7 +495,7 @@ def deactivate_did(): print("2. PASS: Mike creates a DID for himself, but the controller list is empty. Mike attempts to deactivate it \n") - # Register Alice's DID + # Register Mike's DID kp_mike = generate_key_pair() signers = [] did_doc_string = generate_did_document(kp_mike) @@ -518,6 +518,36 @@ def deactivate_did(): deactivate_tx_cmd = form_did_deactivate_tx_multisig(did_doc_mike, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) run_blockchain_command(deactivate_tx_cmd, f"Deactivation of Mike's DID with Id: {did_doc_mike}") + print("3. FAIL: Mike creates a DID for himself, but the controller list is empty. Mike deactivates it and then attempts to updates it. \n") + + kp_mike = generate_key_pair() + signers = [] + did_doc_string = generate_did_document(kp_mike) + did_doc_string["controller"] = [] + did_doc_mike = did_doc_string["id"] + did_doc_mike_vm = did_doc_string["verificationMethod"][0] + signPair_mike = { + "kp": kp_mike, + "verificationMethodId": did_doc_mike_vm["id"], + "signing_algo": "ed25519" + } + signers.append(signPair_mike) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering of Mike's DID with Id: {did_doc_mike}") + + # Deactivate DID + signers = [] + signers.append(signPair_mike) + deactivate_tx_cmd = form_did_deactivate_tx_multisig(did_doc_mike, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(deactivate_tx_cmd, f"Deactivation of Mike's DID with Id: {did_doc_mike}") + + # Attempt to update deactivated DID + signers = [] + signers.append(signPair_mike) + did_doc_string["context"] = ["hii"] + update_tx_cmd = form_did_update_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(update_tx_cmd, f"Bob (non-controller) attempts to update Org DID with Id: {did_doc_org}", True) + print("--- Test Completed ---\n") def schema_test(): @@ -812,6 +842,7 @@ def caip10_cosmos_support_test(): did_doc_string_2 = generate_did_document(kp, kp_algo, "osmo") did_doc_string_2_vm = did_doc_string_2["verificationMethod"][0] did_doc_string_2_vm["id"] = did_doc_string_2_vm["id"] + "new" + did_doc_string_2_vm["controller"] = did_doc_string_1["verificationMethod"][0]["controller"] did_doc_string_1["verificationMethod"] = [ did_doc_string_1["verificationMethod"][0], @@ -998,7 +1029,7 @@ def vm_type_test(): print("4. PASS: Registering DID Document with a verification method of type EcdsaSecp256k1VerificationKey2019. Only publicKeyMultibase is passed.") kp_algo = "secp256k1" kp = generate_key_pair(algo=kp_algo) - did_doc_string = generate_did_document(kp, kp_algo) + did_doc_string = generate_did_document(kp, kp_algo, is_uuid=True, bech32prefix="") did_doc_id = did_doc_string["id"] did_doc_string["verificationMethod"][0]["blockchainAccountId"] = "" signers = [] @@ -1092,4 +1123,131 @@ def vm_type_test(): run_blockchain_command(create_tx_cmd, f"Registering DID with Id: {did_doc_id}") print("--- Test Completed ---\n") - \ No newline at end of file + +def method_specific_id_test(): + print("\n--- Method Specific ID Tests ---\n") + + print("1. PASS: Registering a DID Document where the user provides a blockchain address in MSI that they own") + + kp_algo = "secp256k1" + kp_alice = generate_key_pair(algo=kp_algo) + signers = [] + did_doc_string = generate_did_document(kp_alice, algo=kp_algo) + did_doc_alice = did_doc_string["id"] + did_doc_alice_vm = did_doc_string["verificationMethod"] + signPair_alice = { + "kp": kp_alice, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair_alice) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering Alice's DID with Id: {did_doc_alice}") + + print("2. FAIL: Registering a DID Document where the user provides a blockchain address in MSI that they don't own") + + kp_algo = "secp256k1" + kp_bob = generate_key_pair(algo=kp_algo) + signers = [] + did_doc_string = generate_did_document(kp_bob, algo=kp_algo) + did_doc_string["controller"] = [did_doc_alice] + did_doc_string["verificationMethod"] = did_doc_alice_vm + + did_doc_bob = did_doc_string["id"] + signers.append(signPair_alice) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering Bob's DID with Id: {did_doc_bob}", True) + + print("3. PASS: Registering a DID Document where the user provides a multibase encoded public key in MSI that they own") + + kp_algo = "ed25519" + kp_alice = generate_key_pair(algo=kp_algo) + signers = [] + did_doc_string = generate_did_document(kp_alice, algo=kp_algo) + did_doc_alice = did_doc_string["id"] + did_doc_alice_vm = did_doc_string["verificationMethod"] + signPair_alice = { + "kp": kp_alice, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair_alice) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering Alice's DID with Id: {did_doc_alice}") + + print("4. PASS: Registering a DID Document where the user provides a multibase encoded public key in MSI that they don't own") + + kp_algo = "ed25519" + kp_bob = generate_key_pair(algo=kp_algo) + signers = [] + did_doc_string = generate_did_document(kp_bob, algo=kp_algo) + did_doc_string["controller"] = [did_doc_alice] + did_doc_string["verificationMethod"] = did_doc_alice_vm + + did_doc_bob = did_doc_string["id"] + signers.append(signPair_alice) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering Bob's DID with Id: {did_doc_bob}") + + print("5. FAIL: Attempt to Register Invalid DID Documents with invalid DID Id") + + did_id_list = [ + "did:hid:1:", + "did:hid:1", + "did:hid:devnet", + "did:hid:devnet:", + "did:hid:devnet:zHiii", + "did:hid:devnet:asa54qf", + "did:hid:devnet:asa54qf|sds", + "did:hid:devnet:-asa54-qfsds", + "did:hid:devnet:.com", + ] + + for did_id in did_id_list: + did_doc_string["id"] = did_id + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering Invalid DID with Id: {did_id}", True, True) + + print("6. PASS: Alice tries to update their DID Document by removing the Verification Method associated with the method specific id (CAIP-10 Blockchain Address)") + + kp_algo = "secp256k1" + kp_alice = generate_key_pair(algo=kp_algo) + signers = [] + did_doc_string = generate_did_document(kp_alice, algo=kp_algo) + did_doc_alice = did_doc_string["id"] + did_doc_alice_vm = did_doc_string["verificationMethod"] + signPair_alice = { + "kp": kp_alice, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair_alice) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering Alice's DID with Id: {did_doc_alice}") + + did_doc_string["verificationMethod"] = [] + update_tx_cmd = form_did_update_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(update_tx_cmd, f"Removal of Verification Method associated with method specific id") + + print("7. PASS: Alice tries to update their DID Document by removing the Verification Method associated with the method specific id (Multibase Encoded PublicKey)") + + kp_algo = "ed25519" + kp_alice = generate_key_pair(algo=kp_algo) + signers = [] + did_doc_string = generate_did_document(kp_alice, algo=kp_algo) + did_doc_alice = did_doc_string["id"] + did_doc_alice_vm = did_doc_string["verificationMethod"] + signPair_alice = { + "kp": kp_alice, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair_alice) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering Alice's DID with Id: {did_doc_alice}") + + did_doc_string["verificationMethod"] = [] + update_tx_cmd = form_did_update_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(update_tx_cmd, f"Removal of Verification Method associated with method specific id") + + print("--- Test Completed ---\n") \ No newline at end of file diff --git a/tests/e2e/ssi_tests/generate_doc.py b/tests/e2e/ssi_tests/generate_doc.py index ecdc3e5..f668520 100644 --- a/tests/e2e/ssi_tests/generate_doc.py +++ b/tests/e2e/ssi_tests/generate_doc.py @@ -6,7 +6,7 @@ from utils import run_command, generate_document_id, get_document_signature, \ secp256k1_pubkey_to_address -def generate_did_document(key_pair, algo="ed25519", bech32prefix="hid"): +def generate_did_document(key_pair, algo="ed25519", bech32prefix="hid", is_uuid=False): base_document = { "context" : [ "https://www.w3.org/ns/did/v1" @@ -17,7 +17,7 @@ def generate_did_document(key_pair, algo="ed25519", bech32prefix="hid"): "authentication": [], } - did_id = generate_document_id("did", key_pair, algo) + did_id = generate_document_id("did", key_pair, algo, is_uuid) # Form the DID Document vm_type = "" @@ -31,12 +31,13 @@ def generate_did_document(key_pair, algo="ed25519", bech32prefix="hid"): raise Exception("unknown signing algorithm: " + key_pair) verification_method = { - "id": did_id + "#key-1", - "type": vm_type, - "controller": did_id, + "id": "", + "type": "", + "controller": "", "blockchainAccountId": "", "publicKeyMultibase": "" } + if algo == "recover-eth": verification_method["blockchainAccountId"] = "eip155:1:" + key_pair["ethereum_address"] elif algo == "secp256k1": @@ -44,16 +45,22 @@ def generate_did_document(key_pair, algo="ed25519", bech32prefix="hid"): if bech32prefix == "hid": verification_method["blockchainAccountId"] = "cosmos:jagrat:" + \ secp256k1_pubkey_to_address(key_pair["pub_key_base_64"], bech32prefix) + did_id = "did:hid:devnet:" + verification_method["blockchainAccountId"] elif bech32prefix == "osmo": verification_method["blockchainAccountId"] = "cosmos:osmosis-1:" + \ secp256k1_pubkey_to_address(key_pair["pub_key_base_64"], bech32prefix) + did_id = "did:hid:devnet:" + verification_method["blockchainAccountId"] else: - raise Exception("unsupported bech32 prefix " + bech32prefix) + verification_method["blockchainAccountId"] = "" verification_method["publicKeyMultibase"] = key_pair["pub_key_multibase"] else: verification_method["publicKeyMultibase"] = key_pair["pub_key_multibase"] + verification_method["controller"] = did_id + verification_method["type"] = vm_type + verification_method["id"] = did_id + "#k1" + base_document["id"] = did_id base_document["controller"] = [did_id] base_document["verificationMethod"] = [verification_method] diff --git a/tests/e2e/ssi_tests/run.py b/tests/e2e/ssi_tests/run.py index 75d5607..8ebe69e 100644 --- a/tests/e2e/ssi_tests/run.py +++ b/tests/e2e/ssi_tests/run.py @@ -32,6 +32,7 @@ def run_all_tests(): caip10_ethereum_support_test() caip10_cosmos_support_test() vm_type_test() + method_specific_id_test() print("============= 😃️ All test cases completed successfully ============== \n") diff --git a/tests/e2e/ssi_tests/utils.py b/tests/e2e/ssi_tests/utils.py index 3997f97..33feb1e 100644 --- a/tests/e2e/ssi_tests/utils.py +++ b/tests/e2e/ssi_tests/utils.py @@ -1,5 +1,6 @@ import subprocess import json +import uuid def run_command(cmd_string): if type(cmd_string) != str: @@ -58,16 +59,19 @@ def generate_key_pair(algo="ed25519"): kp = json.loads(result_str) return kp -def generate_document_id(doc_type: str, kp: dict = None, algo: str = "ed25519"): +def generate_document_id(doc_type: str, kp: dict = None, algo: str = "ed25519", is_uuid: bool =False): id = "" if not kp: kp = generate_key_pair(algo) - if algo in ["recover-eth"]: - method_specific_id = kp["ethereum_address"] + if is_uuid: + method_specific_id = str(uuid.uuid4()) else: - method_specific_id = kp["pub_key_multibase"] - + if algo in ["recover-eth"]: + method_specific_id = kp["ethereum_address"] + else: + method_specific_id = kp["pub_key_multibase"] + if method_specific_id == None: raise Exception("Public key is empty") diff --git a/x/ssi/client/cli/tx_ssi.go b/x/ssi/client/cli/tx_ssi.go index 9c4ef15..6458a8e 100644 --- a/x/ssi/client/cli/tx_ssi.go +++ b/x/ssi/client/cli/tx_ssi.go @@ -1,43 +1,123 @@ package cli import ( + "encoding/base64" + "fmt" + "os" + "path/filepath" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/hypersign-protocol/hid-node/x/ssi/types" "github.com/spf13/cobra" ) +const didAliasFlag = "did-alias" + func CmdCreateDID() *cobra.Command { cmd := &cobra.Command{ - Use: "create-did [did-doc-string] [vm-id-1] [sign-key-1] [sign-key-algo-1] ... [vm-id-N] [sign-key-N] [sign-key-algo-N]", + Use: "create-did [did-doc-string] ([vm-id-1] [sign-key-1] [sign-key-algo-1] ... [vm-id-N] [sign-key-N] [sign-key-algo-N]) [flags]\n hid-noded tx ssi create-did --did-alias [flags]", Short: "Registers a DID Document", - Args: cobra.MinimumNArgs(4), + Args: cobra.ArbitraryArgs, RunE: func(cmd *cobra.Command, args []string) (err error) { - argDidDocString := args[0] - - clientCtx, err := client.GetClientTxContext(cmd) + didAlias, err := cmd.Flags().GetString(didAliasFlag) if err != nil { return err } - // Unmarshal DidDocString - var didDoc types.Did - err = clientCtx.Codec.UnmarshalJSON([]byte(argDidDocString), &didDoc) + clientCtx, err := client.GetClientTxContext(cmd) if err != nil { return err } - // Prepare Signatures - signInfos, err := getSignatures(cmd, didDoc.GetSignBytes(), args[1:]) - if err != nil { - return err + var didDoc types.Did + var signInfos []*types.SignInfo + txAuthorAddr := clientCtx.GetFromAddress() + txAuthorAddrString := clientCtx.GetFromAddress().String() + + if didAlias == "" { + // Minimum 4 CLI arguments are expected + if len(args) < 4 { + return fmt.Errorf("requires at least 4 arg(s), only received %v", len(args)) + } + + argDidDocString := args[0] + + // Unmarshal DidDocString + err = clientCtx.Codec.UnmarshalJSON([]byte(argDidDocString), &didDoc) + if err != nil { + return err + } + + // Prepare Signatures + signInfos, err = getSignatures(cmd, didDoc.GetSignBytes(), args[1:]) + if err != nil { + return err + } + } else { + // Get the DID Document from local + didAliasConfig, err := types.GetDidAliasConfig(cmd) + if err != nil { + return fmt.Errorf("failed to read DID Alias config: %v", err.Error()) + } + + aliasFile := didAlias + ".json" + didDocBytes, err := os.ReadFile(filepath.Join(didAliasConfig.DidAliasDir, aliasFile)) + if err != nil { + fmt.Fprintf(cmd.ErrOrStderr(), "DID Document alias '%v' does not exist\n", didAlias) + return nil + } + + err = clientCtx.Codec.UnmarshalJSON(didDocBytes, &didDoc) + if err != nil { + return err + } + + // Ensure the --from flag value matches with publicKey multibase + + // Since DID Alias will always have one verification method object, it is safe to + // choose the 0th index + publicKeyMultibase := didDoc.VerificationMethod[0].PublicKeyMultibase + + if err := validateDidAliasSignerAddress(txAuthorAddrString, publicKeyMultibase); err != nil { + return fmt.Errorf("%v: %v", err.Error(), didAlias) + } + + // Sign the DID Document using Keyring to get theSignInfo. Currently, "test" keyring-backend is only supported + keyringBackend, err := cmd.Flags().GetString(flags.FlagKeyringBackend) + if err != nil { + return err + } + if keyringBackend != "test" { + return fmt.Errorf("unsupporeted keyring backend for DID Document Alias Signing: %v", keyringBackend) + } + + kr, err := keyring.New("hid-node-app", keyringBackend, didAliasConfig.HidNodeConfigDir, nil) + if err != nil { + return err + } + + signatureBytes, _, err := kr.SignByAddress(txAuthorAddr, didDoc.GetSignBytes()) + if err != nil { + return err + } + signatureStr := base64.StdEncoding.EncodeToString(signatureBytes) + + signInfos = []*types.SignInfo{ + { + VerificationMethodId: didDoc.VerificationMethod[0].Id, + Signature: signatureStr, + }, + } } + // Submit CreateDID Tx msg := types.MsgCreateDID{ DidDocString: &didDoc, Signatures: signInfos, - Creator: clientCtx.GetFromAddress().String(), + Creator: txAuthorAddrString, } if err := msg.ValidateBasic(); err != nil { @@ -47,6 +127,7 @@ func CmdCreateDID() *cobra.Command { }, } + cmd.Flags().String(didAliasFlag, "", "alias of the generated DID Document which can be referred to while registering on-chain") flags.AddTxFlagsToCmd(cmd) return cmd } diff --git a/x/ssi/client/cli/tx_utils.go b/x/ssi/client/cli/tx_utils.go index 311f4d1..6c6c5c9 100644 --- a/x/ssi/client/cli/tx_utils.go +++ b/x/ssi/client/cli/tx_utils.go @@ -2,12 +2,16 @@ package cli import ( "crypto/ed25519" + "crypto/sha256" "encoding/base64" "encoding/hex" "fmt" + "github.com/cosmos/cosmos-sdk/types/bech32" "github.com/hypersign-protocol/hid-node/x/ssi/types" + "github.com/multiformats/go-multibase" secp256k1 "github.com/tendermint/tendermint/crypto/secp256k1" + "golang.org/x/crypto/ripemd160" // nolint: staticcheck etheraccounts "github.com/ethereum/go-ethereum/accounts" etherhexutil "github.com/ethereum/go-ethereum/common/hexutil" @@ -135,3 +139,36 @@ func getSignatures(cmd *cobra.Command, message []byte, cmdArgs []string) ([]*typ return signInfoList, nil } + +// validateDidAliasSignerAddress checks if the signer address provided in the --from flag matches +// the address extracted from the publicKeyMultibase +func validateDidAliasSignerAddress(fromSignerAddress, publicKeyMultibase string) error { + // Decode public key + _, publicKeyBytes, err := multibase.Decode(publicKeyMultibase) + if err != nil { + return err + } + + // Throw error if the length of secp256k1 publicKey is not 33 + if len(publicKeyBytes) != 33 { + return fmt.Errorf("invalid secp256k1 public key length %v", len(publicKeyBytes)) + } + + // Hash pubKeyBytes as: RIPEMD160(SHA256(public_key_bytes)) + pubKeySha256Hash := sha256.Sum256(publicKeyBytes) + ripemd160hash := ripemd160.New() + ripemd160hash.Write(pubKeySha256Hash[:]) + addressBytes := ripemd160hash.Sum(nil) + + // Convert addressBytes to bech32 encoded address + convertedAddress, err := bech32.ConvertAndEncode("hid", addressBytes) + if err != nil { + return err + } + + if fromSignerAddress != convertedAddress { + return fmt.Errorf("transaction signer address is not the author of DID Document alias") + } + + return nil +} diff --git a/x/ssi/keeper/msg_server_create_did.go b/x/ssi/keeper/msg_server_create_did.go index 486f93d..93be00f 100644 --- a/x/ssi/keeper/msg_server_create_did.go +++ b/x/ssi/keeper/msg_server_create_did.go @@ -25,7 +25,13 @@ func (k msgServer) CreateDID(goCtx context.Context, msg *types.MsgCreateDID) (*t } // Validate namespace in DID Document - if err := didDocNamespaceValidation(k, ctx, msgDidDocument); err != nil { + chainNamespace := k.GetChainNamespace(&ctx) + if err := types.DidChainNamespaceValidation(msgDidDocument, chainNamespace); err != nil { + return nil, sdkerrors.Wrap(types.ErrInvalidDidDoc, err.Error()) + } + + // Validate ownership of method specific id + if err := checkMethodSpecificIdOwnership(msgDidDocument.VerificationMethod, msgDidDocument.Id); err != nil { return nil, sdkerrors.Wrap(types.ErrInvalidDidDoc, err.Error()) } @@ -76,6 +82,37 @@ func (k msgServer) CreateDID(goCtx context.Context, msg *types.MsgCreateDID) (*t return &types.MsgCreateDIDResponse{Id: id}, nil } +// checkMethodSpecificIdOwnership validates the ownership of blockchain account id passed in the method specific +// identifier of DID Document. This ensures that a DID ID (containing a blockchain address) is being created by someone +// who owns the blockchain address. +func checkMethodSpecificIdOwnership(verificationMethods []*types.VerificationMethod, didId string) error { + inputMSI, inputMSIType, err := types.GetMethodSpecificIdAndType(didId) + if err != nil { + return err + } + + if inputMSIType == types.MSIBlockchainAccountId { + foundMSIinAnyVM := false + for _, vm := range verificationMethods { + if vm.BlockchainAccountId == inputMSI { + foundMSIinAnyVM = true + break + } + } + + if !foundMSIinAnyVM { + return fmt.Errorf( + "proof of ownership for method-specific-id in %v must be provided", + didId, + ) + } else { + return nil + } + } else { + return nil + } +} + // getControllersForCreateDID returns a list of controller DIDs // from controller and verification method attributes func getControllersForCreateDID(didDocument *types.Did) []string { diff --git a/x/ssi/keeper/msg_server_update_did.go b/x/ssi/keeper/msg_server_update_did.go index 17e8b93..73c185a 100644 --- a/x/ssi/keeper/msg_server_update_did.go +++ b/x/ssi/keeper/msg_server_update_did.go @@ -26,7 +26,8 @@ func (k msgServer) UpdateDID(goCtx context.Context, msg *types.MsgUpdateDID) (*t } // Validate namespace in DID Document - if err := didDocNamespaceValidation(k, ctx, msgDidDocument); err != nil { + chainNamespace := k.GetChainNamespace(&ctx) + if err := types.DidChainNamespaceValidation(msgDidDocument, chainNamespace); err != nil { return nil, sdkerrors.Wrap(types.ErrInvalidDidDoc, err.Error()) } @@ -42,6 +43,11 @@ func (k msgServer) UpdateDID(goCtx context.Context, msg *types.MsgUpdateDID) (*t } existingDidDocument := existingDidDocumentState.DidDocument + // Check if the DID Document is already deactivated + if existingDidDocumentState.DidDocumentMetadata.Deactivated { + return nil, sdkerrors.Wrapf(types.ErrDidDocDeactivated, "cannot update didDocument %v as it is deactivated", existingDidDocument.Id) + } + // Check if the incoming DID Document has any changes. If not, throw an error. if reflect.DeepEqual(existingDidDocument, msgDidDocument) { return nil, sdkerrors.Wrap(types.ErrInvalidDidDoc, "incoming DID Document does not have any changes") diff --git a/x/ssi/keeper/namespace.go b/x/ssi/keeper/namespace.go deleted file mode 100644 index ad6e8e2..0000000 --- a/x/ssi/keeper/namespace.go +++ /dev/null @@ -1,79 +0,0 @@ -package keeper - -import ( - "fmt" - "strings" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/hypersign-protocol/hid-node/x/ssi/types" -) - -// didNamespaceValidation validates the namespace in DID Document Id -func didNamespaceValidation(k msgServer, ctx sdk.Context, docId string) error { - genesisNamespace := k.GetChainNamespace(&ctx) - - docIdElements := strings.Split(docId, ":") - docNamespaceIndex := 2 - - if genesisNamespace == "" { - if len(docIdElements) != 3 { - return fmt.Errorf( - "expected number of did id elements for mainnet to be 3, got %s for id: %s", - fmt.Sprint(len(docIdElements)), - docId, - ) - } - } else { - docNamespace := docIdElements[docNamespaceIndex] - if genesisNamespace != docNamespace { - return fmt.Errorf( - "expected namespace for id %s to be %s, got %s", - docId, genesisNamespace, docNamespace) - } - } - - return nil -} - -// didDocNamespaceValidation validates the namespace in Did Document -func didDocNamespaceValidation(k msgServer, ctx sdk.Context, didDoc *types.Did) error { - // Subject ID check - if err := didNamespaceValidation(k, ctx, didDoc.Id); err != nil { - return err - } - - // Controllers check - for _, controller := range didDoc.Controller { - if err := didNamespaceValidation(k, ctx, controller); err != nil { - return err - } - } - - // Verification Method ID checks - for _, vm := range didDoc.VerificationMethod { - didId, _ := types.GetElementsFromDidUrl(vm.Id) - if err := didNamespaceValidation(k, ctx, didId); err != nil { - return err - } - } - - // Verification Relationships check - vmRelationshipList := [][]string{ - didDoc.Authentication, - didDoc.AssertionMethod, - didDoc.KeyAgreement, - didDoc.CapabilityDelegation, - didDoc.CapabilityInvocation, - } - - for _, vmRelationship := range vmRelationshipList { - // didUrl check and presence in verification methods - for _, id := range vmRelationship { - if err := didNamespaceValidation(k, ctx, id); err != nil { - return err - } - } - } - - return nil -} diff --git a/x/ssi/types/chain_namespace_validation.go b/x/ssi/types/chain_namespace_validation.go new file mode 100644 index 0000000..bac87ea --- /dev/null +++ b/x/ssi/types/chain_namespace_validation.go @@ -0,0 +1,62 @@ +package types + +import ( + "fmt" +) + +func chainNamespaceValidation(docId string, validChainNamespace string) error { + documentChainNamespace := getDocumentChainNamespace(docId) + if documentChainNamespace != validChainNamespace { + return fmt.Errorf( + "expected namespace for id %v to be %v, got %v", + docId, + validChainNamespace, + documentChainNamespace, + ) + } else { + return nil + } +} + +// DidDocNamespaceValidation validates the namespace in Did Document +func DidChainNamespaceValidation(didDoc *Did, validChainNamespace string) error { + // Subject ID check + if err := chainNamespaceValidation(didDoc.Id, validChainNamespace); err != nil { + return err + } + + // Controllers check + for _, controller := range didDoc.Controller { + if err := chainNamespaceValidation(controller, validChainNamespace); err != nil { + return err + } + } + + // Verification Method ID checks + for _, vm := range didDoc.VerificationMethod { + didId, _ := GetElementsFromDidUrl(vm.Id) + if err := chainNamespaceValidation(didId, validChainNamespace); err != nil { + return err + } + } + + // Verification Relationships check + vmRelationshipList := [][]string{ + didDoc.Authentication, + didDoc.AssertionMethod, + didDoc.KeyAgreement, + didDoc.CapabilityDelegation, + didDoc.CapabilityInvocation, + } + + for _, vmRelationship := range vmRelationshipList { + // didUrl check and presence in verification methods + for _, id := range vmRelationship { + if err := chainNamespaceValidation(id, validChainNamespace); err != nil { + return err + } + } + } + + return nil +} \ No newline at end of file diff --git a/x/ssi/types/common.go b/x/ssi/types/common.go index c267377..440a703 100644 --- a/x/ssi/types/common.go +++ b/x/ssi/types/common.go @@ -1,5 +1,9 @@ package types +// Supported method-specific-id Formats +const MSIBlockchainAccountId = "MSIBlockchainAccountId" +const MSINonBlockchainAccountId = "MSINonBlockchainAccountId" + // Supported Verification Method Types const Ed25519VerificationKey2020 = "Ed25519VerificationKey2020" const EcdsaSecp256k1VerificationKey2019 = "EcdsaSecp256k1VerificationKey2019" diff --git a/x/ssi/types/did_alias.go b/x/ssi/types/did_alias.go new file mode 100644 index 0000000..b733496 --- /dev/null +++ b/x/ssi/types/did_alias.go @@ -0,0 +1,27 @@ +package types + +import ( + "path/filepath" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/spf13/cobra" +) + +type DidAliasConfig struct { + HidNodeConfigDir string + DidAliasDir string +} + +func GetDidAliasConfig(cmd *cobra.Command) (*DidAliasConfig, error) { + configDir, err := cmd.Flags().GetString(flags.FlagHome) + if err != nil { + return nil, err + } + + didAliasDir := filepath.Join(configDir, "generated-ssi-docs") + + return &DidAliasConfig{ + HidNodeConfigDir: configDir, + DidAliasDir: didAliasDir, + }, nil +} diff --git a/x/ssi/types/diddoc_validation.go b/x/ssi/types/diddoc_validation.go index d228459..039cb16 100644 --- a/x/ssi/types/diddoc_validation.go +++ b/x/ssi/types/diddoc_validation.go @@ -3,51 +3,65 @@ package types import ( "fmt" "regexp" - "strings" ) // isValidDidDoc checks if the DID Id is valid func isValidDidDocId(id string) error { - // check the number of elements in DID Document - idElements := strings.Split(id, ":") - if !(len(idElements) == 3 || len(idElements) == 4) { - return fmt.Errorf( - "number of elements in DID Id %s should be either 3 or 4", - id, - ) + inputDocumentIdentifier, err := getDocumentIdentifier(id) + if err != nil { + return err + } + + inputDidMethod, err := getDocumentMethod(id) + if err != nil { + return err } - // check if the first element is valid document identifier - if idElements[0] != DocumentIdentifierDid { + inputMSI, inputMSIType, err := GetMethodSpecificIdAndType(id) + if err != nil { + return err + } + + // Validate Document Identifier + if inputDocumentIdentifier != DocumentIdentifierDid { return fmt.Errorf( "document identifier should be %s", DocumentIdentifierDid, ) } - // check if the second element is the correct DID method - if idElements[1] != DidMethod { + // Validate DID Method + if inputDidMethod != DidMethod { return fmt.Errorf( "DID method should be %s", DidMethod, ) } - // check proper method specific id - // TODO: need to define a specification for method-specific-id - methodSpecificId := idElements[len(idElements)-1] - isProperMethodSpecificId, err := regexp.MatchString( - "^[a-zA-Z0-9]{32,}$", - methodSpecificId, - ) - if err != nil { - return fmt.Errorf("error in parsing regular expression for method-specific-id: %s", err.Error()) - } - if !isProperMethodSpecificId { - return fmt.Errorf( - "method-specific-id should be an alphanumeric string with minimum of 32 characters, received: %s", - methodSpecificId, + // Validate Method Specific ID + switch inputMSIType { + case MSIBlockchainAccountId: + if err := validateBlockchainAccountId(inputMSI); err != nil { + return err + } + case MSINonBlockchainAccountId: + // Non Blockchain Account ID should be a string that supports alphanumeric characters, + // and dot (.) and hypen (-). The first character MUST NOT be dot (.) or hyphen (-). + isValidMSI, err := regexp.MatchString( + "^[a-zA-Z0-9][a-zA-Z0-9.-]*$", + inputMSI, ) + if err != nil { + return err + } + if !isValidMSI { + return fmt.Errorf( + "method-specific-id of non BlockchainAccountId type %v should only contain alphanumeric, dot (.) and hyphen (-)", + inputMSI, + ) + } + default: + return fmt.Errorf("invalid method specific id type: %v", inputMSIType) } return nil @@ -143,19 +157,6 @@ func verificationKeyCheck(vm *VerificationMethod) error { return nil } -// checkDuplicateItems return a duplicate Id from the list, if found -func checkDuplicateItems(list []string) string { - presentMap := map[string]bool{} - for idx := range list { - if _, present := presentMap[list[idx]]; !present { - presentMap[list[idx]] = true - } else { - return list[idx] - } - } - return "" -} - // validateServices validates the Service attribute of DID Document func validateServices(services []*Service) error { for _, service := range services { @@ -224,12 +225,12 @@ func validateVerificationMethods(vms []*VerificationMethod) error { vmIdList := []string{} publicKeyMultibaseList := []string{} blockchainAccountIdList := []string{} - + var pubKeyMultibaseBlockchainAccIdMap map[string]bool = map[string]bool{} for _, vm := range vms { vmIdList = append(vmIdList, vm.Id) - + if vm.Type == EcdsaSecp256k1VerificationKey2019 { if _, present := pubKeyMultibaseBlockchainAccIdMap[vm.PublicKeyMultibase]; present { // TODO: Following is a temporary measure, where we will be allowing duplicate publicKeyMultibase values @@ -239,7 +240,7 @@ func validateVerificationMethods(vms []*VerificationMethod) error { // generated signature is figured out. if vm.BlockchainAccountId == "" { return fmt.Errorf( - "duplicate publicKeyMultibase of type EcdsaSecp256k1VerificationKey2019 without blockchainAccountId is not allowed: %s ", + "duplicate publicKeyMultibase of type EcdsaSecp256k1VerificationKey2019 without blockchainAccountId is not allowed: %s ", vm.PublicKeyMultibase, ) } @@ -249,7 +250,7 @@ func validateVerificationMethods(vms []*VerificationMethod) error { } else { publicKeyMultibaseList = append(publicKeyMultibaseList, vm.PublicKeyMultibase) } - + blockchainAccountIdList = append(blockchainAccountIdList, vm.BlockchainAccountId) } diff --git a/x/ssi/types/utils.go b/x/ssi/types/utils.go index 4bddd18..224e3e1 100644 --- a/x/ssi/types/utils.go +++ b/x/ssi/types/utils.go @@ -1,6 +1,7 @@ package types import ( + "fmt" "strings" ) @@ -46,3 +47,84 @@ func FindInSlice(list []string, element string) bool { } return false } + +// checkDuplicateItems return a duplicate Id from the list, if found +func checkDuplicateItems(list []string) string { + presentMap := map[string]bool{} + for idx := range list { + if _, present := presentMap[list[idx]]; !present { + presentMap[list[idx]] = true + } else { + return list[idx] + } + } + return "" +} + +func getDocumentChainNamespace(docId string) string { + docIdElements := strings.Split(docId, ":") + + // Non Blockchain Account ID MSI + if len(docIdElements) == 4 || len(docIdElements) == 6 { + return docIdElements[2] + } else { + return "" + } +} + +func getDocumentIdentifier(docId string) (string, error) { + docIdElements := strings.Split(docId, ":") + + if len(docIdElements) < 1 { + return "", fmt.Errorf("invalid document Id: %v", docId) + } + + return docIdElements[0], nil +} + +func getDocumentMethod(docId string) (string, error) { + docIdElements := strings.Split(docId, ":") + + if len(docIdElements) < 2 { + return "", fmt.Errorf("invalid document Id: %v", docId) + } + + return docIdElements[1], nil +} + +// GetMethodSpecificIdAndType extracts the method-specific-id from Document Id and which of following +// types it belongs to: +// +// 1. CAIP-10 Blockchain Account ID +// +// 2. String consisting of Alphanumeric, dot (.) and hyphen (-) characters only +func GetMethodSpecificIdAndType(didId string) (string, string, error) { + docIdElements := strings.Split(didId, ":") + var methodSpecificId string + var methodSpecificIdCondition string + + if isMSIBlockchainAccountId(didId) { + methodSpecificId = strings.Join(docIdElements[(len(docIdElements)-3):], ":") + methodSpecificIdCondition = MSIBlockchainAccountId + } else if isMSINonBlockchainAccountId(didId) { + methodSpecificId = docIdElements[len(docIdElements)-1] + methodSpecificIdCondition = MSINonBlockchainAccountId + } else { + return "", "", fmt.Errorf( + "unable to retrieve method-specific-id from DID Id: %v. It should either be a CAIP-10 Blockchain Account ID or a string consisting of alphanumeric, dot(.) and hyphen(-) characters only", didId) + } + + return methodSpecificId, methodSpecificIdCondition, nil +} + +// isMSINonBlockchainAccountId asserts if the Method Specific Id is a CAIP-10 Blockchain Account Id +func isMSINonBlockchainAccountId(didId string) bool { + didIdElements := strings.Split(didId, ":") + return (len(didIdElements) == 3 || len(didIdElements) == 4) +} + +// isMSIBlockchainAccountId asserts if the Method Specific Id is a string containing alphanumeric, dot (.) and hyphen (-) characters +func isMSIBlockchainAccountId(didId string) bool { + didIdElements := strings.Split(didId, ":") + return (len(didIdElements) == 5 || len(didIdElements) == 6) +} diff --git a/x/ssi/verification/crypto.go b/x/ssi/verification/crypto.go index 265765c..5fd10a6 100644 --- a/x/ssi/verification/crypto.go +++ b/x/ssi/verification/crypto.go @@ -179,7 +179,7 @@ func verifyCosmosBlockchainAccountId(blockchainAccountId, publicKeyMultibase str if err != nil { return err } - validAddressPrefix := types.CosmosCAIP10ChainIdBech32PrefixMap[chainId] + validAddressPrefix := types.CosmosCAIP10ChainIdBech32PrefixMap[chainId] convertedAddress, err := publicKeyToCosmosBech32Address(validAddressPrefix, publicKeyBytes) if err != nil { return err