diff --git a/dcrec/secp256k1/error.go b/dcrec/secp256k1/error.go new file mode 100644 index 00000000..30f4f124 --- /dev/null +++ b/dcrec/secp256k1/error.go @@ -0,0 +1,126 @@ +// Copyright (c) 2020 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package secp256k1 + +import ( + "fmt" +) + +// ErrorCode identifies a kind of pubkey-related error. It has full support for +// errors.Is and errors.As, so the caller can directly check against an error +// code when determining the reason for an error. +type ErrorCode int + +// These constants are used to identify a specific RuleError. +const ( + // ErrPubKeyInvalidLen indicates that the length of a serialized public + // key is not one of the allowed lengths. + ErrPubKeyInvalidLen ErrorCode = iota + + // ErrPubKeyInvalidFormat indicates an attempt was made to parse a public + // key that does not specify one of the supported formats. + ErrPubKeyInvalidFormat + + // ErrPubKeyXTooBig indicates that the x coordinate for a public key + // is greater than or equal to the prime of the field underlying the group. + ErrPubKeyXTooBig + + // ErrPubKeyYTooBig indicates that the y coordinate for a public key is + // greater than or equal to the prime of the field underlying the group. + ErrPubKeyYTooBig + + // ErrPubKeyNotOnCurve indicates that a public key is not a point on the + // secp256k1 curve. + ErrPubKeyNotOnCurve + + // ErrPubKeyMismatchedOddness indicates that a hybrid public key specified + // an oddness of the y coordinate that does not match the actual oddness of + // the provided y coordinate. + ErrPubKeyMismatchedOddness + + // numErrorCodes is the maximum error code number used in tests. This entry + // MUST be the last entry in the enum. + numErrorCodes +) + +// Map of ErrorCode values back to their constant names for pretty printing. +var errorCodeStrings = map[ErrorCode]string{ + ErrPubKeyInvalidLen: "ErrPubKeyInvalidLen", + ErrPubKeyXTooBig: "ErrPubKeyXTooBig", + ErrPubKeyYTooBig: "ErrPubKeyYTooBig", + ErrPubKeyNotOnCurve: "ErrPubKeyNotOnCurve", + ErrPubKeyMismatchedOddness: "ErrPubKeyMismatchedOddness", + ErrPubKeyInvalidFormat: "ErrPubKeyInvalidFormat", +} + +// String returns the ErrorCode as a human-readable name. +func (e ErrorCode) String() string { + if s := errorCodeStrings[e]; s != "" { + return s + } + return fmt.Sprintf("Unknown ErrorCode (%d)", int(e)) +} + +// Error implements the error interface. +func (e ErrorCode) Error() string { + return e.String() +} + +// Is implements the interface to work with the standard library's errors.Is. +// +// It returns true in the following cases: +// - The target is a Error and the error codes match +// - The target is a ErrorCode and the error codes match +func (e ErrorCode) Is(target error) bool { + switch target := target.(type) { + case Error: + return e == target.ErrorCode + + case ErrorCode: + return e == target + } + + return false +} + +// Error identifies a pubkey-related error. It has full support for errors.Is +// and errors.As, so the caller can ascertain the specific reason for the error +// by checking the underlying error code. +type Error struct { + ErrorCode ErrorCode // Describes the kind of error + Description string // Human readable description of the issue +} + +// Error satisfies the error interface and prints human-readable errors. +func (e Error) Error() string { + return e.Description +} + +// Is implements the interface to work with the standard library's errors.Is. +// +// It returns true in the following cases: +// - The target is a Error and the error codes match +// - The target is a ErrorCode and it the error codes match +func (e Error) Is(target error) bool { + switch target := target.(type) { + case Error: + return e.ErrorCode == target.ErrorCode + + case ErrorCode: + return target == e.ErrorCode + } + + return false +} + +// Unwrap returns the underlying wrapped error code. +func (e Error) Unwrap() error { + return e.ErrorCode +} + +// makeError creates a Error given a set of arguments. +func makeError(c ErrorCode, desc string) Error { + return Error{ErrorCode: c, Description: desc} +} diff --git a/dcrec/secp256k1/error_test.go b/dcrec/secp256k1/error_test.go new file mode 100644 index 00000000..abf62633 --- /dev/null +++ b/dcrec/secp256k1/error_test.go @@ -0,0 +1,145 @@ +// Copyright (c) 2020 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package secp256k1 + +import ( + "errors" + "testing" +) + +// TestErrorCodeStringer tests the stringized output for the ErrorCode type. +func TestErrorCodeStringer(t *testing.T) { + tests := []struct { + in ErrorCode + want string + }{ + {ErrPubKeyInvalidLen, "ErrPubKeyInvalidLen"}, + {ErrPubKeyInvalidFormat, "ErrPubKeyInvalidFormat"}, + {ErrPubKeyXTooBig, "ErrPubKeyXTooBig"}, + {ErrPubKeyYTooBig, "ErrPubKeyYTooBig"}, + {ErrPubKeyNotOnCurve, "ErrPubKeyNotOnCurve"}, + {ErrPubKeyMismatchedOddness, "ErrPubKeyMismatchedOddness"}, + {0xffff, "Unknown ErrorCode (65535)"}, + } + + // Detect additional error codes that don't have the stringer added. + if len(tests)-1 != int(numErrorCodes) { + t.Fatalf("It appears an error code was added without adding an " + + "associated stringer test") + } + + for i, test := range tests { + result := test.in.String() + if result != test.want { + t.Errorf("#%d: got: %s want: %s", i, result, test.want) + continue + } + } +} + +// TestError tests the error output for the Error type. +func TestError(t *testing.T) { + tests := []struct { + in Error + want string + }{{ + Error{Description: "some error"}, + "some error", + }, { + Error{Description: "human-readable error"}, + "human-readable error", + }} + + for i, test := range tests { + result := test.in.Error() + if result != test.want { + t.Errorf("#%d: got: %s want: %s", i, result, test.want) + continue + } + } +} + +// TestErrorCodeIsAs ensures both ErrorCode and Error can be identified as being +// a specific error code via errors.Is and unwrapped via errors.As. +func TestErrorCodeIsAs(t *testing.T) { + tests := []struct { + name string + err error + target error + wantMatch bool + wantAs ErrorCode + }{{ + name: "ErrPubKeyInvalidLen == ErrPubKeyInvalidLen", + err: ErrPubKeyInvalidLen, + target: ErrPubKeyInvalidLen, + wantMatch: true, + wantAs: ErrPubKeyInvalidLen, + }, { + name: "Error.ErrPubKeyInvalidLen == ErrPubKeyInvalidLen", + err: makeError(ErrPubKeyInvalidLen, ""), + target: ErrPubKeyInvalidLen, + wantMatch: true, + wantAs: ErrPubKeyInvalidLen, + }, { + name: "ErrPubKeyInvalidLen == Error.ErrPubKeyInvalidLen", + err: ErrPubKeyInvalidLen, + target: makeError(ErrPubKeyInvalidLen, ""), + wantMatch: true, + wantAs: ErrPubKeyInvalidLen, + }, { + name: "Error.ErrPubKeyInvalidLen == Error.ErrPubKeyInvalidLen", + err: makeError(ErrPubKeyInvalidLen, ""), + target: makeError(ErrPubKeyInvalidLen, ""), + wantMatch: true, + wantAs: ErrPubKeyInvalidLen, + }, { + name: "ErrPubKeyInvalidFormat != ErrPubKeyInvalidLen", + err: ErrPubKeyInvalidFormat, + target: ErrPubKeyInvalidLen, + wantMatch: false, + wantAs: ErrPubKeyInvalidFormat, + }, { + name: "Error.ErrPubKeyInvalidFormat != ErrPubKeyInvalidLen", + err: makeError(ErrPubKeyInvalidFormat, ""), + target: ErrPubKeyInvalidLen, + wantMatch: false, + wantAs: ErrPubKeyInvalidFormat, + }, { + name: "ErrPubKeyInvalidFormat != Error.ErrPubKeyInvalidLen", + err: ErrPubKeyInvalidFormat, + target: makeError(ErrPubKeyInvalidLen, ""), + wantMatch: false, + wantAs: ErrPubKeyInvalidFormat, + }, { + name: "Error.ErrPubKeyInvalidFormat != Error.ErrPubKeyInvalidLen", + err: makeError(ErrPubKeyInvalidFormat, ""), + target: makeError(ErrPubKeyInvalidLen, ""), + wantMatch: false, + wantAs: ErrPubKeyInvalidFormat, + }} + + for _, test := range tests { + // Ensure the error matches or not depending on the expected result. + result := errors.Is(test.err, test.target) + if result != test.wantMatch { + t.Errorf("%s: incorrect error identification -- got %v, want %v", + test.name, result, test.wantMatch) + continue + } + + // Ensure the underlying error code can be unwrapped and is the expected + // code. + var code ErrorCode + if !errors.As(test.err, &code) { + t.Errorf("%s: unable to unwrap to error code", test.name) + continue + } + if code != test.wantAs { + t.Errorf("%s: unexpected unwrapped error code -- got %v, want %v", + test.name, code, test.wantAs) + continue + } + } +} diff --git a/dcrec/secp256k1/pubkey.go b/dcrec/secp256k1/pubkey.go index 5af75fcd..e5979f65 100644 --- a/dcrec/secp256k1/pubkey.go +++ b/dcrec/secp256k1/pubkey.go @@ -6,7 +6,6 @@ package secp256k1 import ( - "errors" "fmt" "math/big" ) @@ -53,10 +52,13 @@ func isOdd(a *big.Int) bool { func decompressPoint(x *big.Int, ybit bool) (*big.Int, error) { var fx, fy FieldVal if overflow := fx.SetByteSlice(x.Bytes()); overflow { - return nil, fmt.Errorf("invalid public key x coordinate") + str := "invalid public key: x >= field prime" + return nil, makeError(ErrPubKeyXTooBig, str) } if !DecompressY(&fx, ybit, &fy) { - return nil, fmt.Errorf("invalid public key x coordinate") + str := fmt.Sprintf("invalid public key: x coordinate %v is not on the "+ + "secp256k1 curve", fx) + return nil, makeError(ErrPubKeyNotOnCurve, str) } fy.Normalize() @@ -89,7 +91,8 @@ func ParsePubKey(pubKeyStr []byte) (key *PublicKey, err error) { pubkey := PublicKey{} if len(pubKeyStr) == 0 { - return nil, errors.New("pubkey string is empty") + str := "invalid public key: empty" + return nil, makeError(ErrPubKeyInvalidLen, str) } format := pubKeyStr[0] @@ -99,15 +102,18 @@ func ParsePubKey(pubKeyStr []byte) (key *PublicKey, err error) { switch len(pubKeyStr) { case PubKeyBytesLenUncompressed: if format != pubkeyUncompressed && format != pubkeyHybrid { - return nil, fmt.Errorf("invalid magic in pubkey str: "+ - "%d", pubKeyStr[0]) + str := fmt.Sprintf("invalid public key: unsupported format: %x", + format) + return nil, makeError(ErrPubKeyInvalidFormat, str) } pubkey.x = new(big.Int).SetBytes(pubKeyStr[1:33]) pubkey.y = new(big.Int).SetBytes(pubKeyStr[33:]) // hybrid keys have extra information, make use of it. if format == pubkeyHybrid && ybit != isOdd(pubkey.y) { - return nil, fmt.Errorf("ybit doesn't match oddness") + str := fmt.Sprintf("invalid public key: y oddness does not match "+ + "specified value of %v", ybit) + return nil, makeError(ErrPubKeyMismatchedOddness, str) } case PubKeyBytesLenCompressed: @@ -115,8 +121,9 @@ func ParsePubKey(pubKeyStr []byte) (key *PublicKey, err error) { // solution determines which solution of the curve we use. /// y^2 = x^3 + Curve.B if format != pubkeyCompressed { - return nil, fmt.Errorf("invalid magic in compressed "+ - "pubkey string: %d", pubKeyStr[0]) + str := fmt.Sprintf("invalid public key: unsupported format: %x", + format) + return nil, makeError(ErrPubKeyInvalidFormat, str) } pubkey.x = new(big.Int).SetBytes(pubKeyStr[1:33]) pubkey.y, err = decompressPoint(pubkey.x, ybit) @@ -125,20 +132,24 @@ func ParsePubKey(pubKeyStr []byte) (key *PublicKey, err error) { } default: // wrong! - return nil, fmt.Errorf("invalid pub key length %d", + str := fmt.Sprintf("malformed public key: invalid length: %d", len(pubKeyStr)) + return nil, makeError(ErrPubKeyInvalidLen, str) } curve := S256() if pubkey.x.Cmp(curveParams.P) >= 0 { - return nil, fmt.Errorf("pubkey X parameter is >= to P") + str := "invalid public key: x >= field prime" + return nil, makeError(ErrPubKeyXTooBig, str) } if pubkey.y.Cmp(curveParams.P) >= 0 { - return nil, fmt.Errorf("pubkey Y parameter is >= to P") + str := "invalid public key: y >= field prime" + return nil, makeError(ErrPubKeyYTooBig, str) } if !curve.IsOnCurve(pubkey.x, pubkey.y) { - return nil, fmt.Errorf("pubkey [%v,%v] isn't on secp256k1 curve", + str := fmt.Sprintf("invalid public key: [%v,%v] not on secp256k1 curve", pubkey.x, pubkey.y) + return nil, makeError(ErrPubKeyNotOnCurve, str) } return &pubkey, nil } diff --git a/hdkeychain/extendedkey_test.go b/hdkeychain/extendedkey_test.go index 4e174302..9b3564b0 100644 --- a/hdkeychain/extendedkey_test.go +++ b/hdkeychain/extendedkey_test.go @@ -15,6 +15,8 @@ import ( "errors" "reflect" "testing" + + "github.com/decred/dcrd/dcrec/secp256k1/v3" ) // mockNetParams implements the NetworkParams interface and is used throughout @@ -748,7 +750,10 @@ func TestErrors(t *testing.T) { { name: "pubkey not on curve", key: "dpubZ9169KDAEUnyoTzA7pDGtXbxpji5LuUk8johUPVGY2CDsz6S7hahGNL6QkeYrUeAPnaJD1MBmrsUnErXScGZdjL6b2gjCRX1Z1GNhLdVCjv", - err: errors.New("invalid public key x coordinate"), + err: secp256k1.Error{ + ErrorCode: secp256k1.ErrPubKeyNotOnCurve, + Description: "invalid public key: x coordinate 0000000000000000000000000000000000000000000000000000000000000000 is not on the secp256k1 curve", + }, }, { name: "unsupported version",