Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add gnoland secrets command suite #1593

Merged
merged 24 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
00a36d8
Add secrets init all
zivkovicmilos Jan 25, 2024
a248ff5
Add secrets init single
zivkovicmilos Jan 25, 2024
d21b713
Add secrets init unit tests
zivkovicmilos Jan 26, 2024
dc2ccf9
Add verify subcommands and tests
zivkovicmilos Jan 26, 2024
d81e84b
Add show subcommands
zivkovicmilos Jan 26, 2024
267a4f0
Add show all unit tests
zivkovicmilos Jan 26, 2024
862e796
Add show subcommand unit tests
zivkovicmilos Jan 26, 2024
13ee6be
Merge branch 'master' into feat/secrets
zivkovicmilos Jan 29, 2024
7d24dd4
Make 'secrets' a subcommand of 'gnoland'
zivkovicmilos Jan 29, 2024
8743a3d
Merge branch 'master' into feat/secrets
gfanton Feb 15, 2024
e1af56e
Merge branch 'master' into feat/secrets
zivkovicmilos Mar 14, 2024
402feac
Merge branch 'master' into feat/secrets
zivkovicmilos Mar 27, 2024
5367c44
Standardize gnoland secrets
zivkovicmilos Mar 27, 2024
08660a5
Tidy error handling
zivkovicmilos Mar 27, 2024
ea258b3
Add available keys in the secrets long help
zivkovicmilos Mar 28, 2024
88a6247
Merge branch 'master' into feat/secrets
zivkovicmilos Mar 29, 2024
5096f07
Merge branch 'master' into feat/secrets
zivkovicmilos Apr 1, 2024
572c3b2
Swap indent character in amino marshal
zivkovicmilos Apr 1, 2024
889693b
Add additional explanation on single-key manipulation
zivkovicmilos Apr 1, 2024
bda224e
Move out generation logic to secrets_init
zivkovicmilos Apr 1, 2024
888712c
Add warning about skipping validator signed state verification
zivkovicmilos Apr 1, 2024
6965f12
Rename key value
zivkovicmilos Apr 1, 2024
c058f86
Move common methods to secrets_common
zivkovicmilos Apr 1, 2024
cf5abb6
Add overwrite protection
zivkovicmilos Apr 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gno.land/cmd/gnoland/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func newRootCmd(io commands.IO) *commands.Command {

cmd.AddSubCommands(
newStartCmd(io),
newSecretsCmd(io),
newConfigCmd(io),
)

Expand Down
94 changes: 94 additions & 0 deletions gno.land/cmd/gnoland/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import (
"errors"
"flag"
"fmt"

"github.com/gnolang/gno/tm2/pkg/commands"
)

var (
errInvalidDataDir = errors.New("invalid data directory provided")
errInvalidSecretsKey = errors.New("invalid number of secret key arguments")
)

const (
defaultSecretsDir = "./secrets"
defaultValidatorKeyName = "priv_validator_key.json"
defaultNodeKeyName = "node_key.json"
defaultValidatorStateName = "priv_validator_state.json"
)

const (
nodeKeyKey = "NodeKey"
validatorPrivateKeyKey = "ValidatorPrivateKey"
validatorStateKey = "ValidatorStateKey"
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
)

// newSecretsCmd creates the secrets root command
func newSecretsCmd(io commands.IO) *commands.Command {
cmd := commands.NewCommand(
commands.Metadata{
Name: "secrets",
ShortUsage: "secrets <subcommand> [flags] [<arg>...]",
ShortHelp: "gno secrets manipulation suite",
LongHelp: "gno secrets manipulation suite, for managing the validator key, p2p key and validator state",
},
commands.NewEmptyConfig(),
commands.HelpExec,
)

cmd.AddSubCommands(
newSecretsInitCmd(io),
newSecretsVerifyCmd(io),
newSecretsGetCmd(io),
)

return cmd
}

// commonAllCfg is the common
// configuration for secrets commands
// that require a bundled secrets dir
type commonAllCfg struct {
dataDir string
}

func (c *commonAllCfg) RegisterFlags(fs *flag.FlagSet) {
fs.StringVar(
&c.dataDir,
"data-dir",
defaultSecretsDir,
"the secrets output directory",
)
}

// verifySecretsKey verifies the secrets key value from the passed in arguments
func verifySecretsKey(args []string) error {
// Check if any key is set
if len(args) == 0 {
return nil
}

// Check if more than 1 key is set
if len(args) > 1 {
return errInvalidSecretsKey

Check warning on line 76 in gno.land/cmd/gnoland/secrets.go

View check run for this annotation

Codecov / codecov/patch

gno.land/cmd/gnoland/secrets.go#L76

Added line #L76 was not covered by tests
}

// Verify the set key
key := args[0]

if key != nodeKeyKey &&
key != validatorPrivateKeyKey &&
key != validatorStateKey {
return fmt.Errorf(
"invalid secrets key value [%s, %s, %s]",
validatorPrivateKeyKey,
validatorStateKey,
nodeKeyKey,
)

Check warning on line 90 in gno.land/cmd/gnoland/secrets.go

View check run for this annotation

Codecov / codecov/patch

gno.land/cmd/gnoland/secrets.go#L85-L90

Added lines #L85 - L90 were not covered by tests
}

return nil
}
190 changes: 190 additions & 0 deletions gno.land/cmd/gnoland/secrets_common.go
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package main

import (
"errors"
"fmt"
"os"

"github.com/gnolang/gno/tm2/pkg/amino"
"github.com/gnolang/gno/tm2/pkg/bft/privval"
"github.com/gnolang/gno/tm2/pkg/crypto"
"github.com/gnolang/gno/tm2/pkg/crypto/ed25519"
"github.com/gnolang/gno/tm2/pkg/p2p"
)

var (
errInvalidPrivateKey = errors.New("invalid validator private key")
errPublicKeyMismatch = errors.New("public key does not match private key derivation")
errAddressMismatch = errors.New("address does not match public key")

errInvalidSignStateStep = errors.New("invalid sign state step value")
errInvalidSignStateHeight = errors.New("invalid sign state height value")
errInvalidSignStateRound = errors.New("invalid sign state round value")

errSignatureMismatch = errors.New("signature does not match signature bytes")
errSignatureValuesMissing = errors.New("missing signature value")

errInvalidNodeKey = errors.New("invalid node p2p key")
)

// generateValidatorPrivateKey generates the validator's private key
func generateValidatorPrivateKey() *privval.FilePVKey {
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
privKey := ed25519.GenPrivKey()

return &privval.FilePVKey{
Address: privKey.PubKey().Address(),
PubKey: privKey.PubKey(),
PrivKey: privKey,
}
}

// generateLastSignValidatorState generates the empty last sign state
func generateLastSignValidatorState() *privval.FilePVLastSignState {
return &privval.FilePVLastSignState{} // Empty last sign state
}

// generateNodeKey generates the p2p node key
func generateNodeKey() *p2p.NodeKey {
privKey := ed25519.GenPrivKey()

return &p2p.NodeKey{
PrivKey: privKey,
}
}

// saveSecretData saves the given data as Amino JSON to the path
func saveSecretData(data any, path string) error {
// Get Amino JSON
marshalledData, err := amino.MarshalJSONIndent(data, "", " ")
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("unable to marshal data into JSON, %w", err)

Check warning on line 60 in gno.land/cmd/gnoland/secrets_common.go

View check run for this annotation

Codecov / codecov/patch

gno.land/cmd/gnoland/secrets_common.go#L60

Added line #L60 was not covered by tests
}

// Save the data to disk
if err := os.WriteFile(path, marshalledData, 0o644); err != nil {
return fmt.Errorf("unable to save data to path, %w", err)
}

return nil
}

