Skip to content

crypto: improve error messages in LoadECDSA #20718

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

Merged
merged 12 commits into from
Apr 8, 2020
31 changes: 31 additions & 0 deletions cmd/geth/accountcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,37 @@ Path of the secret key file: .*UTC--.+--[0-9a-f]{40}
`)
}

func TestAccountImport(t *testing.T) {
tests := []struct{ key, output string }{
{
key: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
output: "Address: {fcad0b19bb29d4674531d6f115237e16afce377c}\n",
},
{
key: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef1",
output: "Fatal: Failed to load the private key: invalid character '1' at end of key file\n",
},
}
for _, test := range tests {
importAccountWithExpect(t, test.key, test.output)
}
}

func importAccountWithExpect(t *testing.T, key string, expected string) {
dir := tmpdir(t)
keyfile := filepath.Join(dir, "key.prv")
if err := ioutil.WriteFile(keyfile, []byte(key), 0600); err != nil {
t.Error(err)
}
passwordFile := filepath.Join(dir, "password.txt")
if err := ioutil.WriteFile(passwordFile, []byte("foobar"), 0600); err != nil {
t.Error(err)
}
geth := runGeth(t, "account", "import", keyfile, "-password", passwordFile)
defer geth.ExpectExit()
geth.Expect(expected)
}

func TestAccountNewBadRepeat(t *testing.T) {
geth := runGeth(t, "account", "new", "--lightkdf")
defer geth.ExpectExit()
Expand Down
56 changes: 48 additions & 8 deletions crypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package crypto

import (
"bufio"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
Expand Down Expand Up @@ -158,29 +159,67 @@ func FromECDSAPub(pub *ecdsa.PublicKey) []byte {
// HexToECDSA parses a secp256k1 private key.
func HexToECDSA(hexkey string) (*ecdsa.PrivateKey, error) {
b, err := hex.DecodeString(hexkey)
if err != nil {
return nil, errors.New("invalid hex string")
if byteErr, ok := err.(hex.InvalidByteError); ok {
return nil, fmt.Errorf("invalid hex character %q in private key", byte(byteErr))
} else if err != nil {
return nil, errors.New("invalid hex data for private key")
}
return ToECDSA(b)
}

// LoadECDSA loads a secp256k1 private key from the given file.
func LoadECDSA(file string) (*ecdsa.PrivateKey, error) {
buf := make([]byte, 64)
fd, err := os.Open(file)
if err != nil {
return nil, err
}
defer fd.Close()
if _, err := io.ReadFull(fd, buf); err != nil {
return nil, err
}

key, err := hex.DecodeString(string(buf))
r := bufio.NewReader(fd)
buf := make([]byte, 64)
n, err := readASCII(buf, r)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of reading any ascii char, why not readHexDigits() that would stop when it encounters a non-hexdigit? Then we could give the exact location of the first wrong char and we don't have to complicate HexToECDSA().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change in HexToECDSA is good to have on its own. It didn't report the offending character before, now it does.

if err != nil {
return nil, err
} else if n != len(buf) {
return nil, fmt.Errorf("key file too short, want 64 hex characters")
}
if err := checkKeyFileEnd(r); err != nil {
return nil, err
}

return HexToECDSA(string(buf))
}

// readASCII reads into 'buf', stopping when the buffer is full or
// when a non-printable control character is encountered.
func readASCII(buf []byte, r *bufio.Reader) (n int, err error) {
for ; n < len(buf); n++ {
buf[n], err = r.ReadByte()
switch {
case err == io.EOF || buf[n] < '!':
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't there a less hacky way to check a char, along the lines of Python's buf[n] in string.printable?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no better way to do this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could use unicode.IsPrint(rune r) https://golang.org/pkg/unicode/#IsPrint

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could, but don't want to import "unicode" just for checking if something is ASCII.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats fair

return n, nil
case err != nil:
return n, err
}
}
return n, nil
}

// checkKeyFileEnd skips over additional newlines at the end of a key file.
func checkKeyFileEnd(r *bufio.Reader) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if the distinction between invalid character at the end of key file and key file to long is important enough for such a complicated function

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this because Adam's original added support for trailing newlines. These can happen if you paste the key into a file using a text editor. So we want to allow newlines, but not too many of them. That's why this function is so complicated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah okay, then the PR looks good to me

for i := 0; ; i++ {
b, err := r.ReadByte()
switch {
case err == io.EOF:
return nil
case err != nil:
return err
case b != '\n' && b != '\r':
return fmt.Errorf("invalid character %q at end of key file", b)
case i >= 2:
return errors.New("key file too long, want 64 hex characters")
}
}
return ToECDSA(key)
}

// SaveECDSA saves a secp256k1 private key to the given file with
Expand All @@ -190,6 +229,7 @@ func SaveECDSA(file string, key *ecdsa.PrivateKey) error {
return ioutil.WriteFile(file, []byte(k), 0600)
}

// GenerateKey generates a new private key.
func GenerateKey() (*ecdsa.PrivateKey, error) {
return ecdsa.GenerateKey(S256(), rand.Reader)
}
Expand Down
85 changes: 64 additions & 21 deletions crypto/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,39 +139,82 @@ func TestNewContractAddress(t *testing.T) {
checkAddr(t, common.HexToAddress("c9ddedf451bc62ce88bf9292afb13df35b670699"), caddr2)
}

func TestLoadECDSAFile(t *testing.T) {
keyBytes := common.FromHex(testPrivHex)
fileName0 := "test_key0"
fileName1 := "test_key1"
checkKey := func(k *ecdsa.PrivateKey) {
checkAddr(t, PubkeyToAddress(k.PublicKey), common.HexToAddress(testAddrHex))
loadedKeyBytes := FromECDSA(k)
if !bytes.Equal(loadedKeyBytes, keyBytes) {
t.Fatalf("private key mismatch: want: %x have: %x", keyBytes, loadedKeyBytes)
}
func TestLoadECDSA(t *testing.T) {
tests := []struct {
input string
err string
}{
// good
{input: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"},
{input: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n"},
{input: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n\r"},
{input: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\r\n"},
{input: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n\n"},
{input: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n\r"},
// bad
{
input: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde",
err: "key file too short, want 64 hex characters",
},
{
input: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\n",
err: "key file too short, want 64 hex characters",
},
{
input: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdeX",
err: "invalid hex character 'X' in private key",
},
{
input: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefX",
err: "invalid character 'X' at end of key file",
},
{
input: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n\n\n",
err: "key file too long, want 64 hex characters",
},
}

ioutil.WriteFile(fileName0, []byte(testPrivHex), 0600)
defer os.Remove(fileName0)
for _, test := range tests {
f, err := ioutil.TempFile("", "loadecdsa_test.*.txt")
if err != nil {
t.Fatal(err)
}
filename := f.Name()
f.WriteString(test.input)
f.Close()

_, err = LoadECDSA(filename)
switch {
case err != nil && test.err == "":
t.Fatalf("unexpected error for input %q:\n %v", test.input, err)
case err != nil && err.Error() != test.err:
t.Fatalf("wrong error for input %q:\n %v", test.input, err)
case err == nil && test.err != "":
t.Fatalf("LoadECDSA did not return error for input %q", test.input)
}
}
}

key0, err := LoadECDSA(fileName0)
func TestSaveECDSA(t *testing.T) {
f, err := ioutil.TempFile("", "saveecdsa_test.*.txt")
if err != nil {
t.Fatal(err)
}
checkKey(key0)
file := f.Name()
f.Close()
defer os.Remove(file)

// again, this time with SaveECDSA instead of manual save:
err = SaveECDSA(fileName1, key0)
if err != nil {
key, _ := HexToECDSA(testPrivHex)
if err := SaveECDSA(file, key); err != nil {
t.Fatal(err)
}
defer os.Remove(fileName1)

key1, err := LoadECDSA(fileName1)
loaded, err := LoadECDSA(file)
if err != nil {
t.Fatal(err)
}
checkKey(key1)
if !reflect.DeepEqual(key, loaded) {
t.Fatal("loaded key not equal to saved key")
}
}

func TestValidateSignatureValues(t *testing.T) {
Expand Down