Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BIP-340 signature verification #10

Merged
merged 6 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 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,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("point publicKey is infinite")
}
if publicKey.X.Cmp(b.curve.P) == 1 {
return false, fmt.Errorf("point 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("point R is infinite")
}

// Fail if not has_even_y(R).
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
215 changes: 215 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,219 @@ 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: "point publicKey is infinite",
},

{
signature: "FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2",
publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
isValid: false,
expectedErr: "coordinate R.y is not even",
},
{
signature: "1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD",
publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
isValid: false,
expectedErr: "coordinate R.y is not even",
},

{
signature: "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6",
publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
isValid: false,
expectedErr: "coordinate R.x != r",
},
{
signature: "0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051",
publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
isValid: false,
expectedErr: "point R is infinite",
},
{
signature: "00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197",
publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
isValid: false,
expectedErr: "point R is infinite",
},
{
signature: "4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B",
publicKeyX: "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659",
message: "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
isValid: false,
expectedErr: "point R is infinite",
},
Copy link
Member Author

@pdyraga pdyraga Jan 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test vector is problematic. In BIP-340 test vectors, the comment is:

sig[0:32] is not an X coordinate on the curve

So I would expect the test vector to fail on point R is infinite check. It does fail later on coordinate R.x != r. I checked and the point seems to be a valid point on the curve with [x,y] as
[101293062680523315514373137351023114440902235251657644508821325047911886333529, 95491709537915294920828256998521669146617750390665870859237534620269297521559]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signature verification algorithm in BIP-340 does not check that the 32 public key bytes in the signature (rb in the code, r in the BIP) match a valid point on the curve. This check is effectively implicit in the algorithm later, because G and P are known valid curve points, so the point R that is calculated is also a valid point. Thus, if r is not a valid point's X-coordinate, R.X != r.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense! Fixed in 929f34d.

{
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: "point 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
Expand Down
Loading