diff --git a/frost/bip340.go b/frost/bip340.go index 17888a2..0eacf3b 100644 --- a/frost/bip340.go +++ b/frost/bip340.go @@ -2,6 +2,7 @@ package frost import ( "crypto/sha256" + "fmt" "math/big" "github.com/ethereum/go-ethereum/crypto/secp256k1" @@ -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. diff --git a/frost/bip340_test.go b/frost/bip340_test.go index 86bfb66..1025a9b 100644 --- a/frost/bip340_test.go +++ b/frost/bip340_test.go @@ -2,6 +2,8 @@ package frost import ( "bytes" + "encoding/hex" + "fmt" "math/big" "testing" @@ -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 diff --git a/frost/ciphersuite.go b/frost/ciphersuite.go index 6d878e4..db3426a 100644 --- a/frost/ciphersuite.go +++ b/frost/ciphersuite.go @@ -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 diff --git a/internal/testutils/testutils.go b/internal/testutils/testutils.go index 5cafd82..1c29bc1 100644 --- a/internal/testutils/testutils.go +++ b/internal/testutils/testutils.go @@ -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