// isValidDirectory verifies the directory at the given path exists
func isValidDirectory(dirPath string) bool {
fileInfo, err := os.Stat(dirPath)
if err != nil {
return false

Check warning on line 75 in gno.land/cmd/gnoland/secrets_common.go

View check run for this annotation

Codecov / codecov/patch

gno.land/cmd/gnoland/secrets_common.go#L75

Added line #L75 was not covered by tests
}

// Check if the path is indeed a directory
return fileInfo.IsDir()
}

type secretData interface {
privval.FilePVKey | privval.FilePVLastSignState | p2p.NodeKey
}

// readSecretData reads the secret data from the given path
func readSecretData[T secretData](
path string,
) (*T, error) {
dataRaw, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("unable to read data, %w", err)
}

var data T
if err := amino.UnmarshalJSON(dataRaw, &data); err != nil {
return nil, fmt.Errorf("unable to unmarshal data, %w", err)
}

return &data, nil
}

// validateValidatorKey validates the validator's private key
func validateValidatorKey(key *privval.FilePVKey) error {
// Make sure the private key is set
if key.PrivKey == nil {
return errInvalidPrivateKey
}

// Make sure the public key is derived
// from the private one
if !key.PrivKey.PubKey().Equals(key.PubKey) {
return errPublicKeyMismatch
}

// Make sure the address is derived
// from the public key
if key.PubKey.Address().Compare(key.Address) != 0 {
return errAddressMismatch
}

return nil
}

// validateValidatorState validates the validator's last sign state
func validateValidatorState(state *privval.FilePVLastSignState) error {
// Make sure the sign step is valid
if state.Step < 0 {
return errInvalidSignStateStep
}

// Make sure the height is valid
if state.Height < 0 {
return errInvalidSignStateHeight
}

// Make sure the round is valid
if state.Round < 0 {
return errInvalidSignStateRound
}

return nil
}

// validateValidatorStateSignature validates the signature section
// of the last sign validator state
func validateValidatorStateSignature(
state *privval.FilePVLastSignState,
key crypto.PubKey,
) error {
// Make sure the signature and signature bytes are valid
signBytesPresent := state.SignBytes != nil
signaturePresent := state.Signature != nil

if signBytesPresent && !signaturePresent ||
!signBytesPresent && signaturePresent {
return errSignatureValuesMissing
}

if !signaturePresent {
// No need to verify further
return nil
}

// Make sure the signature bytes match the signature
if !key.VerifyBytes(state.SignBytes, state.Signature) {
return errSignatureMismatch
}

return nil
}

// validateNodeKey validates the node's p2p key
func validateNodeKey(key *p2p.NodeKey) error {
if key.PrivKey == nil {
return errInvalidNodeKey
}

return nil
}

// getAvailableSecretsKeys formats and returns the available secret keys (constants)
func getAvailableSecretsKeys() string {
return fmt.Sprintf(
"[%s, %s, %s]",
validatorPrivateKeyKey,
nodeKeyKey,
validatorStateKey,
)
}
Loading
Loading