From bf00e4e843c0d6dbe3cb8708f4ce20357725425d Mon Sep 17 00:00:00 2001 From: arnabghose997 Date: Fri, 24 Mar 2023 17:29:43 +0530 Subject: [PATCH 1/6] feat: added cli commands to generate and sign did documents through aliases --- cmd/hid-noded/cmd/generate_ssi.go | 261 ++++++++++++++++++++++++ cmd/hid-noded/cmd/generate_ssi_utils.go | 31 +++ cmd/hid-noded/cmd/root.go | 1 + x/ssi/client/cli/tx_ssi.go | 107 ++++++++-- x/ssi/client/cli/tx_utils.go | 37 ++++ x/ssi/types/did_alias.go | 27 +++ 6 files changed, 451 insertions(+), 13 deletions(-) create mode 100644 cmd/hid-noded/cmd/generate_ssi.go create mode 100644 cmd/hid-noded/cmd/generate_ssi_utils.go create mode 100644 x/ssi/types/did_alias.go 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/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/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 +} From 7a7c4208018c6134db4e191d13038a78ba1c03d6 Mon Sep 17 00:00:00 2001 From: arnabghose997 Date: Fri, 24 Mar 2023 17:34:59 +0530 Subject: [PATCH 2/6] refactor: changed DID namespace to devnet --- localnetsetup.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) mode change 100644 => 100755 localnetsetup.sh 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." From cbaedc09c5f128cd86d16cebf24714f7942684ca Mon Sep 17 00:00:00 2001 From: arnabghose997 Date: Wed, 19 Apr 2023 13:14:43 +0530 Subject: [PATCH 3/6] refactor: defined rules for method-specific-id in DID Document Ids --- tests/e2e/ssi_tests/e2e_tests.py | 132 +++++++++++++++++++++++++- tests/e2e/ssi_tests/generate_doc.py | 19 ++-- tests/e2e/ssi_tests/run.py | 1 + tests/e2e/ssi_tests/utils.py | 14 ++- x/ssi/keeper/msg_server_create_did.go | 36 +++++++ x/ssi/types/common.go | 4 + x/ssi/types/diddoc_validation.go | 87 ++++++++--------- x/ssi/types/utils.go | 71 ++++++++++++++ x/ssi/verification/crypto.go | 2 +- 9 files changed, 309 insertions(+), 57 deletions(-) diff --git a/tests/e2e/ssi_tests/e2e_tests.py b/tests/e2e/ssi_tests/e2e_tests.py index e065f61..2c23545 100644 --- a/tests/e2e/ssi_tests/e2e_tests.py +++ b/tests/e2e/ssi_tests/e2e_tests.py @@ -812,6 +812,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 +999,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 +1093,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/keeper/msg_server_create_did.go b/x/ssi/keeper/msg_server_create_did.go index 486f93d..c17c994 100644 --- a/x/ssi/keeper/msg_server_create_did.go +++ b/x/ssi/keeper/msg_server_create_did.go @@ -29,6 +29,11 @@ func (k msgServer) CreateDID(goCtx context.Context, msg *types.MsgCreateDID) (*t 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()) + } + // Checks if the Did Document is already registered if k.HasDid(ctx, msgDidDocument.Id) { return nil, sdkerrors.Wrap(types.ErrDidDocExists, msgDidDocument.Id) @@ -76,6 +81,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/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/diddoc_validation.go b/x/ssi/types/diddoc_validation.go index d228459..ce55855 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 := getDidDocumentIdentifier(id) + if err != nil { + return err + } + + inputDidMethod, err := getDidDocumentMethod(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..721197c 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,73 @@ 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 getDidDocumentIdentifier(didId string) (string, error) { + didIdElements := strings.Split(didId, ":") + + if len(didIdElements) < 1 { + return "", fmt.Errorf("invalid DID Id: %v", didId) + } + + return didIdElements[0], nil +} + +func getDidDocumentMethod(didId string) (string, error) { + didIdElements := strings.Split(didId, ":") + + if len(didIdElements) < 2 { + return "", fmt.Errorf("invalid DID Id: %v", didId) + } + + return didIdElements[1], nil +} + +// GetMethodSpecificIdAndType extracts the method-specific-id from DID 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) { + didIdElements := strings.Split(didId, ":") + var methodSpecificId string + var methodSpecificIdCondition string + + if getMSIBlockchainAccountIdCondition(didId) { + methodSpecificId = strings.Join(didIdElements[(len(didIdElements)-3):], ":") + methodSpecificIdCondition = MSIBlockchainAccountId + } else if getMSINonBlockchainAccountIdCondition(didId) { + methodSpecificId = didIdElements[len(didIdElements)-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 +} + +// getMSINonBlockchainAccountIdCondition asserts if the Method Specific Id is a CAIP-10 Blockchain Account Id +func getMSINonBlockchainAccountIdCondition(didId string) bool { + didIdElements := strings.Split(didId, ":") + return (len(didIdElements) == 3 || len(didIdElements) == 4) +} + +// getMSIMultibaseCondition asserts if the Method Specific Id is a Multibase encoded string +func getMSIBlockchainAccountIdCondition(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 From 2a41872ab0d264f4132ead78f01d80ba79118513 Mon Sep 17 00:00:00 2001 From: arnabghose997 Date: Wed, 19 Apr 2023 15:23:36 +0530 Subject: [PATCH 4/6] fix: changed namespace validation check --- x/ssi/keeper/msg_server_create_did.go | 3 +- x/ssi/keeper/msg_server_update_did.go | 3 +- x/ssi/keeper/namespace.go | 79 ----------------------- x/ssi/types/chain_namespace_validation.go | 62 ++++++++++++++++++ x/ssi/types/diddoc_validation.go | 4 +- x/ssi/types/utils.go | 39 +++++++---- 6 files changed, 93 insertions(+), 97 deletions(-) delete mode 100644 x/ssi/keeper/namespace.go create mode 100644 x/ssi/types/chain_namespace_validation.go diff --git a/x/ssi/keeper/msg_server_create_did.go b/x/ssi/keeper/msg_server_create_did.go index c17c994..93be00f 100644 --- a/x/ssi/keeper/msg_server_create_did.go +++ b/x/ssi/keeper/msg_server_create_did.go @@ -25,7 +25,8 @@ 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()) } diff --git a/x/ssi/keeper/msg_server_update_did.go b/x/ssi/keeper/msg_server_update_did.go index 17e8b93..28b2b40 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()) } 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/diddoc_validation.go b/x/ssi/types/diddoc_validation.go index ce55855..039cb16 100644 --- a/x/ssi/types/diddoc_validation.go +++ b/x/ssi/types/diddoc_validation.go @@ -7,12 +7,12 @@ import ( // isValidDidDoc checks if the DID Id is valid func isValidDidDocId(id string) error { - inputDocumentIdentifier, err := getDidDocumentIdentifier(id) + inputDocumentIdentifier, err := getDocumentIdentifier(id) if err != nil { return err } - inputDidMethod, err := getDidDocumentMethod(id) + inputDidMethod, err := getDocumentMethod(id) if err != nil { return err } diff --git a/x/ssi/types/utils.go b/x/ssi/types/utils.go index 721197c..6ae8bce 100644 --- a/x/ssi/types/utils.go +++ b/x/ssi/types/utils.go @@ -61,42 +61,53 @@ func checkDuplicateItems(list []string) string { return "" } -func getDidDocumentIdentifier(didId string) (string, error) { - didIdElements := strings.Split(didId, ":") +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(didIdElements) < 1 { - return "", fmt.Errorf("invalid DID Id: %v", didId) + if len(docIdElements) < 1 { + return "", fmt.Errorf("invalid document Id: %v", docId) } - return didIdElements[0], nil + return docIdElements[0], nil } -func getDidDocumentMethod(didId string) (string, error) { - didIdElements := strings.Split(didId, ":") +func getDocumentMethod(docId string) (string, error) { + docIdElements := strings.Split(docId, ":") - if len(didIdElements) < 2 { - return "", fmt.Errorf("invalid DID Id: %v", didId) + if len(docIdElements) < 2 { + return "", fmt.Errorf("invalid document Id: %v", docId) } - return didIdElements[1], nil + return docIdElements[1], nil } -// GetMethodSpecificIdAndType extracts the method-specific-id from DID Id and which of following +// 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) { - didIdElements := strings.Split(didId, ":") + docIdElements := strings.Split(didId, ":") var methodSpecificId string var methodSpecificIdCondition string if getMSIBlockchainAccountIdCondition(didId) { - methodSpecificId = strings.Join(didIdElements[(len(didIdElements)-3):], ":") + methodSpecificId = strings.Join(docIdElements[(len(docIdElements)-3):], ":") methodSpecificIdCondition = MSIBlockchainAccountId } else if getMSINonBlockchainAccountIdCondition(didId) { - methodSpecificId = didIdElements[len(didIdElements)-1] + methodSpecificId = docIdElements[len(docIdElements)-1] methodSpecificIdCondition = MSINonBlockchainAccountId } else { return "", "", fmt.Errorf( From 2d8b27d4d8eae957fc675ee908d1dccb9495274d Mon Sep 17 00:00:00 2001 From: arnabghose997 Date: Thu, 20 Apr 2023 11:19:04 +0530 Subject: [PATCH 5/6] refactor: changed the names of getMSIBlockchainAccountIdCondition and getMSINonBlockchainAccountIdCondition to isMSIBlockchainAccountId and isMSINonBlockchainAccountId respectively since they return boolean conditions; fixed comment of isMSIBlockchainAccountId --- x/ssi/types/utils.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/x/ssi/types/utils.go b/x/ssi/types/utils.go index 6ae8bce..224e3e1 100644 --- a/x/ssi/types/utils.go +++ b/x/ssi/types/utils.go @@ -103,10 +103,10 @@ func GetMethodSpecificIdAndType(didId string) (string, string, error) { var methodSpecificId string var methodSpecificIdCondition string - if getMSIBlockchainAccountIdCondition(didId) { + if isMSIBlockchainAccountId(didId) { methodSpecificId = strings.Join(docIdElements[(len(docIdElements)-3):], ":") methodSpecificIdCondition = MSIBlockchainAccountId - } else if getMSINonBlockchainAccountIdCondition(didId) { + } else if isMSINonBlockchainAccountId(didId) { methodSpecificId = docIdElements[len(docIdElements)-1] methodSpecificIdCondition = MSINonBlockchainAccountId } else { @@ -117,14 +117,14 @@ func GetMethodSpecificIdAndType(didId string) (string, string, error) { return methodSpecificId, methodSpecificIdCondition, nil } -// getMSINonBlockchainAccountIdCondition asserts if the Method Specific Id is a CAIP-10 Blockchain Account Id -func getMSINonBlockchainAccountIdCondition(didId string) bool { +// 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) } -// getMSIMultibaseCondition asserts if the Method Specific Id is a Multibase encoded string -func getMSIBlockchainAccountIdCondition(didId string) bool { +// 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) } From ee265d0386f75908642b85efa558c4224d042465 Mon Sep 17 00:00:00 2001 From: arnabghose997 Date: Fri, 21 Apr 2023 09:21:47 +0530 Subject: [PATCH 6/6] bugfix: added check to ensure deactivated DIDs are not allowed to update --- tests/e2e/ssi_tests/e2e_tests.py | 32 ++++++++++++++++++++++++++- x/ssi/keeper/msg_server_update_did.go | 5 +++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/e2e/ssi_tests/e2e_tests.py b/tests/e2e/ssi_tests/e2e_tests.py index e065f61..ca3422a 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(): diff --git a/x/ssi/keeper/msg_server_update_did.go b/x/ssi/keeper/msg_server_update_did.go index 17e8b93..db62ce5 100644 --- a/x/ssi/keeper/msg_server_update_did.go +++ b/x/ssi/keeper/msg_server_update_did.go @@ -42,6 +42,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")