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

Commit 58bb77b

Browse files
Merge branch 'master' into signature-publication-delays
2 parents d3afde8 + d31ce2e commit 58bb77b

4 files changed

Lines changed: 291 additions & 66 deletions

File tree

cmd/signing.go

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
eth "github.com/keep-network/keep-ecdsa/pkg/chain"
2020

2121
"github.com/ethereum/go-ethereum/common"
22+
2223
"github.com/keep-network/keep-common/pkg/persistence"
2324
"github.com/keep-network/keep-ecdsa/internal/config"
2425
"github.com/keep-network/keep-ecdsa/pkg/registry"
@@ -57,6 +58,7 @@ func init() {
5758
Action: SignDigest,
5859
ArgsUsage: "[unprefixed-hex-digest] [key-shares-dir]",
5960
},
61+
EthereumSigningCommand,
6062
},
6163
}
6264
}
@@ -110,33 +112,7 @@ func DecryptKeyShare(c *cli.Context) error {
110112
)
111113
}
112114

113-
if outputFilePath := c.String("output-file"); len(outputFilePath) > 0 {
114-
if _, err := os.Stat(outputFilePath); !os.IsNotExist(err) {
115-
return fmt.Errorf(
116-
"could not write shares to file; file [%s] already exists",
117-
outputFilePath,
118-
)
119-
}
120-
121-
err = ioutil.WriteFile(outputFilePath, signerBytes, 0444) // read-only
122-
if err != nil {
123-
return fmt.Errorf(
124-
"failed to write to file [%s]: [%v]",
125-
outputFilePath,
126-
err,
127-
)
128-
}
129-
} else {
130-
_, err = os.Stdout.Write(signerBytes)
131-
if err != nil {
132-
return fmt.Errorf(
133-
"could not write signer bytes to stdout: [%v]",
134-
err,
135-
)
136-
}
137-
}
138-
139-
return nil
115+
return outputData(c, signerBytes, 0444) // store to read-only file
140116
}
141117

142118
// SignDigest signs a given digest using key shares from the provided directory.
@@ -285,3 +261,32 @@ func SignDigest(c *cli.Context) error {
285261

286262
return nil
287263
}
264+
265+
// If `output-file` flag is provided stores the output in a file.
266+
// `fileMode` determines the access permission for the output file. Sample values:
267+
// 0444 - read-only for all
268+
// 0644 - readable for all, but writeable only for the user (owner)
269+
func outputData(c *cli.Context, data []byte, fileMode os.FileMode) error {
270+
if outputFilePath := c.String("output-file"); len(outputFilePath) > 0 {
271+
err := ioutil.WriteFile(outputFilePath, data, fileMode)
272+
if err != nil {
273+
return fmt.Errorf(
274+
"failed to write output to a file [%s]: [%v]",
275+
outputFilePath,
276+
err,
277+
)
278+
}
279+
280+
fmt.Printf("output stored to a file: %s", outputFilePath)
281+
} else {
282+
_, err := os.Stdout.Write(data)
283+
if err != nil {
284+
return fmt.Errorf(
285+
"could not write bytes to stdout: [%v]",
286+
err,
287+
)
288+
}
289+
}
290+
291+
return nil
292+
}

