Repository: onflow/flow-cli
Pull request #449 (2022-01-25 22:02:20)

Khalil/1659 secure grpc support
Closes https://github.com/dapperlabs/flow-internal/issues/1659

## Description

This PR adds support for secure GRPC client connections. 

______

For contributor use:

- [ ] Targeted PR against `master` branch
- [ ] Linked to Github issue with discussion and accepted design OR link to spec that describes this work
- [ ] Code follows the [standards mentioned here](https://github.com/onflow/flow-cli/blob/master/CONTRIBUTING.md#styleguides)
- [ ] Updated relevant documentation
- [ ] Re-reviewed `Files changed` in the Github PR explorer
- [ ] Added appropriate labels



Patch:
diff --git a/docs/account-add-contract.md b/docs/account-add-contract.md
index 88c6ae0c..ae26c227 100644
--- a/docs/account-add-contract.md
+++ b/docs/account-add-contract.md
@@ -97,6 +97,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/account-remove-contract.md b/docs/account-remove-contract.md
index 796af544..422b38d7 100644
--- a/docs/account-remove-contract.md
+++ b/docs/account-remove-contract.md
@@ -89,6 +89,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/account-staking-info.md b/docs/account-staking-info.md
index 545cf187..9174521e 100644
--- a/docs/account-staking-info.md
+++ b/docs/account-staking-info.md
@@ -71,6 +71,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/account-update-contract.md b/docs/account-update-contract.md
index afe91a58..6b17a240 100644
--- a/docs/account-update-contract.md
+++ b/docs/account-update-contract.md
@@ -94,6 +94,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/build-transactions.md b/docs/build-transactions.md
index 8f09b671..cda0dee6 100644
--- a/docs/build-transactions.md
+++ b/docs/build-transactions.md
@@ -156,6 +156,14 @@ Specify the gas limit for this transaction.
 Specify the hostname of the Access API that will be
 used to execute the commands.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/configuration.md b/docs/configuration.md
index 69d46399..3eddf970 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -94,7 +94,11 @@ We'll walk through each property one by one.
   "networks": {
     "emulator": "127.0.0.1:3569",
     "mainnet": "access.mainnet.nodes.onflow.org:9000",
-    "testnet": "access.devnet.nodes.onflow.org:9000"
+    "testnet": "access.devnet.nodes.onflow.org:9000",
+    "testnetSecure": {
+      "Host": "access-001.devnet30.nodes.onflow.org:9001",
+      "NetworkKey": "ba69f7d2e82b9edf25b103c195cd371cf0cc047ef8884a9bbe331e62982d46daeebf836f7445a2ac16741013b192959d8ad26998aff12f2adc67a99e1eb2988d"
+    }
   },
 
   "emulators": {
@@ -291,13 +295,28 @@ Format for networks is:
 ...
 ```
 
+```json
+...
+"networks": {
+  "NETWORK NAME": {
+    "host": "ADDRESS",
+    "key": "ACCESS NODE NETWORK KEY"    
+  }
+}
+...
+```
+
 ```json
 ...
 
 "networks": {
     "emulator": "127.0.0.1:3569",
     "mainnet": "access.mainnet.nodes.onflow.org:9000",
-    "testnet": "access.devnet.nodes.onflow.org:9000"
+    "testnet": "access.devnet.nodes.onflow.org:9000",
+    "testnetSecure": {
+        "host": "access-001.devnet30.nodes.onflow.org:9001",
+        "key": "ba69f7d2e82b9edf25b103c195cd371cf0cc047ef8884a9bbe331e62982d46daeebf836f7445a2ac16741013b192959d8ad26998aff12f2adc67a99e1eb2988d"
+    },
 }
 
 ...
diff --git a/docs/create-accounts.md b/docs/create-accounts.md
index c76b30e0..24d71705 100644
--- a/docs/create-accounts.md
+++ b/docs/create-accounts.md
@@ -120,6 +120,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/deploy-project-contracts.md b/docs/deploy-project-contracts.md
index f8ee81da..9d2211ef 100644
--- a/docs/deploy-project-contracts.md
+++ b/docs/deploy-project-contracts.md
@@ -142,6 +142,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/execute-scripts.md b/docs/execute-scripts.md
index b0b20c7b..63815eee 100644
--- a/docs/execute-scripts.md
+++ b/docs/execute-scripts.md
@@ -83,6 +83,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/get-accounts.md b/docs/get-accounts.md
index 9d99d725..0795952c 100644
--- a/docs/get-accounts.md
+++ b/docs/get-accounts.md
@@ -75,6 +75,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/get-blocks.md b/docs/get-blocks.md
index 0aaea949..e78084fa 100644
--- a/docs/get-blocks.md
+++ b/docs/get-blocks.md
@@ -99,6 +99,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/get-collections.md b/docs/get-collections.md
index 4ef0f809..2090a4e0 100644
--- a/docs/get-collections.md
+++ b/docs/get-collections.md
@@ -50,6 +50,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/get-events.md b/docs/get-events.md
index 34dc46b4..fb2f0877 100644
--- a/docs/get-events.md
+++ b/docs/get-events.md
@@ -147,6 +147,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/get-status.md b/docs/get-status.md
index 48753141..4332165f 100644
--- a/docs/get-status.md
+++ b/docs/get-status.md
@@ -38,6 +38,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Filter
 
 - Flag: `--filter`
diff --git a/docs/get-transactions.md b/docs/get-transactions.md
index 8f4373f4..a93dda75 100644
--- a/docs/get-transactions.md
+++ b/docs/get-transactions.md
@@ -109,6 +109,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/send-signed-transactions.md b/docs/send-signed-transactions.md
index 7747c5da..62981a10 100644
--- a/docs/send-signed-transactions.md
+++ b/docs/send-signed-transactions.md
@@ -88,6 +88,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/send-transactions.md b/docs/send-transactions.md
index 269c18ed..e74d16cd 100644
--- a/docs/send-transactions.md
+++ b/docs/send-transactions.md
@@ -151,6 +151,14 @@ Specify the hostname of the Access API that will be
 used to execute the command. This flag overrides
 any host defined by the `--network` flag.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/sign-transaction.md b/docs/sign-transaction.md
index fd793f93..bc308d07 100644
--- a/docs/sign-transaction.md
+++ b/docs/sign-transaction.md
@@ -95,6 +95,14 @@ Specify the name of the account that will be used to sign the transaction.
 Specify the hostname of the Access API that will be
 used to execute the commands.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/snapshot-save.md b/docs/snapshot-save.md
index af4292bc..37250388 100644
--- a/docs/snapshot-save.md
+++ b/docs/snapshot-save.md
@@ -40,6 +40,14 @@ Output path where the protocol snapshot JSON file will be saved.
 Specify the hostname of the Access API that will be
 used to execute the commands.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/docs/template.md b/docs/template.md
index 525a1c8a..00970033 100644
--- a/docs/template.md
+++ b/docs/template.md
@@ -59,6 +59,14 @@ Specify the name of the account that will be used to sign the transaction.
 Specify the hostname of the Access API that will be
 used to execute the commands.
 
+### Network Key
+
+- Flag: `--network-key`
+- Valid inputs: A valid network public key of the host in hex string format
+
+Specify the network public key of the Access API that will be
+used to create a secure GRPC client when executing the command.
+
 ### Network
 
 - Flag: `--network`
diff --git a/internal/command/command.go b/internal/command/command.go
index 20eb4e00..da4ddf5a 100644
--- a/internal/command/command.go
+++ b/internal/command/command.go
@@ -33,9 +33,11 @@ import (
 	"github.com/onflow/flow-cli/pkg/flowkit/gateway"
 	"github.com/onflow/flow-cli/pkg/flowkit/output"
 	"github.com/onflow/flow-cli/pkg/flowkit/services"
+	"github.com/onflow/flow-cli/pkg/flowkit/util"
 
 	"github.com/getsentry/sentry-go"
 	"github.com/spf13/afero"
+
 	"github.com/spf13/cobra"
 )
 
@@ -97,10 +99,10 @@ func (c Command) AddToParent(parent *cobra.Command) {
 			handleError("Config Error", confErr)
 		}
 
-		host, err := resolveHost(state, Flags.Host, Flags.Network)
+		host, hostNetworkKey, err := resolveHost(state, Flags.Host, Flags.HostNetworkKey, Flags.Network)
 		handleError("Host Error", err)
 
-		clientGateway, err := createGateway(host)
+		clientGateway, err := createGateway(host, hostNetworkKey)
 		handleError("Gateway Error", err)
 
 		logger := createLogger(Flags.Log, Flags.Format)
@@ -140,8 +142,12 @@ func (c Command) AddToParent(parent *cobra.Command) {
 }
 
 // createGateway creates a gateway to be used, defaults to grpc but can support others.
-func createGateway(host string) (gateway.Gateway, error) {
-	// create default grpc client
+func createGateway(host, hostNetworkKey string) (gateway.Gateway, error) {
+	// create secure grpc client if hostNetworkKey provided
+	if hostNetworkKey != "" {
+		return gateway.NewSecureGrpcGateway(host, hostNetworkKey)
+	}
+
 	return gateway.NewGrpcGateway(host)
 }
 
@@ -152,34 +158,45 @@ func createGateway(host string) (gateway.Gateway, error) {
 // 2. if conf is initialized return host by network flag
 // 3. if conf is not initialized and network flag is provided resolve to coded value for that network
 // 4. default to emulator network
-func resolveHost(state *flowkit.State, hostFlag string, networkFlag string) (string, error) {
+func resolveHost(state *flowkit.State, hostFlag, networkKeyFlag, networkFlag string) (string, string, error) {
 	// don't allow both network and host flag as the host might be different
 	if networkFlag != config.DefaultEmulatorNetwork().Name && hostFlag != "" {
-		return "", fmt.Errorf("shouldn't use both host and network flags, better to use network flag")
+		return "", "", fmt.Errorf("shouldn't use both host and network flags, better to use network flag")
 	}
 
 	// host flag has highest priority
 	if hostFlag != "" {
-		return hostFlag, nil
+		// if network-key was provided validate it
+		if networkKeyFlag != "" {
+			err := util.ValidateECDSAP256Pub(networkKeyFlag)
+			if err != nil {
+				return "", "", fmt.Errorf("invalid network key %s: %w", networkKeyFlag, err)
+			}
+
+			return hostFlag, networkKeyFlag, nil
+		}
+
+		return hostFlag, networkKeyFlag, nil
 	}
+
 	// network flag with project initialized is next
 	if state != nil {
 		stateNetwork, err := state.Networks().ByName(networkFlag)
 		if err != nil {
-			return "", fmt.Errorf("network with name %s does not exist in configuration", networkFlag)
+			return "", "", fmt.Errorf("network with name %s does not exist in configuration", networkFlag)
 		}
 
-		return stateNetwork.Host, nil
+		return stateNetwork.Host, stateNetwork.Key, nil
 	}
 
 	networks := config.DefaultNetworks()
 	network, err := networks.ByName(networkFlag)
 
 	if err != nil {
-		return "", fmt.Errorf("invalid network with name %s", networkFlag)
+		return "", "", fmt.Errorf("invalid network with name %s", networkFlag)
 	}
 
-	return network.Host, nil
+	return network.Host, network.Key, nil
 }
 
 // create logger utility.
diff --git a/internal/command/flags.go b/internal/command/flags.go
index 18af3666..26d6e406 100644
--- a/internal/command/flags.go
+++ b/internal/command/flags.go
@@ -30,26 +30,28 @@ import (
 
 // GlobalFlags contains all global flags definitions.
 type GlobalFlags struct {
-	Filter      string
-	Format      string
-	Save        string
-	Host        string
-	Log         string
-	Network     string
-	Yes         bool
-	ConfigPaths []string
+	Filter         string
+	Format         string
+	Save           string
+	Host           string
+	HostNetworkKey string
+	Log            string
+	Network        string
+	Yes            bool
+	ConfigPaths    []string
 }
 
 // Flags initialized to default values.
 var Flags = GlobalFlags{
-	Filter:      "",
-	Format:      formatText,
-	Save:        "",
-	Host:        "",
-	Network:     config.DefaultEmulatorNetwork().Name,
-	Log:         logLevelInfo,
-	Yes:         false,
-	ConfigPaths: config.DefaultPaths(),
+	Filter:         "",
+	Format:         formatText,
+	Save:           "",
+	Host:           "",
+	HostNetworkKey: "",
+	Network:        config.DefaultEmulatorNetwork().Name,
+	Log:            logLevelInfo,
+	Yes:            false,
+	ConfigPaths:    config.DefaultPaths(),
 }
 
 // InitFlags init all the global persistent flags.
@@ -70,6 +72,14 @@ func InitFlags(cmd *cobra.Command) {
 		"Flow Access API host address",
 	)
 
+	cmd.PersistentFlags().StringVarP(
+		&Flags.HostNetworkKey,
+		"network-key",
+		"",
+		Flags.HostNetworkKey,
+		"Flow Access API host network key for secure client connections",
+	)
+
 	cmd.PersistentFlags().StringVarP(
 		&Flags.Format,
 		"output",
diff --git a/internal/config/add-network.go b/internal/config/add-network.go
index 93265d4e..b736f104 100644
--- a/internal/config/add-network.go
+++ b/internal/config/add-network.go
@@ -23,6 +23,7 @@ import (
 	"net/url"
 
 	"github.com/onflow/flow-cli/pkg/flowkit"
+	"github.com/onflow/flow-cli/pkg/flowkit/util"
 
 	"github.com/spf13/cobra"
 
@@ -35,6 +36,7 @@ import (
 type flagsAddNetwork struct {
 	Name string `flag:"name" info:"Network name"`
 	Host string `flag:"host" info:"Flow Access API host address"`
+	Key  string `flag:"network-key" info:"Flow Access API host network key for secure client connections"`
 }
 
 var addNetworkFlags = flagsAddNetwork{}
@@ -66,7 +68,7 @@ func addNetwork(
 		networkData = output.NewNetworkPrompt()
 	}
 
-	network := config.StringToNetwork(networkData["name"], networkData["host"])
+	network := config.StringToNetwork(networkData["name"], networkData["host"], networkData["key"])
 	state.Networks().AddOrUpdate(network.Name, network)
 
 	err = state.SaveEdited(globalFlags.ConfigPaths)
@@ -95,8 +97,14 @@ func flagsToNetworkData(flags flagsAddNetwork) (map[string]string, bool, error)
 		return nil, true, err
 	}
 
+	err = util.ValidateECDSAP256Pub(flags.Key)
+	if err != nil {
+		return nil, true, fmt.Errorf("invalid network-key provided")
+	}
+
 	return map[string]string{
 		"name": flags.Name,
 		"host": flags.Host,
+		"key":  flags.Key,
 	}, true, nil
 }
diff --git a/internal/snapshot/save.go b/internal/snapshot/save.go
index 084df3ca..a019163a 100644
--- a/internal/snapshot/save.go
+++ b/internal/snapshot/save.go
@@ -34,6 +34,7 @@ var SaveCommand = &command.Command{
 		Use:     "save",
 		Short:   "Get the latest finalized protocol snapshot",
 		Example: "flow snapshot save /tmp/snapshot.json",
+		Args:    cobra.ExactArgs(1),
 	},
 	Flags: &struct{}{},
 	Run:   save,
diff --git a/pkg/flowkit/config/json/network.go b/pkg/flowkit/config/json/network.go
index 92cf26b8..c7f9fe04 100644
--- a/pkg/flowkit/config/json/network.go
+++ b/pkg/flowkit/config/json/network.go
@@ -20,55 +20,94 @@ package json
 
 import (
 	"encoding/json"
+	"fmt"
 
 	"github.com/onflow/flow-cli/pkg/flowkit/config"
+	"github.com/onflow/flow-cli/pkg/flowkit/util"
 )
 
 type jsonNetworks map[string]jsonNetwork
 
 // transformToConfig transforms json structures to config structure.
 func (j jsonNetworks) transformToConfig() (config.Networks, error) {
-	var networks config.Networks
+	networks := make(config.Networks, 0)
 
 	for networkName, n := range j {
-		network := config.Network{
-			Name: networkName,
-			Host: n.Host,
+		if n.Advanced.Key != "" && n.Advanced.Host != "" {
+			err := util.ValidateECDSAP256Pub(n.Advanced.Key)
+			if err != nil {
+				return nil, fmt.Errorf("invalid key %s for network with name %s", n.Advanced.Key, networkName)
+			}
+
+			networks = append(networks, config.Network{
+				Name: networkName,
+				Host: n.Advanced.Host,
+				Key:  n.Advanced.Key,
+			})
+		} else if n.Simple.Host != "" {
+			networks = append(networks, config.Network{
+				Name: networkName,
+				Host: n.Simple.Host,
+			})
+		} else {
+			return nil, fmt.Errorf("failed to transform networks configuration")
 		}
-
-		networks = append(networks, network)
 	}
 
 	return networks, nil
 }
 
-// transformToJSON transforms config structure to json structures for saving.
+// transformNetworksToJSON transforms config structure to json structures for saving.
 func transformNetworksToJSON(networks config.Networks) jsonNetworks {
 	jsonNetworks := jsonNetworks{}
 
 	for _, n := range networks {
-		jsonNetworks[n.Name] = jsonNetwork{
-			Host: n.Host,
+		if n.Key != "" {
+			jsonNetworks[n.Name] = transformAdvancedNetworkToJSON(n)
+		} else {
+			jsonNetworks[n.Name] = transformSimpleNetworkToJSON(n)
 		}
 	}
 
 	return jsonNetworks
 }
 
+func transformSimpleNetworkToJSON(n config.Network) jsonNetwork {
+	return jsonNetwork{
+		Simple: simpleNetwork{
+			Host: n.Host,
+		},
+	}
+}
+
+func transformAdvancedNetworkToJSON(n config.Network) jsonNetwork {
+	return jsonNetwork{
+		Advanced: advancedNetwork{
+			Host: n.Host,
+			Key:  n.Key,
+		},
+	}
+}
+
 type jsonNetwork struct {
-	Host string
+	Simple   simpleNetwork
+	Advanced advancedNetwork
+}
+
+type simpleNetwork struct {
+	Host string `json:"host"`
 }
 
 type advancedNetwork struct {
-	Host  string
-	Chain string
+	Host string `json:"host"`
+	Key  string `json:"key"`
 }
 
 func (j *jsonNetwork) UnmarshalJSON(b []byte) error {
 	var host string
 	err := json.Unmarshal(b, &host)
 	if err == nil {
-		j.Host = host
+		j.Simple.Host = host
 		return nil
 	}
 
@@ -76,12 +115,17 @@ func (j *jsonNetwork) UnmarshalJSON(b []byte) error {
 	var advanced advancedNetwork
 	err = json.Unmarshal(b, &advanced)
 	if err == nil {
-		j.Host = advanced.Host
+		j.Advanced.Host = advanced.Host
+		j.Advanced.Key = advanced.Key
 	}
 
 	return err
 }
 
 func (j jsonNetwork) MarshalJSON() ([]byte, error) {
-	return json.Marshal(j.Host)
+	if j.Simple != (simpleNetwork{}) {
+		return json.Marshal(j.Simple.Host)
+	}
+
+	return json.Marshal(j.Advanced)
 }
diff --git a/pkg/flowkit/config/network.go b/pkg/flowkit/config/network.go
index 931a90b0..ab373e64 100644
--- a/pkg/flowkit/config/network.go
+++ b/pkg/flowkit/config/network.go
@@ -28,6 +28,7 @@ type Networks []Network
 type Network struct {
 	Name string
 	Host string
+	Key  string
 }
 
 // ByName get network by name.
diff --git a/pkg/flowkit/config/parsers.go b/pkg/flowkit/config/parsers.go
index 5e5a4ec0..11db6cd1 100644
--- a/pkg/flowkit/config/parsers.go
+++ b/pkg/flowkit/config/parsers.go
@@ -155,10 +155,11 @@ func StringToContracts(
 }
 
 // StringToNetwork converts string to network.
-func StringToNetwork(name string, host string) Network {
+func StringToNetwork(name, host, networkKey string) Network {
 	return Network{
 		Name: name,
 		Host: host,
+		Key:  networkKey,
 	}
 }
 
diff --git a/pkg/flowkit/gateway/emulator.go b/pkg/flowkit/gateway/emulator.go
index 5ec08859..122b8d1f 100644
--- a/pkg/flowkit/gateway/emulator.go
+++ b/pkg/flowkit/gateway/emulator.go
@@ -208,3 +208,8 @@ func (g *EmulatorGateway) GetBlockByHeight(height uint64) (*flow.Block, error) {
 func (g *EmulatorGateway) GetLatestProtocolStateSnapshot() ([]byte, error) {
 	return []byte{}, nil
 }
+
+// SecureConnection placeholder func to complete gateway interface implementation
+func (g *EmulatorGateway) SecureConnection() bool {
+	return false
+}
diff --git a/pkg/flowkit/gateway/gateway.go b/pkg/flowkit/gateway/gateway.go
index 046daf82..6b10f68f 100644
--- a/pkg/flowkit/gateway/gateway.go
+++ b/pkg/flowkit/gateway/gateway.go
@@ -40,4 +40,5 @@ type Gateway interface {
 	GetCollection(flow.Identifier) (*flow.Collection, error)
 	GetLatestProtocolStateSnapshot() ([]byte, error)
 	Ping() error
+	SecureConnection() bool
 }
diff --git a/pkg/flowkit/gateway/grpc.go b/pkg/flowkit/gateway/grpc.go
index 0c572eea..5e306185 100644
--- a/pkg/flowkit/gateway/grpc.go
+++ b/pkg/flowkit/gateway/grpc.go
@@ -21,8 +21,11 @@ package gateway
 import (
 	"context"
 	"fmt"
+	"strings"
 	"time"
 
+	"github.com/onflow/flow-go/utils/grpcutils"
+
 	"github.com/onflow/flow-cli/pkg/flowkit"
 
 	"github.com/onflow/cadence"
@@ -38,8 +41,9 @@ const maxGRPCMessageSize = 1024 * 1024 * 16
 
 // GrpcGateway is a gateway implementation that uses the Flow Access gRPC API.
 type GrpcGateway struct {
-	client *client.Client
-	ctx    context.Context
+	client       *client.Client
+	ctx          context.Context
+	secureClient bool
 }
 
 // NewGrpcGateway returns a new gRPC gateway.
@@ -57,8 +61,34 @@ func NewGrpcGateway(host string) (*GrpcGateway, error) {
 	}
 
 	return &GrpcGateway{
-		client: gClient,
-		ctx:    ctx,
+		client:       gClient,
+		ctx:          ctx,
+		secureClient: false,
+	}, nil
+}
+
+// NewSecureGrpcGateway returns a new gRPC gateway with a secure client connection.
+func NewSecureGrpcGateway(host, hostNetworkKey string) (*GrpcGateway, error) {
+	secureDialOpts, err := grpcutils.SecureGRPCDialOpt(strings.TrimPrefix(hostNetworkKey, "0x"))
+	if err != nil {
+		return nil, fmt.Errorf("failed to create secure GRPC dial options with network key \"%s\": %w", hostNetworkKey, err)
+	}
+
+	gClient, err := client.New(
+		host,
+		secureDialOpts,
+		grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxGRPCMessageSize)),
+	)
+	ctx := context.Background()
+
+	if err != nil || gClient == nil {
+		return nil, fmt.Errorf("failed to connect to host %s", host)
+	}
+
+	return &GrpcGateway{
+		client:       gClient,
+		ctx:          ctx,
+		secureClient: true,
 	}, nil
 }
 
@@ -163,3 +193,8 @@ func (g *GrpcGateway) GetLatestProtocolStateSnapshot() ([]byte, error) {
 func (g *GrpcGateway) Ping() error {
 	return g.client.Ping(g.ctx)
 }
+
+// SecureConnection is used to log warning if a service should be using a secure client but is not
+func (g *GrpcGateway) SecureConnection() bool {
+	return g.secureClient
+}
diff --git a/pkg/flowkit/output/prompt.go b/pkg/flowkit/output/prompt.go
index 068df6b3..3cdcf8b0 100644
--- a/pkg/flowkit/output/prompt.go
+++ b/pkg/flowkit/output/prompt.go
@@ -157,6 +157,25 @@ func namePrompt() string {
 	return name
 }
 
+func secureNetworkKeyPrompt() string {
+	networkKeyPrompt := promptui.Prompt{
+		Label: "Enter a valid host network key or leave blank",
+		Validate: func(s string) error {
+			if s == "" {
+				return nil
+			}
+
+			return util.ValidateECDSAP256Pub(s)
+		},
+	}
+	networkKey, err := networkKeyPrompt.Run()
+	if err == promptui.ErrInterrupt {
+		os.Exit(-1)
+	}
+
+	return networkKey
+}
+
 func addressPrompt() string {
 	addressPrompt := promptui.Prompt{
 		Label: "Address",
@@ -321,6 +340,8 @@ func NewNetworkPrompt() map[string]string {
 		os.Exit(-1)
 	}
 
+	networkData["key"] = secureNetworkKeyPrompt()
+
 	return networkData
 }
 
diff --git a/pkg/flowkit/services/snapshot.go b/pkg/flowkit/services/snapshot.go
index 63d40d9f..be2e64eb 100644
--- a/pkg/flowkit/services/snapshot.go
+++ b/pkg/flowkit/services/snapshot.go
@@ -50,6 +50,10 @@ func NewSnapshot(
 func (s *Snapshot) GetLatestProtocolStateSnapshot() ([]byte, error) {
 	s.logger.StartProgress("Downloading protocol snapshot...")
 
+	if !s.gateway.SecureConnection() {
+		s.logger.Info(fmt.Sprintf("%s warning: using insecure client connection to download snapshot, you should use a secure network configuration...", output.WarningEmoji()))
+	}
+
 	b, err := s.gateway.GetLatestProtocolStateSnapshot()
 	if err != nil {
 		return nil, fmt.Errorf("failed to get latest finalized protocol snapshot from gateway: %w", err)
diff --git a/pkg/flowkit/util/utilities.go b/pkg/flowkit/util/utilities.go
index 3d69d7b5..4b27ec15 100644
--- a/pkg/flowkit/util/utilities.go
+++ b/pkg/flowkit/util/utilities.go
@@ -20,7 +20,9 @@ package util
 
 import (
 	"bytes"
+	"encoding/hex"
 	"fmt"
+	"strings"
 	"text/tabwriter"
 
 	"github.com/onflow/flow-go-sdk"
@@ -99,3 +101,18 @@ func RemoveFromStringArray(s []string, el string) []string {
 
 	return s
 }
+
+// ValidateECDSAP256Pub attempt to decode the hex string representation of a ECDSA P256 public key
+func ValidateECDSAP256Pub(key string) error {
+	b, err := hex.DecodeString(strings.TrimPrefix(key, "0x"))
+	if err != nil {
+		return fmt.Errorf("failed to decode public key hex string: %w", err)
+	}
+
+	_, err = crypto.DecodePublicKey(crypto.ECDSA_P256, b)
+	if err != nil {
+		return fmt.Errorf("failed to decode public key: %w", err)
+	}
+
+	return nil
+}


Test patch:
diff --git a/pkg/flowkit/config/config_test.go b/pkg/flowkit/config/config_test.go
index 0f8e5231..5bbf353f 100644
--- a/pkg/flowkit/config/config_test.go
+++ b/pkg/flowkit/config/config_test.go
@@ -130,6 +130,10 @@ func generateComplexConfig() config.Config {
 		Networks: config.Networks{{
 			Name: "emulator",
 			Host: "127.0.0.1.3569",
+		}, {
+			Name: "testnet",
+			Host: "access.devnet.nodes.onflow.org:9000",
+			Key:  "5000676131ad3e22d853a3f75a5b5d0db4236d08dd6612e2baad771014b5266a242bccecc3522ff7207ac357dbe4f225c709d9b273ac484fed5d13976a39bdcd",
 		}},
 	}
 }
@@ -188,7 +192,11 @@ func Test_GetDeploymentsByNetworkComplex(t *testing.T) {
 func Test_GetNetworkByNameComplex(t *testing.T) {
 	conf := generateComplexConfig()
 	network, err := conf.Networks.ByName("emulator")
-
 	assert.NoError(t, err)
 	assert.Equal(t, network.Host, "127.0.0.1.3569")
+
+	network, err = conf.Networks.ByName("testnet")
+	assert.NoError(t, err)
+	assert.Equal(t, network.Host, "access.devnet.nodes.onflow.org:9000")
+	assert.Equal(t, network.Key, "5000676131ad3e22d853a3f75a5b5d0db4236d08dd6612e2baad771014b5266a242bccecc3522ff7207ac357dbe4f225c709d9b273ac484fed5d13976a39bdcd")
 }
diff --git a/pkg/flowkit/config/json/network_test.go b/pkg/flowkit/config/json/network_test.go
index 3ad2d163..c81d35a4 100644
--- a/pkg/flowkit/config/json/network_test.go
+++ b/pkg/flowkit/config/json/network_test.go
@@ -44,8 +44,11 @@ func Test_ConfigNetworkSimple(t *testing.T) {
 
 func Test_ConfigNetworkMultiple(t *testing.T) {
 	b := []byte(`{
-    "emulator": "127.0.0.1:3569",
-    "testnet": "access.testnet.nodes.onflow.org:9000"
+    		"emulator": "127.0.0.1:3569",
+    		"testnet": {
+				"host": "access.testnet.nodes.onflow.org:9000",
+				"key": "5000676131ad3e22d853a3f75a5b5d0db4236d08dd6612e2baad771014b5266a242bccecc3522ff7207ac357dbe4f225c709d9b273ac484fed5d13976a39bdcd"
+			}
 	}`)
 
 	var jsonNetworks jsonNetworks
@@ -59,6 +62,7 @@ func Test_ConfigNetworkMultiple(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, network.Host, "access.testnet.nodes.onflow.org:9000")
 	assert.Equal(t, network.Name, "testnet")
+	assert.Equal(t, network.Key, "5000676131ad3e22d853a3f75a5b5d0db4236d08dd6612e2baad771014b5266a242bccecc3522ff7207ac357dbe4f225c709d9b273ac484fed5d13976a39bdcd")
 
 	emulator, err := networks.ByName("emulator")
 	assert.NoError(t, err)
@@ -67,7 +71,7 @@ func Test_ConfigNetworkMultiple(t *testing.T) {
 }
 
 func Test_TransformNetworkToJSON(t *testing.T) {
-	b := []byte(`{"emulator":"127.0.0.1:3569","testnet":"access.testnet.nodes.onflow.org:9000"}`)
+	b := []byte(`{"emulator":"127.0.0.1:3569","testnet":{"host":"access.testnet.nodes.onflow.org:9000","key":"5000676131ad3e22d853a3f75a5b5d0db4236d08dd6612e2baad771014b5266a242bccecc3522ff7207ac357dbe4f225c709d9b273ac484fed5d13976a39bdcd"}}`)
 
 	var jsonNetworks jsonNetworks
 	err := json.Unmarshal(b, &jsonNetworks)
@@ -82,15 +86,8 @@ func Test_TransformNetworkToJSON(t *testing.T) {
 	assert.Equal(t, string(b), string(x))
 }
 
-func Test_IngoreOldFormat(t *testing.T) {
-	b := []byte(`{
-		"emulator":"127.0.0.1:3569",
-		"testnet":"access.testnet.nodes.onflow.org:9000",
-		"mainnet": {
-			"host": "access.mainnet.nodes.onflow.org:9000",
-			"chain": "flow-mainnet"
-		}
-	}`)
+func Test_IgnoreOldFormat(t *testing.T) {
+	b := []byte(`{"emulator":"127.0.0.1:3569","testnet":{"host":"access.testnet.nodes.onflow.org:9000","key":"5000676131ad3e22d853a3f75a5b5d0db4236d08dd6612e2baad771014b5266a242bccecc3522ff7207ac357dbe4f225c709d9b273ac484fed5d13976a39bdcd"},"mainnet":{"host": "access.mainnet.nodes.onflow.org:9000","chain":"flow-mainnet","key":"5000676131ad3e22d853a3f75a5b5d0db4236d08dd6612e2baad771014b5266a242bccecc3522ff7207ac357dbe4f225c709d9b273ac484fed5d13976a39bdcd"}}`)
 
 	var jsonNetworks jsonNetworks
 	err := json.Unmarshal(b, &jsonNetworks)
@@ -108,5 +105,51 @@ func Test_IngoreOldFormat(t *testing.T) {
 	assert.NoError(t, err)
 
 	assert.Equal(t, testnet.Host, "access.testnet.nodes.onflow.org:9000")
+	assert.Equal(t, testnet.Key, "5000676131ad3e22d853a3f75a5b5d0db4236d08dd6612e2baad771014b5266a242bccecc3522ff7207ac357dbe4f225c709d9b273ac484fed5d13976a39bdcd")
 	assert.Equal(t, mainnet.Host, "access.mainnet.nodes.onflow.org:9000")
+	assert.Equal(t, mainnet.Key, "5000676131ad3e22d853a3f75a5b5d0db4236d08dd6612e2baad771014b5266a242bccecc3522ff7207ac357dbe4f225c709d9b273ac484fed5d13976a39bdcd")
+}
+
+func Test_TransformConfigAdvanced(t *testing.T) {
+	t.Run("should returned advanced config with no errors", func(t *testing.T) {
+		b := []byte(`{"testnet":{"host":"access.testnet.nodes.onflow.org:9000", "key": "5000676131ad3e22d853a3f75a5b5d0db4236d08dd6612e2baad771014b5266a242bccecc3522ff7207ac357dbe4f225c709d9b273ac484fed5d13976a39bdcd"}}`)
+		var jsonNetworks jsonNetworks
+		err := json.Unmarshal(b, &jsonNetworks)
+		assert.NoError(t, err)
+
+		conf, err := jsonNetworks.transformToConfig()
+		assert.NoError(t, err)
+
+		testnet, err := conf.ByName("testnet")
+		assert.NoError(t, err)
+		assert.Equal(t, testnet.Host, "access.testnet.nodes.onflow.org:9000")
+		assert.Equal(t, testnet.Key, "5000676131ad3e22d853a3f75a5b5d0db4236d08dd6612e2baad771014b5266a242bccecc3522ff7207ac357dbe4f225c709d9b273ac484fed5d13976a39bdcd")
+	})
+	t.Run("should return error if advanced config does not have key", func(t *testing.T) {
+		b := []byte(`{"testnet":{"host":"access.testnet.nodes.onflow.org:9000"}}`)
+		var jsonNetworks jsonNetworks
+		err := json.Unmarshal(b, &jsonNetworks)
+		assert.NoError(t, err)
+
+		_, err = jsonNetworks.transformToConfig()
+		assert.Error(t, err)
+	})
+	t.Run("should return error if advanced config does not have host", func(t *testing.T) {
+		b := []byte(`{"testnet":{"key": "5000676131ad3e22d853a3f75a5b5d0db4236d08dd6612e2baad771014b5266a242bccecc3522ff7207ac357dbe4f225c709d9b273ac484fed5d13976a39bdcd"}}`)
+		var jsonNetworks jsonNetworks
+		err := json.Unmarshal(b, &jsonNetworks)
+		assert.NoError(t, err)
+
+		_, err = jsonNetworks.transformToConfig()
+		assert.Error(t, err)
+	})
+	t.Run("should return error if advanced config provides invalid network key", func(t *testing.T) {
+		b := []byte(`{"testnet":{"host":"access.testnet.nodes.onflow.org:9000","key": "0xpublickey"}}`)
+		var jsonNetworks jsonNetworks
+		err := json.Unmarshal(b, &jsonNetworks)
+		assert.NoError(t, err)
+
+		_, err = jsonNetworks.transformToConfig()
+		assert.Error(t, err)
+	})
 }
diff --git a/tests/mocks/Gateway.go b/tests/mocks/Gateway.go
index 05ff1704..2f88ef05 100644
--- a/tests/mocks/Gateway.go
+++ b/tests/mocks/Gateway.go
@@ -262,6 +262,20 @@ func (_m *Gateway) Ping() error {
 	return r0
 }
 
+// SecureConnection provides a mock function with given fields:
+func (_m *Gateway) SecureConnection() bool {
+	ret := _m.Called()
+
+	var r0 bool
+	if rf, ok := ret.Get(0).(func() bool); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(bool)
+	}
+
+	return r0
+}
+
 // SendSignedTransaction provides a mock function with given fields: _a0
 func (_m *Gateway) SendSignedTransaction(_a0 *flowkit.Transaction) (*flow.Transaction, error) {
 	ret := _m.Called(_a0)