diff --git a/cmd/signing.go b/cmd/signing.go
index a02260e61..a336fbf4f 100644
--- a/cmd/signing.go
+++ b/cmd/signing.go
@@ -19,6 +19,7 @@ import (
eth "github.com/keep-network/keep-ecdsa/pkg/chain"
"github.com/ethereum/go-ethereum/common"
+
"github.com/keep-network/keep-common/pkg/persistence"
"github.com/keep-network/keep-ecdsa/internal/config"
"github.com/keep-network/keep-ecdsa/pkg/registry"
@@ -57,6 +58,7 @@ func init() {
Action: SignDigest,
ArgsUsage: "[unprefixed-hex-digest] [key-shares-dir]",
},
+ EthereumSigningCommand,
},
}
}
@@ -110,33 +112,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, 0444) // store to read-only file
}
// SignDigest signs a given digest using key shares from the provided directory.
@@ -285,3 +261,32 @@ func SignDigest(c *cli.Context) error {
return nil
}
+
+// 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 {
+ err := ioutil.WriteFile(outputFilePath, data, fileMode)
+ 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
+}
diff --git a/cmd/signing_ethereum.go b/cmd/signing_ethereum.go
new file mode 100644
index 000000000..2d1a22fc8
--- /dev/null
+++ b/cmd/signing_ethereum.go
@@ -0,0 +1,225 @@
+package cmd
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "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 the 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.",
+ },
+ cli.StringFlag{
+ Name: "output-file,o",
+ Usage: "Output file for the signature",
+ },
+ },
+ },
+ {
+ Name: "verify",
+ Usage: "Verifies a signature",
+ Description: ethereumVerifyDescription,
+ Action: EthereumVerify,
+ ArgsUsage: "[ethereum-signature]",
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "input-file,i",
+ Usage: "Input file with the signature",
+ },
+ },
+ },
+ },
+}
+
+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 {
+ message := c.Args().First()
+ if len(message) == 0 {
+ return fmt.Errorf("invalid digest")
+ }
+
+ var ethKeyFilePath, ethKeyPassword string
+ // 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)
+ }
+
+ ethKeyFilePath = 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)
+ }
+
+ ethereumSignature := &EthereumSignature{
+ Address: ethereumKey.Address,
+ Message: message,
+ Signature: hex.EncodeToString(signature),
+ Version: ethSignatureVersion,
+ }
+
+ marshaledSignature, err := json.Marshal(ethereumSignature)
+ if err != nil {
+ return fmt.Errorf("failed to marshal ethereum signature: [%v]", err)
+ }
+
+ return outputData(c, marshaledSignature, 0644) // store to user writeable file
+}
+
+// EthereumVerify verifies if a signature was calculated by a signer with the
+// given ethereum address.
+func EthereumVerify(c *cli.Context) error {
+ var marshaledSignature []byte
+ if inputFilePath := c.String("input-file"); len(inputFilePath) > 0 {
+ fileContent, err := ioutil.ReadFile(filepath.Clean(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)
+ }
+
+ ethereumSignature := &EthereumSignature{}
+ err := json.Unmarshal(marshaledSignature, ethereumSignature)
+ if err != nil {
+ return fmt.Errorf("failed to unmarshal ethereum signature: [%v]", err)
+ }
+
+ if ethereumSignature.Version != ethSignatureVersion {
+ return fmt.Errorf(
+ "unsupported ethereum signature version\n"+
+ "\texpected: %d\n"+
+ "\tactual: %d",
+ ethSignatureVersion,
+ ethereumSignature.Version,
+ )
+ }
+
+ digest := sha256.Sum256([]byte(ethereumSignature.Message))
+
+ signatureBytes, err := hex.DecodeString(ethereumSignature.Signature)
+ if err != nil {
+ return fmt.Errorf("failed to decode signature: [%v]", err)
+ }
+
+ publicKey, err := crypto.SigToPub(digest[:], signatureBytes)
+ if err != nil {
+ return fmt.Errorf("could not recover public key from signature [%v]", err)
+ }
+
+ recoveredAddress := crypto.PubkeyToAddress(*publicKey)
+
+ if !bytes.Equal(recoveredAddress.Bytes(), ethereumSignature.Address.Bytes()) {
+ return fmt.Errorf(
+ "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",
+ ethereumSignature.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
}