Skip to content

Commit

Permalink
Draft implementation of BIP-340 signature verification
Browse files Browse the repository at this point in the history
This is a draft implementation. The remaining BIP-340 test vectors need to be
added, TODOs need to be addressed, and one failing test needs to be fixed.

The VerifySignature verifies the provided BIP-340 signature for the message
against the group public key. The function returns true and nil error when the
signature is valid. The function returns false and an error when the signature
is invalid. The error provides a detailed explanation on why the signature
verification failed.

VerifySignature implements Verify(pk, m, sig) function as defined in BIP-340.

One important design decision is to accept *Signature and *Point into Verify
function instead of bytes, as in the prototype. This has the implication. To
ensure consistency and that we'll not return positive verification result for
(x,y) that is not on the curve, we need to verify y coordinate even though
BIP-340 verification is not really interested in it. I think this is acceptable
given the complexity of byte operations is hidden behind a ciphersuite and
inside the protocol code, we'll operate on known domain objects.
  • Loading branch information
pdyraga committed Jan 5, 2024
1 parent f6af9a5 commit da10ff0
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 0 deletions.
112 changes: 112 additions & 0 deletions frost/bip340.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package frost

import (
"crypto/sha256"
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/crypto/secp256k1"
Expand Down Expand Up @@ -213,6 +214,117 @@ func (b *Bip340Ciphersuite) EncodePoint(point *Point) []byte {
return xbs
}

// VerifySignature verifies the provided [BIP-340] signature for the message
// against the group public key. The function returns true and nil error when
// the signature is valid. The function returns false and an error when the
// signature is invalid. The error provides a detailed explanation on why the
// signature verification failed.
//
// VerifySignature implements Verify(pk, m, sig) function as defined in [BIP-340].
func (b *Bip340Ciphersuite) VerifySignature(
signature *Signature,
publicKey *Point,
message []byte,
) (bool, error) {
// Not required by [BIP-340] but performed to ensure input data consistency.
// We do not want to return true if Y is an invalid coordinate.
/*
// TODO: check if not nil coordinates
if !b.curve.IsOnCurve(signature.R.X, signature.R.Y) {
return false, fmt.Errorf("signature.R is not on the curve")
}
if !b.curve.IsOnCurve(publicKey.X, publicKey.Y) {
return false, fmt.Errorf("publicKey is not on the curve")
}
*/

// Let P = lift_x(int(pk)); fail if that fails.
pk := new(big.Int).SetBytes(b.EncodePoint(publicKey))
P, err := b.liftX(pk)
if err != nil {
return false, fmt.Errorf("liftX failed: [%v]", err)
}

// Let r = int(sig[0:32]); fail if r ≥ p.
r := signature.R.X // int(sig[0:32])
if r.Cmp(b.curve.P) != -1 {
return false, fmt.Errorf("r >= P")
}

// Let s = int(sig[32:64]); fail if s ≥ n.
s := signature.Z // int(sig[32:64])
if s.Cmp(b.curve.N) != -1 {
return false, fmt.Errorf("s >= N")
}

// Let e = int(hashBIP0340/challenge(bytes(r) || bytes(P) || m)) mod n.
eHash := b.H2(signature.R.X.Bytes(), P.X.Bytes(), message)
e := new(big.Int).Mod(eHash, b.curve.N)

// Let R = s⋅G - e⋅P.
R := b.curve.EcSub(
b.curve.EcBaseMul(s),
b.curve.EcMul(P, e),
)

// Fail if .
if !b.curve.IsOnCurve(R.X, R.Y) {
return false, fmt.Errorf("point R is infinite")
}

// Fail if .
if R.Y.Bit(0) != 0 {
return false, fmt.Errorf("coordinate R.y is not even")
}

// Fail if x(R) ≠ r.
if R.X.Cmp(r) != 0 {
return false, fmt.Errorf("coordinate R.x != r")
}

// Return success if no failure occurred before reaching this point.
return true, nil
}

// liftX function implements lift_x(x) function as defined in [BIP-340].
func (b *Bip340Ciphersuite) liftX(x *big.Int) (*Point, error) {
// From [BIP-340] specification section:
//
// The function lift_x(x), where x is a 256-bit unsigned integer, returns
// the point P for which x(P) = x[10] and has_even_y(P), or fails if x is
// greater than p-1 or no such point exists.

// Fail if x ≥ p.
p := b.curve.P
if x.Cmp(p) != -1 {
return nil, fmt.Errorf("value of x exceeds field size")
}

// Let c = x^3 + 7 mod p.
c := new(big.Int).Exp(x, big.NewInt(3), p)
c.Add(c, big.NewInt(7))
c.Mod(c, p)

// Let y = c^[(p+1)/4] mod p.
e := new(big.Int).Add(p, big.NewInt(1))
e.Div(e, big.NewInt(4))
y := new(big.Int).Exp(c, e, p)

// Fail if c ≠ y^2 mod p.
y2 := new(big.Int).Exp(y, big.NewInt(2), p)
if c.Cmp(y2) != 0 {
return nil, fmt.Errorf("no curve point matching x")
}

// Return the unique point P such that x(P) = x and y(P) = y if y mod 2 = 0
// or y(P) = p-y otherwise.
if y.Bit(0) != 0 {
y.Sub(p, y)
}
return &Point{x, y}, nil
}

// concat performs a concatenation of byte slices without the modification of
// the slices passed as parameters. A brand new slice instance is always
// returned from the function.
Expand Down
99 changes: 99 additions & 0 deletions frost/bip340_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package frost

import (
"bytes"
"encoding/hex"
"fmt"
"math/big"
"testing"

Expand Down Expand Up @@ -407,6 +409,103 @@ func TestBip340CiphersuiteHash(t *testing.T) {
}
}

func TestVerifySignature(t *testing.T) {
tests := []struct {
signature string
publicKeyX string
message string
isValid bool
expectedErr string
}{
{
signature: "E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0",
publicKeyX: "F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",
message: "0000000000000000000000000000000000000000000000000000000000000000",
isValid: true,
},
{
signature: "6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A",
publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
isValid: true,
},
{
signature: "5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7",
publicKeyX: "DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8",
message: "7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C",
isValid: true,
},
{
signature: "7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3",
publicKeyX: "25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517",
message: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
isValid: true,
},
{
signature: "00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4",
publicKeyX: "D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9",
message: "4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703",
isValid: true,
},
// TODO: add the remaining BIP-340 test vectors
// https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv
}

for i, test := range tests {
t.Run(fmt.Sprintf("test case %v", i), func(t *testing.T) {
sigBytes, err := hex.DecodeString(test.signature)
if err != nil {
t.Fatal(err)
}

pubKeyXBytes, err := hex.DecodeString(test.publicKeyX)
if err != nil {
t.Fatal(err)
}

msg, err := hex.DecodeString(test.message)
if err != nil {
t.Fatal(err)
}

signature := &Signature{
R: &Point{
X: new(big.Int).SetBytes(sigBytes[0:32]),
Y: nil, // TODO: fix it
},
Z: new(big.Int).SetBytes(sigBytes[32:64]),
}

pubKey := &Point{
X: new(big.Int).SetBytes(pubKeyXBytes),
Y: nil, // TODO: fix it
}

ciphersuite = NewBip340Ciphersuite()
res, err := ciphersuite.VerifySignature(signature, pubKey, msg)

testutils.AssertBoolsEqual(
t,
"signature verification result",
test.isValid,
res,
)

if !test.isValid {
if err == nil {
t.Fatal("expected not-nil error")
}
testutils.AssertStringsEqual(
t,
"signature verification error message",
test.expectedErr,
err.Error(),
)
}
})
}
}

func TestConcat(t *testing.T) {
tests := map[string]struct {
expected []byte
Expand Down
11 changes: 11 additions & 0 deletions frost/ciphersuite.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ type Ciphersuite interface {
// serialization, always reflecting the given ciphersuite's specification
// requirements.
EncodePoint(point *Point) []byte

// VerifySignature verifies the provided signature for the message against
// the group public key. The function returns true and nil error when the
// signature is valid. The function returns false and an error when the
// signature is valid. The error provides a detailed explanation on why
// the signature verification failed.
VerifySignature(
signature *Signature,
publicKey *Point,
message []byte,
) (bool, error)
}

// Hashing interface abstracts out hash functions implementations specific to the
Expand Down
13 changes: 13 additions & 0 deletions internal/testutils/testutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ func AssertStringsEqual(t *testing.T, description string, expected string, actua
}
}

// AssertBoolsEqual checks if two booleans are equal. If not, it reports a test
// failure.
func AssertBoolsEqual(t *testing.T, description string, expected bool, actual bool) {
if expected != actual {
t.Errorf(
"unexpected %s\nexpected: %v\nactual: %v\n",
description,
expected,
actual,
)
}
}

func testBytesEqual(expectedBytes []byte, actualBytes []byte) error {
minLen := len(expectedBytes)
diffCount := 0
Expand Down

0 comments on commit da10ff0

Please sign in to comment.