secp256k1: Add pubkey parsing error infrastructure.

This introduces a new error type and associated error code for public
key errors which can be used with the errors.Is and errors.As interfaces
to programmatically detect the failure reason and updates the public key
parsing code to return appropriate error codes.

It includes full tests for the new error infrastructure.
This commit is contained in:
Dave Collins 2020-04-05 14:38:24 -05:00
parent 86b11abbd6
commit 9060dc3afe
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
4 changed files with 301 additions and 14 deletions

126
dcrec/secp256k1/error.go Normal file
View File

@ -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}
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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",