diff --git a/frost/bip340.go b/frost/bip340.go index 17888a2..9a36153 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" @@ -54,7 +55,7 @@ func (bc *Bip340Curve) EcAdd(a *Point, b *Point) *Point { // EcSub returns the subtraction of two elliptic curve points. func (bc *Bip340Curve) EcSub(a *Point, b *Point) *Point { - bNeg := &Point{b.X, new(big.Int).Neg(b.Y)} + bNeg := &Point{b.X, new(big.Int).Sub(bc.Params().P, b.Y)} return bc.EcAdd(a, bNeg) } @@ -213,6 +214,145 @@ 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 defined in [BIP-340]. +func (b *Bip340Ciphersuite) VerifySignature( + signature *Signature, + publicKey *Point, + message []byte, +) (bool, error) { + // This function accepts the public key as an elliptic curve point and + // signature as a structure outputted as defined in [FROST] aggregate + // function. This is not precisely how [BIP-340] defines the verification + // function signature. From [BIP-340]: + // + // "Note that the correctness of verification relies on the fact that lift_x + // always returns a point with an even Y coordinate. A hypothetical + // verification algorithm that treats points as public keys, and takes the + // point P directly as input would fail any time a point with odd Y is used. + // While it is possible to correct for this by negating points with odd Y + // coordinate before further processing, this would result in a scheme where + // every (message, signature) pair is valid for two public keys (a type of + // malleability that exists for ECDSA as well, but we don't wish to retain). + // We avoid these problems by treating just the X coordinate as public key." + // + // In our specific case, we define FROST ciphersuite that will operate on + // the same types as the [FROST] algorithm. This is a requirement to make + // the ciphersuite used generic for [FROST]. + // + // Accepting the public key as a point and signature as a struct outputted + // from the aggregate function makes the code easier to follow as no + // conversions have to be made before the verification. Also, from our + // specific perspective, it does not make a difference where the conversion + // is made and where we strip the public key's Y coordinate information: + // between the aggregation and before the verification or inside the + // verification. For a more generic case where we would validate [BIP-340] + // signatures from Bitcoin chain, it would make more sense to strip Y + // coordinate before calling this function. + + // 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. + if !b.curve.IsOnCurve(publicKey.X, publicKey.Y) { + return false, fmt.Errorf("publicKey is infinite") + } + if publicKey.X.Cmp(b.curve.P) == 1 { + return false, fmt.Errorf("publicKey exceeds field size") + } + + // 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( + b.EncodePoint(signature.R), + b.EncodePoint(P), + 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 is_infinite(R) + if !b.curve.IsOnCurve(R.X, R.Y) { + return false, fmt.Errorf("R is infinite") + } + + // Fail if not has_even_y(R). + if R.Y.Bit(0) != 0 { + return false, fmt.Errorf("R.y is not even") + } + + // Fail if x(R) != r. + if R.X.Cmp(r) != 0 { + return false, fmt.Errorf("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..dd18be9 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,218 @@ func TestBip340CiphersuiteHash(t *testing.T) { } } +func TestVerifySignature(t *testing.T) { + tests := []struct { + signature string + publicKeyX string + message string + isValid bool + expectedErr string + }{ + // official [BIP-340] test vectors: https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv + { + 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, + }, + { + signature: "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", + publicKeyX: "EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34", + message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + isValid: false, + expectedErr: "publicKey is infinite", + }, + { + signature: "FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2", + publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + isValid: false, + expectedErr: "R.y is not even", + }, + { + signature: "1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD", + publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + isValid: false, + expectedErr: "R.y is not even", + }, + + { + signature: "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6", + publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + isValid: false, + expectedErr: "R.x != r", + }, + { + signature: "0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051", + publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + isValid: false, + expectedErr: "R is infinite", + }, + { + signature: "00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197", + publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + isValid: false, + expectedErr: "R is infinite", + }, + { + signature: "4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", + publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + isValid: false, + expectedErr: "R.x != r", + }, + { + signature: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", + publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + isValid: false, + expectedErr: "r >= P", + }, + { + signature: "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + isValid: false, + expectedErr: "s >= N", + }, + { + signature: "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", + publicKeyX: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + isValid: false, + expectedErr: "publicKey exceeds field size", + }, + { + signature: "71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63", + publicKeyX: "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + message: "", + isValid: true, + }, + { + signature: "08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF", + publicKeyX: "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + message: "11", + isValid: true, + }, + { + signature: "5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5", + publicKeyX: "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + message: "0102030405060708090A0B0C0D0E0F1011", + isValid: true, + }, + { + signature: "403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367", + publicKeyX: "778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117", + message: "99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", + isValid: true, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("test case %v", i), func(t *testing.T) { + ciphersuite = NewBip340Ciphersuite() + + calculateY := func(x *big.Int) *big.Int { + x3 := new(big.Int).Mul(x, x) //x² + x3.Mul(x3, x) //x³ + x3.Add(x3, ciphersuite.curve.B) //x³+B + x3.Mod(x3, ciphersuite.curve.P) //(x³+B)%P + y := new(big.Int).ModSqrt(x3, ciphersuite.curve.P) + + // x is not on the curve; this is a negative test case for + // which we can't calculate y + if y == nil { + return big.NewInt(2) // even + } + + return y + } + + 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) + } + + rX := new(big.Int).SetBytes(sigBytes[0:32]) + rY := calculateY(rX) + signature := &Signature{ + R: &Point{ + X: rX, + Y: rY, + }, + Z: new(big.Int).SetBytes(sigBytes[32:64]), + } + + pubKeyX := new(big.Int).SetBytes(pubKeyXBytes) + pubKeyY := calculateY(pubKeyX) + pubKey := &Point{ + X: pubKeyX, + Y: pubKeyY, + } + + 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..8d22396 100644 --- a/frost/ciphersuite.go +++ b/frost/ciphersuite.go @@ -14,17 +14,16 @@ type Ciphersuite interface { Hashing Curve() Curve - // EncodePoint encodes the given elliptic curve point to a byte slice in - // a way that is *specific* to the given ciphersuite needs. This is - // especially important when calculating a signature challenge in [FROST]. - // - // This function may yield a different result than SerializePoint function - // from the Curve interface. While the SerializePoint result should be - // considered an internal serialization that may be optimized for speed or - // data consistency, the EncodePoint result should be considered an external - // 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 @@ -44,6 +43,18 @@ type Hashing interface { H3(m []byte, ms ...[]byte) *big.Int H4(m []byte) []byte H5(m []byte) []byte + + // EncodePoint encodes the given elliptic curve point to a byte slice in + // a way that is *specific* to the given ciphersuite needs. This is + // especially important when calculating a signature challenge in [FROST]. + // + // This function may yield a different result than SerializePoint function + // from the Curve interface. While the SerializePoint result should be + // considered an internal serialization that may be optimized for speed or + // data consistency, the EncodePoint result should be considered an external + // serialization, always reflecting the given ciphersuite's specification + // requirements. + EncodePoint(point *Point) []byte } // Curve interface abstracts out the particular elliptic curve implementation 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