Skip to content
This repository has been archived by the owner on May 22, 2023. It is now read-only.

Commit

Permalink
Merge pull request #613 from keep-network/operator-signing
Browse files Browse the repository at this point in the history
CLI command to sign and verify operator's signature
  • Loading branch information
dimpar authored Nov 19, 2020
2 parents 548f89c + a28bbaa commit d7e6cd0
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 29 deletions.
59 changes: 32 additions & 27 deletions cmd/signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -57,6 +58,7 @@ func init() {
Action: SignDigest,
ArgsUsage: "[unprefixed-hex-digest] [key-shares-dir]",
},
EthereumSigningCommand,
},
}
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
225 changes: 225 additions & 0 deletions cmd/signing_ethereum.go
Original file line number Diff line number Diff line change
@@ -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": "<address>",
"msg": "<content>",
"sig": "<signature>",
"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": "<address>",
"msg": "<content>",
"sig": "<signature>",
"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
}
5 changes: 3 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down

0 comments on commit d7e6cd0

Please sign in to comment.