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:
parent
86b11abbd6
commit
9060dc3afe
126
dcrec/secp256k1/error.go
Normal file
126
dcrec/secp256k1/error.go
Normal 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}
|
||||
}
|
||||
145
dcrec/secp256k1/error_test.go
Normal file
145
dcrec/secp256k1/error_test.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user