cmd/signing_ethereum.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"encoding/json"
8+
"fmt"
9+
"io/ioutil"
10+
"os"
11+
"path/filepath"
12+
13+
"github.com/ethereum/go-ethereum/common"
14+
"github.com/ethereum/go-ethereum/crypto"
15+
"github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil"
16+
"github.com/keep-network/keep-ecdsa/internal/config"
17+
"github.com/urfave/cli"
18+
)
19+
20+
// EthereumSigningCommand contains the definition of the `signing ethereum`
21+
// command-line subcommand and its own subcommands.
22+
var EthereumSigningCommand = cli.Command{
23+
Name: "ethereum",
24+
Usage: "Ethereum signatures calculation",
25+
Subcommands: []cli.Command{
26+
{
27+
Name: "sign",
28+
Usage: "Sign a message using the operator's key",
29+
Description: ethereumSignDescription,
30+
Action: EthereumSign,
31+
ArgsUsage: "[message]",
32+
Flags: []cli.Flag{
33+
cli.StringFlag{
34+
Name: "eth-key-file,k",
35+
Usage: "Path to the ethereum key file. " +
36+
"If not provided read the path from a config file.",
37+
},
38+
cli.StringFlag{
39+
Name: "output-file,o",
40+
Usage: "Output file for the signature",
41+
},
42+
},
43+
},
44+
{
45+
Name: "verify",
46+
Usage: "Verifies a signature",
47+
Description: ethereumVerifyDescription,
48+
Action: EthereumVerify,
49+
ArgsUsage: "[ethereum-signature]",
50+
Flags: []cli.Flag{
51+
cli.StringFlag{
52+
Name: "input-file,i",
53+
Usage: "Input file with the signature",
54+
},
55+
},
56+
},
57+
},
58+
}
59+
60+
const ethereumSignDescription = `Calculates an ethereum signature for a given message.
61+
The message is expected to be provided as a string, it is later hashed with SHA-256
62+
and passed to Ethereum ECDSA signing. Signature is calculated in Ethereum specific
63+
format as a hexadecimal string representation of 65-byte {R, S, V} parameters.
64+
65+
It requires an Ethereum key to be provided in an encrypted file. A path to the key file
66+
can be configured in a config file or specified directly with an 'eth-key-file' flag.
67+
68+
The key file is expected to be encrypted with a password provided as ` + config.PasswordEnvVariable + `
69+
environment variable.
70+
71+
The result is outputted in a common Ethereum signature format:
72+
{
73+
"address": "<address>",
74+
"msg": "<content>",
75+
"sig": "<signature>",
76+
"version": "2"
77+
}
78+
79+
If 'output-file' flag is set the result will be stored in a specified file path.
80+
`
81+
82+
const ethereumVerifyDescription = `Verifies if a signature was calculated for a message
83+
by an ethereum account identified by an address.
84+
85+
It expects a signature to be provided in a common Ethereum signature format:
86+
{
87+
"address": "<address>",
88+
"msg": "<content>",
89+
"sig": "<signature>",
90+
"version": "2"
91+
}
92+
93+
If 'input-file' flag is set the input will be read from a specified file path.
94+
`
95+
96+
// EthereumSignature is a common Ethereum signature format.
97+
type EthereumSignature struct {
98+
Address common.Address `json:"address"`
99+
Message string `json:"msg"`
100+
Signature string `json:"sig"`
101+
Version uint `json:"version"`
102+
}
103+
104+
const ethSignatureVersion uint = 2
105+
106+
// EthereumSign signs a string using operator's ethereum key.
107+
func EthereumSign(c *cli.Context) error {
108+
message := c.Args().First()
109+
if len(message) == 0 {
110+
return fmt.Errorf("invalid digest")
111+
}
112+
113+
var ethKeyFilePath, ethKeyPassword string
114+
// Check if `eth-key-file` flag was set. If not read the key file path from
115+
// a config file.
116+
if ethKeyFilePath = c.String("eth-key-file"); len(ethKeyFilePath) > 0 {
117+
ethKeyPassword = os.Getenv(config.PasswordEnvVariable)
118+
} else {
119+
ethereumConfig, err := config.ReadEthereumConfig(c.GlobalString("config"))
120+
if err != nil {
121+
return fmt.Errorf("failed while reading config file: [%v]", err)
122+
}
123+
124+
ethKeyFilePath = ethereumConfig.Account.KeyFile
125+
ethKeyPassword = ethereumConfig.Account.KeyFilePassword
126+
}
127+
128+
ethereumKey, err := ethutil.DecryptKeyFile(ethKeyFilePath, ethKeyPassword)
129+
if err != nil {
130+
return fmt.Errorf(
131+
"failed to read key file [%s]: [%v]",
132+
ethKeyFilePath,
133+
err,
134+
)
135+
}
136+
137+
digest := sha256.Sum256([]byte(message))
138+
139+
signature, err := crypto.Sign(digest[:], ethereumKey.PrivateKey)
140+
if err != nil {
141+
return fmt.Errorf("failed to sign: [%v]", err)
142+
}
143+
144+
ethereumSignature := &EthereumSignature{
145+
Address: ethereumKey.Address,
146+
Message: message,
147+
Signature: hex.EncodeToString(signature),
148+
Version: ethSignatureVersion,
149+
}
150+
151+
marshaledSignature, err := json.Marshal(ethereumSignature)
152+
if err != nil {
153+
return fmt.Errorf("failed to marshal ethereum signature: [%v]", err)
154+
}
155+
156+
return outputData(c, marshaledSignature, 0644) // store to user writeable file
157+
}
158+
159+
// EthereumVerify verifies if a signature was calculated by a signer with the
160+
// given ethereum address.
161+
func EthereumVerify(c *cli.Context) error {
162+
var marshaledSignature []byte
163+
if inputFilePath := c.String("input-file"); len(inputFilePath) > 0 {
164+
fileContent, err := ioutil.ReadFile(filepath.Clean(inputFilePath))
165+
if err != nil {
166+
return fmt.Errorf("failed to read a file: [%v]", err)
167+
}
168+
marshaledSignature = fileContent
169+
} else {
170+
signatureArg := c.Args().First()
171+
if len(signatureArg) == 0 {
172+
return fmt.Errorf("missing argument")
173+
}
174+
175+
marshaledSignature = []byte(signatureArg)
176+
}
177+
178+
ethereumSignature := &EthereumSignature{}
179+
err := json.Unmarshal(marshaledSignature, ethereumSignature)
180+
if err != nil {
181+
return fmt.Errorf("failed to unmarshal ethereum signature: [%v]", err)
182+
}
183+
184+
if ethereumSignature.Version != ethSignatureVersion {
185+
return fmt.Errorf(
186+
"unsupported ethereum signature version\n"+
187+
"\texpected: %d\n"+
188+
"\tactual: %d",
189+
ethSignatureVersion,
190+
ethereumSignature.Version,
191+
)
192+
}
193+
194+
digest := sha256.Sum256([]byte(ethereumSignature.Message))
195+
196+
signatureBytes, err := hex.DecodeString(ethereumSignature.Signature)
197+
if err != nil {
198+
return fmt.Errorf("failed to decode signature: [%v]", err)
199+
}
200+
201+
publicKey, err := crypto.SigToPub(digest[:], signatureBytes)
202+
if err != nil {
203+
return fmt.Errorf("could not recover public key from signature [%v]", err)
204+
}
205+
206+
recoveredAddress := crypto.PubkeyToAddress(*publicKey)
207+
208+
if !bytes.Equal(recoveredAddress.Bytes(), ethereumSignature.Address.Bytes()) {
209+
return fmt.Errorf(
210+
"signature verification failed: invalid signer\n"+
211+
"\texpected: %s\n"+
212+
"\tactual: %s",
213+
ethereumSignature.Address.Hex(),
214+
recoveredAddress.Hex(),
215+
)
216+
}
217+
218+
fmt.Printf(
219+
"signature verified correctly, message [%s] was signed by [%s]\n",
220+
ethereumSignature.Message,
221+
recoveredAddress.Hex(),
222+
)
223+
224+
return nil
225+
}

internal/config/config.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import (
1212
"github.com/keep-network/keep-ecdsa/pkg/ecdsa/tss"
1313
)
1414

15+
// PasswordEnvVariable environment variable name for ethereum key password.
1516
// #nosec G101 (look for hardcoded credentials)
1617
// This line doesn't contain any credentials.
1718
// It's just the name of the environment variable.
18-
const passwordEnvVariable = "KEEP_ETHEREUM_PASSWORD"
19+
const PasswordEnvVariable = "KEEP_ETHEREUM_PASSWORD"
1920

2021
// Config is the top level config structure.
2122
type Config struct {
@@ -90,7 +91,7 @@ func ReadConfig(filePath string) (*Config, error) {
9091
return nil, fmt.Errorf("failed to decode file [%s]: [%v]", filePath, err)
9192
}
9293

93-
config.Ethereum.Account.KeyFilePassword = os.Getenv(passwordEnvVariable)
94+
config.Ethereum.Account.KeyFilePassword = os.Getenv(PasswordEnvVariable)
9495

9596
return config, nil
9697
}

0 commit comments

Comments
 (0)