From 4ca83c17beb7c0586e1a37bf8309ffa1e112a260 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Wed, 18 Nov 2020 17:32:39 +0100 Subject: [PATCH 1/8] CLI command to sign and verify operator's signature We added functions to let operator running the client to calculate and verify ethereum signatures. This gives easy way of confirming messages with operator's key without a need to pull the key out of the node running the client or installing additional libraries to handle the signing. --- cmd/signing.go | 148 ++++++++++++++++++++++++++++++++++++++ internal/config/config.go | 5 +- 2 files changed, 151 insertions(+), 2 deletions(-) diff --git a/cmd/signing.go b/cmd/signing.go index a02260e61..e918f3591 100644 --- a/cmd/signing.go +++ b/cmd/signing.go @@ -1,7 +1,9 @@ package cmd import ( + "bytes" "context" + "crypto/sha256" "encoding/hex" "fmt" "io/ioutil" @@ -9,6 +11,7 @@ import ( "sync" "time" + "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil" "github.com/keep-network/keep-common/pkg/logging" "github.com/keep-network/keep-core/pkg/net" "github.com/keep-network/keep-core/pkg/net/key" @@ -19,6 +22,8 @@ import ( eth "github.com/keep-network/keep-ecdsa/pkg/chain" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/keep-network/keep-common/pkg/persistence" "github.com/keep-network/keep-ecdsa/internal/config" "github.com/keep-network/keep-ecdsa/pkg/registry" @@ -29,6 +34,22 @@ import ( // subcommand and its own subcommands. var SigningCommand cli.Command +const ethereumSignDescription = "Calculates a signature for a given message. " + + "The message is expected to be provided as a string, it is later hashed with " + + "SHA-256 and passed to ECDSA signing. The calculated signature is returned " + + "in Ethereum specific format as a hexadecimal string representation of 65-byte " + + "{R, S, V} parameters.\n" + + "It reads an Ethereum key from an encrypted file. A path to the key file can be " + + "configured in a config file or specified directly with a `eth-key-file` flag. " + + "The key file is expected to be encrypted with a password provided as " + + config.PasswordEnvVariable + " environment variable." + +const ethereumVerifyDescription = "Verifies if a signature was calculated for a " + + "message by an ethereum account identified by an address. It expects a message " + + "to be provided as an original message string, that is later hashed with SHA-256. " + + "Signature should be provided in Ethereum specific format as a hexadecimal string " + + "representation of 65-byte {R, S, V} parameters." + func init() { SigningCommand = cli.Command{ Name: "signing", @@ -57,6 +78,33 @@ func init() { Action: SignDigest, ArgsUsage: "[unprefixed-hex-digest] [key-shares-dir]", }, + { + Name: "ethereum", + Usage: "Ethereum signatures calculation", + Subcommands: []cli.Command{ + { + Name: "sign", + Usage: "Sign a message using operator's key", + Description: ethereumSignDescription, + Action: EthereumSign, + ArgsUsage: "[message]", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "eth-key-file,k", + Usage: "Path to the ethereum key file. " + + "If not provided read the path from a config file.", + }, + }, + }, + { + Name: "verify", + Usage: "Verifies a signature", + Description: ethereumVerifyDescription, + Action: EthereumVerify, + ArgsUsage: "[message] [address] [signature]", + }, + }, + }, }, } } @@ -285,3 +333,103 @@ func SignDigest(c *cli.Context) error { return nil } + +// EthereumSign signs a string using operator's ethereum key. +func EthereumSign(c *cli.Context) error { + message := c.Args().First() + if len(message) == 0 { + return fmt.Errorf("invalid digest") + } + + var ethKeyFilePath, ethKeyPassword string + if ethKeyFile := c.String("eth-key-file"); len(ethKeyFile) > 0 { + ethKeyFilePath = ethKeyFile + ethKeyPassword = os.Getenv(config.PasswordEnvVariable) + } else { + + ethereumConfig, err := config.ReadEthereumConfig(c.GlobalString("config")) + if err != nil { + return fmt.Errorf("failed while reading config file: [%v]", err) + } + + ethKeyFile = ethereumConfig.Account.KeyFile + ethKeyPassword = ethereumConfig.Account.KeyFilePassword + } + + ethereumKey, err := ethutil.DecryptKeyFile(ethKeyFilePath, ethKeyPassword) + if err != nil { + return fmt.Errorf( + "failed to read key file [%s]: [%v]", + ethKeyFilePath, + err, + ) + } + + digest := sha256.Sum256([]byte(message)) + + signature, err := crypto.Sign(digest[:], ethereumKey.PrivateKey) + if err != nil { + return fmt.Errorf("failed to sign: [%v]", err) + } + + signatureHex := hex.EncodeToString(signature) + + fmt.Printf( + "signature calculated by [%s] for message [%s]: %s\n", + ethereumKey.Address.Hex(), message, signatureHex, + ) + + return nil +} + +// EthereumVerify verifies if a signature was calculated by a signer with the +// given ethereum address. +func EthereumVerify(c *cli.Context) error { + message := c.Args().First() + if len(message) == 0 { + return fmt.Errorf("invalid message") + } + + address := c.Args().Get(1) + if len(address) == 0 { + return fmt.Errorf("invalid address") + } + + signature := c.Args().Get(2) + if len(signature) == 0 { + return fmt.Errorf("invalid signature") + } + + signatureBytes, err := hex.DecodeString(signature) + if err != nil { + return fmt.Errorf("could not decode signature string: [%v]", err) + } + + digest := sha256.Sum256([]byte(message)) + + publicKey, err := crypto.SigToPub(digest[:], signatureBytes) + if err != nil { + return fmt.Errorf("could not recover public key from signature [%v]", err) + } + + expectedAddress := common.HexToAddress(address) + recoveredAddress := crypto.PubkeyToAddress(*publicKey) + + if !bytes.Equal(recoveredAddress.Bytes(), expectedAddress.Bytes()) { + return fmt.Errorf( + "invalid signer\n"+ + "\texpected signer: %s\n"+ + "\tactual signer: %s", + expectedAddress.Hex(), + recoveredAddress.Hex(), + ) + } + + fmt.Printf( + "signature verified correctly, message [%s] was signed by [%s]\n", + message, + recoveredAddress.Hex(), + ) + + return nil +} diff --git a/internal/config/config.go b/internal/config/config.go index 4cbcf092d..f0d607367 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,10 +12,11 @@ import ( "github.com/keep-network/keep-ecdsa/pkg/ecdsa/tss" ) +// PasswordEnvVariable environment variable name for ethereum key password. // #nosec G101 (look for hardcoded credentials) // This line doesn't contain any credentials. // It's just the name of the environment variable. -const passwordEnvVariable = "KEEP_ETHEREUM_PASSWORD" +const PasswordEnvVariable = "KEEP_ETHEREUM_PASSWORD" // Config is the top level config structure. type Config struct { @@ -90,7 +91,7 @@ func ReadConfig(filePath string) (*Config, error) { return nil, fmt.Errorf("failed to decode file [%s]: [%v]", filePath, err) } - config.Ethereum.Account.KeyFilePassword = os.Getenv(passwordEnvVariable) + config.Ethereum.Account.KeyFilePassword = os.Getenv(PasswordEnvVariable) return config, nil } From 383bd44fbde91b3fbfe5dc4d5294ef0dff3acf4c Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 19 Nov 2020 12:04:26 +0100 Subject: [PATCH 2/8] Extracted output data part to a common function We can reuse the part where we provide output directory for a file in other functions. Here we extracted this code. --- cmd/signing.go | 60 +++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/cmd/signing.go b/cmd/signing.go index e918f3591..ebaf68faf 100644 --- a/cmd/signing.go +++ b/cmd/signing.go @@ -158,33 +158,7 @@ func DecryptKeyShare(c *cli.Context) error { ) } - if outputFilePath := c.String("output-file"); len(outputFilePath) > 0 { - if _, err := os.Stat(outputFilePath); !os.IsNotExist(err) { - return fmt.Errorf( - "could not write shares to file; file [%s] already exists", - outputFilePath, - ) - } - - err = ioutil.WriteFile(outputFilePath, signerBytes, 0444) // read-only - if err != nil { - return fmt.Errorf( - "failed to write to file [%s]: [%v]", - outputFilePath, - err, - ) - } - } else { - _, err = os.Stdout.Write(signerBytes) - if err != nil { - return fmt.Errorf( - "could not write signer bytes to stdout: [%v]", - err, - ) - } - } - - return nil + return outputData(c, signerBytes) } // SignDigest signs a given digest using key shares from the provided directory. @@ -433,3 +407,35 @@ func EthereumVerify(c *cli.Context) error { return nil } + +func outputData(c *cli.Context, data []byte) error { + if outputFilePath := c.String("output-file"); len(outputFilePath) > 0 { + if _, err := os.Stat(outputFilePath); !os.IsNotExist(err) { + return fmt.Errorf( + "could not write output to a file; file [%s] already exists", + outputFilePath, + ) + } + + err := ioutil.WriteFile(outputFilePath, data, 0444) // read-only + if err != nil { + return fmt.Errorf( + "failed to write output to a file [%s]: [%v]", + outputFilePath, + err, + ) + } + + fmt.Printf("output stored to a file: %s", outputFilePath) + } else { + _, err := os.Stdout.Write(data) + if err != nil { + return fmt.Errorf( + "could not write bytes to stdout: [%v]", + err, + ) + } + } + + return nil +} From b8deaf577cd53bc44464c5a2b063f373e5a207c8 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 19 Nov 2020 12:11:05 +0100 Subject: [PATCH 3/8] Moved signing ethereum command to a dedicated file We want to enhance the code for ethereum signing so it may be a good idea to extract these to a separate file. --- cmd/signing.go | 148 +----------------------------------- cmd/signing_ethereum.go | 161 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 147 deletions(-) create mode 100644 cmd/signing_ethereum.go diff --git a/cmd/signing.go b/cmd/signing.go index ebaf68faf..dcbc73a7c 100644 --- a/cmd/signing.go +++ b/cmd/signing.go @@ -1,9 +1,7 @@ package cmd import ( - "bytes" "context" - "crypto/sha256" "encoding/hex" "fmt" "io/ioutil" @@ -11,7 +9,6 @@ import ( "sync" "time" - "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil" "github.com/keep-network/keep-common/pkg/logging" "github.com/keep-network/keep-core/pkg/net" "github.com/keep-network/keep-core/pkg/net/key" @@ -22,7 +19,6 @@ import ( eth "github.com/keep-network/keep-ecdsa/pkg/chain" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" "github.com/keep-network/keep-common/pkg/persistence" "github.com/keep-network/keep-ecdsa/internal/config" @@ -34,22 +30,6 @@ import ( // subcommand and its own subcommands. var SigningCommand cli.Command -const ethereumSignDescription = "Calculates a signature for a given message. " + - "The message is expected to be provided as a string, it is later hashed with " + - "SHA-256 and passed to ECDSA signing. The calculated signature is returned " + - "in Ethereum specific format as a hexadecimal string representation of 65-byte " + - "{R, S, V} parameters.\n" + - "It reads an Ethereum key from an encrypted file. A path to the key file can be " + - "configured in a config file or specified directly with a `eth-key-file` flag. " + - "The key file is expected to be encrypted with a password provided as " + - config.PasswordEnvVariable + " environment variable." - -const ethereumVerifyDescription = "Verifies if a signature was calculated for a " + - "message by an ethereum account identified by an address. It expects a message " + - "to be provided as an original message string, that is later hashed with SHA-256. " + - "Signature should be provided in Ethereum specific format as a hexadecimal string " + - "representation of 65-byte {R, S, V} parameters." - func init() { SigningCommand = cli.Command{ Name: "signing", @@ -78,33 +58,7 @@ func init() { Action: SignDigest, ArgsUsage: "[unprefixed-hex-digest] [key-shares-dir]", }, - { - Name: "ethereum", - Usage: "Ethereum signatures calculation", - Subcommands: []cli.Command{ - { - Name: "sign", - Usage: "Sign a message using operator's key", - Description: ethereumSignDescription, - Action: EthereumSign, - ArgsUsage: "[message]", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "eth-key-file,k", - Usage: "Path to the ethereum key file. " + - "If not provided read the path from a config file.", - }, - }, - }, - { - Name: "verify", - Usage: "Verifies a signature", - Description: ethereumVerifyDescription, - Action: EthereumVerify, - ArgsUsage: "[message] [address] [signature]", - }, - }, - }, + EthereumSigningCommand, }, } } @@ -308,106 +262,6 @@ func SignDigest(c *cli.Context) error { return nil } -// EthereumSign signs a string using operator's ethereum key. -func EthereumSign(c *cli.Context) error { - message := c.Args().First() - if len(message) == 0 { - return fmt.Errorf("invalid digest") - } - - var ethKeyFilePath, ethKeyPassword string - if ethKeyFile := c.String("eth-key-file"); len(ethKeyFile) > 0 { - ethKeyFilePath = ethKeyFile - ethKeyPassword = os.Getenv(config.PasswordEnvVariable) - } else { - - ethereumConfig, err := config.ReadEthereumConfig(c.GlobalString("config")) - if err != nil { - return fmt.Errorf("failed while reading config file: [%v]", err) - } - - ethKeyFile = ethereumConfig.Account.KeyFile - ethKeyPassword = ethereumConfig.Account.KeyFilePassword - } - - ethereumKey, err := ethutil.DecryptKeyFile(ethKeyFilePath, ethKeyPassword) - if err != nil { - return fmt.Errorf( - "failed to read key file [%s]: [%v]", - ethKeyFilePath, - err, - ) - } - - digest := sha256.Sum256([]byte(message)) - - signature, err := crypto.Sign(digest[:], ethereumKey.PrivateKey) - if err != nil { - return fmt.Errorf("failed to sign: [%v]", err) - } - - signatureHex := hex.EncodeToString(signature) - - fmt.Printf( - "signature calculated by [%s] for message [%s]: %s\n", - ethereumKey.Address.Hex(), message, signatureHex, - ) - - return nil -} - -// EthereumVerify verifies if a signature was calculated by a signer with the -// given ethereum address. -func EthereumVerify(c *cli.Context) error { - message := c.Args().First() - if len(message) == 0 { - return fmt.Errorf("invalid message") - } - - address := c.Args().Get(1) - if len(address) == 0 { - return fmt.Errorf("invalid address") - } - - signature := c.Args().Get(2) - if len(signature) == 0 { - return fmt.Errorf("invalid signature") - } - - signatureBytes, err := hex.DecodeString(signature) - if err != nil { - return fmt.Errorf("could not decode signature string: [%v]", err) - } - - digest := sha256.Sum256([]byte(message)) - - publicKey, err := crypto.SigToPub(digest[:], signatureBytes) - if err != nil { - return fmt.Errorf("could not recover public key from signature [%v]", err) - } - - expectedAddress := common.HexToAddress(address) - recoveredAddress := crypto.PubkeyToAddress(*publicKey) - - if !bytes.Equal(recoveredAddress.Bytes(), expectedAddress.Bytes()) { - return fmt.Errorf( - "invalid signer\n"+ - "\texpected signer: %s\n"+ - "\tactual signer: %s", - expectedAddress.Hex(), - recoveredAddress.Hex(), - ) - } - - fmt.Printf( - "signature verified correctly, message [%s] was signed by [%s]\n", - message, - recoveredAddress.Hex(), - ) - - return nil -} - func outputData(c *cli.Context, data []byte) error { if outputFilePath := c.String("output-file"); len(outputFilePath) > 0 { if _, err := os.Stat(outputFilePath); !os.IsNotExist(err) { diff --git a/cmd/signing_ethereum.go b/cmd/signing_ethereum.go new file mode 100644 index 000000000..8b5072348 --- /dev/null +++ b/cmd/signing_ethereum.go @@ -0,0 +1,161 @@ +package cmd + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil" + "github.com/keep-network/keep-ecdsa/internal/config" + "github.com/urfave/cli" +) + +// EthereumSigningCommand contains the definition of the `signing ethereum` +// command-line subcommand and its own subcommands. +var EthereumSigningCommand = cli.Command{ + Name: "ethereum", + Usage: "Ethereum signatures calculation", + Subcommands: []cli.Command{ + { + Name: "sign", + Usage: "Sign a message using operator's key", + Description: ethereumSignDescription, + Action: EthereumSign, + ArgsUsage: "[message]", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "eth-key-file,k", + Usage: "Path to the ethereum key file. " + + "If not provided read the path from a config file.", + }, + }, + }, + { + Name: "verify", + Usage: "Verifies a signature", + Description: ethereumVerifyDescription, + Action: EthereumVerify, + ArgsUsage: "[message] [address] [signature]", + }, + }, +} + +const ethereumSignDescription = "Calculates a signature for a given message. " + + "The message is expected to be provided as a string, it is later hashed with " + + "SHA-256 and passed to ECDSA signing. The calculated signature is returned " + + "in Ethereum specific format as a hexadecimal string representation of 65-byte " + + "{R, S, V} parameters.\n" + + "It reads an Ethereum key from an encrypted file. A path to the key file can be " + + "configured in a config file or specified directly with a `eth-key-file` flag. " + + "The key file is expected to be encrypted with a password provided as " + + config.PasswordEnvVariable + " environment variable." + +const ethereumVerifyDescription = "Verifies if a signature was calculated for a " + + "message by an ethereum account identified by an address. It expects a message " + + "to be provided as an original message string, that is later hashed with SHA-256. " + + "Signature should be provided in Ethereum specific format as a hexadecimal string " + + "representation of 65-byte {R, S, V} parameters." + +// EthereumSign signs a string using operator's ethereum key. +func EthereumSign(c *cli.Context) error { + message := c.Args().First() + if len(message) == 0 { + return fmt.Errorf("invalid digest") + } + + var ethKeyFilePath, ethKeyPassword string + if ethKeyFile := c.String("eth-key-file"); len(ethKeyFile) > 0 { + ethKeyFilePath = ethKeyFile + ethKeyPassword = os.Getenv(config.PasswordEnvVariable) + } else { + + ethereumConfig, err := config.ReadEthereumConfig(c.GlobalString("config")) + if err != nil { + return fmt.Errorf("failed while reading config file: [%v]", err) + } + + ethKeyFile = ethereumConfig.Account.KeyFile + ethKeyPassword = ethereumConfig.Account.KeyFilePassword + } + + ethereumKey, err := ethutil.DecryptKeyFile(ethKeyFilePath, ethKeyPassword) + if err != nil { + return fmt.Errorf( + "failed to read key file [%s]: [%v]", + ethKeyFilePath, + err, + ) + } + + digest := sha256.Sum256([]byte(message)) + + signature, err := crypto.Sign(digest[:], ethereumKey.PrivateKey) + if err != nil { + return fmt.Errorf("failed to sign: [%v]", err) + } + + signatureHex := hex.EncodeToString(signature) + + fmt.Printf( + "signature calculated by [%s] for message [%s]: %s\n", + ethereumKey.Address.Hex(), message, signatureHex, + ) + + return nil +} + +// EthereumVerify verifies if a signature was calculated by a signer with the +// given ethereum address. +func EthereumVerify(c *cli.Context) error { + message := c.Args().First() + if len(message) == 0 { + return fmt.Errorf("invalid message") + } + + address := c.Args().Get(1) + if len(address) == 0 { + return fmt.Errorf("invalid address") + } + + signature := c.Args().Get(2) + if len(signature) == 0 { + return fmt.Errorf("invalid signature") + } + + signatureBytes, err := hex.DecodeString(signature) + if err != nil { + return fmt.Errorf("could not decode signature string: [%v]", err) + } + + digest := sha256.Sum256([]byte(message)) + + publicKey, err := crypto.SigToPub(digest[:], signatureBytes) + if err != nil { + return fmt.Errorf("could not recover public key from signature [%v]", err) + } + + expectedAddress := common.HexToAddress(address) + recoveredAddress := crypto.PubkeyToAddress(*publicKey) + + if !bytes.Equal(recoveredAddress.Bytes(), expectedAddress.Bytes()) { + return fmt.Errorf( + "invalid signer\n"+ + "\texpected signer: %s\n"+ + "\tactual signer: %s", + expectedAddress.Hex(), + recoveredAddress.Hex(), + ) + } + + fmt.Printf( + "signature verified correctly, message [%s] was signed by [%s]\n", + message, + recoveredAddress.Hex(), + ) + + return nil +} From 299182376b385d2e895c4e297ff32f6ef0b52675 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 19 Nov 2020 12:13:01 +0100 Subject: [PATCH 4/8] Fixed a problem with reading key file path We used wrong name to set the value for key file path variable. We also don't need to introduce another variable, we can use just one. --- cmd/signing_ethereum.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/signing_ethereum.go b/cmd/signing_ethereum.go index 8b5072348..bd2ad2f5d 100644 --- a/cmd/signing_ethereum.go +++ b/cmd/signing_ethereum.go @@ -68,17 +68,17 @@ func EthereumSign(c *cli.Context) error { } var ethKeyFilePath, ethKeyPassword string - if ethKeyFile := c.String("eth-key-file"); len(ethKeyFile) > 0 { - ethKeyFilePath = ethKeyFile + // Check if `eth-key-file` flag was set. If not read the key file path from + // a config file. + if ethKeyFilePath = c.String("eth-key-file"); len(ethKeyFilePath) > 0 { ethKeyPassword = os.Getenv(config.PasswordEnvVariable) } else { - ethereumConfig, err := config.ReadEthereumConfig(c.GlobalString("config")) if err != nil { return fmt.Errorf("failed while reading config file: [%v]", err) } - ethKeyFile = ethereumConfig.Account.KeyFile + ethKeyFilePath = ethereumConfig.Account.KeyFile ethKeyPassword = ethereumConfig.Account.KeyFilePassword } From 06ca54f3ed7c208422d28bb98cea69be9850c8de Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 19 Nov 2020 12:52:02 +0100 Subject: [PATCH 5/8] Introduce common Ethereum signature JSON format We expect a signature in a common Ethereum signature JSON format: { "address": "
", "msg": "", "sig": "", "version": "2" } Added a possibility to output the signature to a file and read a file on verification. --- cmd/signing_ethereum.go | 149 ++++++++++++++++++++++++++++------------ 1 file changed, 106 insertions(+), 43 deletions(-) diff --git a/cmd/signing_ethereum.go b/cmd/signing_ethereum.go index bd2ad2f5d..2e55e378d 100644 --- a/cmd/signing_ethereum.go +++ b/cmd/signing_ethereum.go @@ -4,7 +4,9 @@ import ( "bytes" "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" + "io/ioutil" "os" "github.com/ethereum/go-ethereum/common" @@ -22,7 +24,7 @@ var EthereumSigningCommand = cli.Command{ Subcommands: []cli.Command{ { Name: "sign", - Usage: "Sign a message using operator's key", + Usage: "Sign a message using the operator's key", Description: ethereumSignDescription, Action: EthereumSign, ArgsUsage: "[message]", @@ -32,6 +34,10 @@ var EthereumSigningCommand = cli.Command{ Usage: "Path to the ethereum key file. " + "If not provided read the path from a config file.", }, + cli.StringFlag{ + Name: "output-file,o", + Usage: "Output file for the signature", + }, }, }, { @@ -39,26 +45,62 @@ var EthereumSigningCommand = cli.Command{ Usage: "Verifies a signature", Description: ethereumVerifyDescription, Action: EthereumVerify, - ArgsUsage: "[message] [address] [signature]", + ArgsUsage: "[ethereum-signature]", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "input-file,i", + Usage: "Input file with the signature", + }, + }, }, }, } -const ethereumSignDescription = "Calculates a signature for a given message. " + - "The message is expected to be provided as a string, it is later hashed with " + - "SHA-256 and passed to ECDSA signing. The calculated signature is returned " + - "in Ethereum specific format as a hexadecimal string representation of 65-byte " + - "{R, S, V} parameters.\n" + - "It reads an Ethereum key from an encrypted file. A path to the key file can be " + - "configured in a config file or specified directly with a `eth-key-file` flag. " + - "The key file is expected to be encrypted with a password provided as " + - config.PasswordEnvVariable + " environment variable." - -const ethereumVerifyDescription = "Verifies if a signature was calculated for a " + - "message by an ethereum account identified by an address. It expects a message " + - "to be provided as an original message string, that is later hashed with SHA-256. " + - "Signature should be provided in Ethereum specific format as a hexadecimal string " + - "representation of 65-byte {R, S, V} parameters." +const ethereumSignDescription = `Calculates an ethereum signature for a given message. +The message is expected to be provided as a string, it is later hashed with SHA-256 +and passed to Ethereum ECDSA signing. Signature is calculated in Ethereum specific +format as a hexadecimal string representation of 65-byte {R, S, V} parameters. + +It requires an Ethereum key to be provided in an encrypted file. A path to the key file +can be configured in a config file or specified directly with an 'eth-key-file' flag. + +The key file is expected to be encrypted with a password provided as ` + config.PasswordEnvVariable + ` +environment variable. + +The result is outputted in a common Ethereum signature format: +{ + "address": "
", + "msg": "", + "sig": "", + "version": "2" +} + +If 'output-file' flag is set the result will be stored in a specified file path. +` + +const ethereumVerifyDescription = `Verifies if a signature was calculated for a message +by an ethereum account identified by an address. + +It expects a signature to be provided in a common Ethereum signature format: +{ + "address": "
", + "msg": "", + "sig": "", + "version": "2" +} + +If 'input-file' flag is set the input will be read from a specified file path. +` + +// EthereumSignature is a common Ethereum signature format. +type EthereumSignature struct { + Address common.Address `json:"address"` + Message string `json:"msg"` + Signature string `json:"sig"` + Version uint `json:"version"` +} + +const ethSignatureVersion uint = 2 // EthereumSign signs a string using operator's ethereum key. func EthereumSign(c *cli.Context) error { @@ -98,62 +140,83 @@ func EthereumSign(c *cli.Context) error { return fmt.Errorf("failed to sign: [%v]", err) } - signatureHex := hex.EncodeToString(signature) + ethereumSignature := &EthereumSignature{ + Address: ethereumKey.Address, + Message: message, + Signature: hex.EncodeToString(signature), + Version: ethSignatureVersion, + } - fmt.Printf( - "signature calculated by [%s] for message [%s]: %s\n", - ethereumKey.Address.Hex(), message, signatureHex, - ) + marshaledSignature, err := json.Marshal(ethereumSignature) + if err != nil { + return fmt.Errorf("failed to marshal ethereum signature: [%v]", err) + } - return nil + return outputData(c, marshaledSignature) } // EthereumVerify verifies if a signature was calculated by a signer with the // given ethereum address. func EthereumVerify(c *cli.Context) error { - message := c.Args().First() - if len(message) == 0 { - return fmt.Errorf("invalid message") + var marshaledSignature []byte + if inputFilePath := c.String("input-file"); len(inputFilePath) > 0 { + fileContent, err := ioutil.ReadFile(inputFilePath) + if err != nil { + return fmt.Errorf("failed to read a file: [%v]", err) + } + marshaledSignature = fileContent + } else { + signatureArg := c.Args().First() + if len(signatureArg) == 0 { + return fmt.Errorf("missing argument") + } + + marshaledSignature = []byte(signatureArg) } - address := c.Args().Get(1) - if len(address) == 0 { - return fmt.Errorf("invalid address") + ethereumSignature := &EthereumSignature{} + err := json.Unmarshal(marshaledSignature, ethereumSignature) + if err != nil { + return fmt.Errorf("failed to unmarshal ethereum signature: [%v]", err) } - signature := c.Args().Get(2) - if len(signature) == 0 { - return fmt.Errorf("invalid signature") + if ethereumSignature.Version != ethSignatureVersion { + return fmt.Errorf( + "unsupported ethereum signature version\n"+ + "\texpected: %d\n"+ + "\tactual: %d", + ethSignatureVersion, + ethereumSignature.Version, + ) } - signatureBytes, err := hex.DecodeString(signature) + digest := sha256.Sum256([]byte(ethereumSignature.Message)) + + signatureBytes, err := hex.DecodeString(ethereumSignature.Signature) if err != nil { - return fmt.Errorf("could not decode signature string: [%v]", err) + return fmt.Errorf("failed to decode signature: [%v]", err) } - digest := sha256.Sum256([]byte(message)) - publicKey, err := crypto.SigToPub(digest[:], signatureBytes) if err != nil { return fmt.Errorf("could not recover public key from signature [%v]", err) } - expectedAddress := common.HexToAddress(address) recoveredAddress := crypto.PubkeyToAddress(*publicKey) - if !bytes.Equal(recoveredAddress.Bytes(), expectedAddress.Bytes()) { + if !bytes.Equal(recoveredAddress.Bytes(), ethereumSignature.Address.Bytes()) { return fmt.Errorf( - "invalid signer\n"+ - "\texpected signer: %s\n"+ - "\tactual signer: %s", - expectedAddress.Hex(), + "signature verification failed: invalid signer\n"+ + "\texpected: %s\n"+ + "\tactual: %s", + ethereumSignature.Address.Hex(), recoveredAddress.Hex(), ) } fmt.Printf( "signature verified correctly, message [%s] was signed by [%s]\n", - message, + ethereumSignature.Message, recoveredAddress.Hex(), ) From 6a692e34ae753876ac5ecce193c31399ba0795bd Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 19 Nov 2020 13:27:48 +0100 Subject: [PATCH 6/8] Clean file path on input read Cleaning the user provided path should solve gosec discovered issue. --- cmd/signing_ethereum.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/signing_ethereum.go b/cmd/signing_ethereum.go index 2e55e378d..6d4a0313e 100644 --- a/cmd/signing_ethereum.go +++ b/cmd/signing_ethereum.go @@ -8,6 +8,7 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -160,7 +161,7 @@ func EthereumSign(c *cli.Context) error { func EthereumVerify(c *cli.Context) error { var marshaledSignature []byte if inputFilePath := c.String("input-file"); len(inputFilePath) > 0 { - fileContent, err := ioutil.ReadFile(inputFilePath) + fileContent, err := ioutil.ReadFile(filepath.Clean(inputFilePath)) if err != nil { return fmt.Errorf("failed to read a file: [%v]", err) } From 3fb44f02715abe806d41eae6fa4417994f8665b3 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 19 Nov 2020 14:38:27 +0100 Subject: [PATCH 7/8] Overwrite file with signature If a file with signature already exists overwrite it. We don't want it to be default bahaviour so left check fo key shares files. --- cmd/signing.go | 24 ++++++++++++++++-------- cmd/signing_ethereum.go | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/cmd/signing.go b/cmd/signing.go index dcbc73a7c..87f20e7a2 100644 --- a/cmd/signing.go +++ b/cmd/signing.go @@ -112,7 +112,7 @@ func DecryptKeyShare(c *cli.Context) error { ) } - return outputData(c, signerBytes) + return outputData(c, signerBytes, false) } // SignDigest signs a given digest using key shares from the provided directory. @@ -262,16 +262,24 @@ func SignDigest(c *cli.Context) error { return nil } -func outputData(c *cli.Context, data []byte) error { +func outputData(c *cli.Context, data []byte, overwrite bool) error { if outputFilePath := c.String("output-file"); len(outputFilePath) > 0 { - if _, err := os.Stat(outputFilePath); !os.IsNotExist(err) { - return fmt.Errorf( - "could not write output to a file; file [%s] already exists", - outputFilePath, - ) + var fileMode os.FileMode + + if !overwrite { + if _, err := os.Stat(outputFilePath); !os.IsNotExist(err) { + return fmt.Errorf( + "could not write output to a file; file [%s] already exists", + outputFilePath, + ) + } + + fileMode = 0444 // read-only + } else { + fileMode = 0644 // user writable } - err := ioutil.WriteFile(outputFilePath, data, 0444) // read-only + err := ioutil.WriteFile(outputFilePath, data, fileMode) if err != nil { return fmt.Errorf( "failed to write output to a file [%s]: [%v]", diff --git a/cmd/signing_ethereum.go b/cmd/signing_ethereum.go index 6d4a0313e..026cf4e5f 100644 --- a/cmd/signing_ethereum.go +++ b/cmd/signing_ethereum.go @@ -153,7 +153,7 @@ func EthereumSign(c *cli.Context) error { return fmt.Errorf("failed to marshal ethereum signature: [%v]", err) } - return outputData(c, marshaledSignature) + return outputData(c, marshaledSignature, true) } // EthereumVerify verifies if a signature was calculated by a signer with the From 379fccceeb49e71726180b24fa5c4cb23e809e24 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Thu, 19 Nov 2020 15:31:20 +0100 Subject: [PATCH 8/8] Pass file mode to output data command We can expect file mode to be provided to outputData function instead of the boolen value. We don't need to check if the file exists before writing to it as access permissions of the file will prevent us from overwriting it if configured properly. --- cmd/signing.go | 23 ++++++----------------- cmd/signing_ethereum.go | 2 +- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/cmd/signing.go b/cmd/signing.go index 87f20e7a2..a336fbf4f 100644 --- a/cmd/signing.go +++ b/cmd/signing.go @@ -112,7 +112,7 @@ func DecryptKeyShare(c *cli.Context) error { ) } - return outputData(c, signerBytes, false) + return outputData(c, signerBytes, 0444) // store to read-only file } // SignDigest signs a given digest using key shares from the provided directory. @@ -262,23 +262,12 @@ func SignDigest(c *cli.Context) error { return nil } -func outputData(c *cli.Context, data []byte, overwrite bool) error { +// If `output-file` flag is provided stores the output in a file. +// `fileMode` determines the access permission for the output file. Sample values: +// 0444 - read-only for all +// 0644 - readable for all, but writeable only for the user (owner) +func outputData(c *cli.Context, data []byte, fileMode os.FileMode) error { if outputFilePath := c.String("output-file"); len(outputFilePath) > 0 { - var fileMode os.FileMode - - if !overwrite { - if _, err := os.Stat(outputFilePath); !os.IsNotExist(err) { - return fmt.Errorf( - "could not write output to a file; file [%s] already exists", - outputFilePath, - ) - } - - fileMode = 0444 // read-only - } else { - fileMode = 0644 // user writable - } - err := ioutil.WriteFile(outputFilePath, data, fileMode) if err != nil { return fmt.Errorf( diff --git a/cmd/signing_ethereum.go b/cmd/signing_ethereum.go index 026cf4e5f..2d1a22fc8 100644 --- a/cmd/signing_ethereum.go +++ b/cmd/signing_ethereum.go @@ -153,7 +153,7 @@ func EthereumSign(c *cli.Context) error { return fmt.Errorf("failed to marshal ethereum signature: [%v]", err) } - return outputData(c, marshaledSignature, true) + return outputData(c, marshaledSignature, 0644) // store to user writeable file } // EthereumVerify verifies if a signature was calculated by a signer with the