From bf00e4e843c0d6dbe3cb8708f4ce20357725425d Mon Sep 17 00:00:00 2001 From: arnabghose997 Date: Fri, 24 Mar 2023 17:29:43 +0530 Subject: [PATCH 1/2] 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/2] 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."