From ff3fac426d4d037505ea8208b79e93c2852451e0 Mon Sep 17 00:00:00 2001 From: "Owain G. Ainsworth" Date: Tue, 11 Feb 2014 01:43:21 +0000 Subject: [PATCH] Add code to produce and verify compact signatures. The format used is identical to that used in bitcoind. --- btcec.go | 2 + pubkey.go | 47 ++++++++----- signature.go | 168 ++++++++++++++++++++++++++++++++++++++++++++++ signature_test.go | 70 +++++++++++++++++++ test_coverage.txt | 51 +++++++------- 5 files changed, 297 insertions(+), 41 deletions(-) diff --git a/btcec.go b/btcec.go index f3d05f7bc..b14a7e5a8 100644 --- a/btcec.go +++ b/btcec.go @@ -38,6 +38,7 @@ var ( type KoblitzCurve struct { *elliptic.CurveParams q *big.Int + H int // cofactor of the curve. } // Params returns the parameters for the curve. @@ -652,6 +653,7 @@ func initS256() { secp256k1.Gx, _ = new(big.Int).SetString("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 16) secp256k1.Gy, _ = new(big.Int).SetString("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 16) secp256k1.BitSize = 256 + secp256k1.H = 1 secp256k1.q = new(big.Int).Div(new(big.Int).Add(secp256k1.P, big.NewInt(1)), big.NewInt(4)) } diff --git a/pubkey.go b/pubkey.go index 9844db1ea..070bbc295 100644 --- a/pubkey.go +++ b/pubkey.go @@ -14,6 +14,32 @@ func isOdd(a *big.Int) bool { return a.Bit(0) == 1 } +// decompressPoint decompresses a point on the given curve given the X point and +// the solution to use. +func decompressPoint(curve *KoblitzCurve, x *big.Int, ybit bool) (*big.Int, error) { + // TODO(oga) This will probably only work for secp256k1 due to + // optimisations. + + // Y = +-sqrt(x^3 + B) + x3 := new(big.Int).Mul(x, x) + x3.Mul(x3, x) + x3.Add(x3, curve.Params().B) + + // now calculate sqrt mod p of x2 + B + // This code used to do a full sqrt based on tonelli/shanks, + // but this was replaced by the algorithms referenced in + // https://bitcointalk.org/index.php?topic=162805.msg1712294#msg1712294 + y := new(big.Int).Exp(x3, curve.QPlus1Div4(), curve.Params().P) + + if ybit != isOdd(y) { + y.Sub(curve.Params().P, y) + } + if ybit != isOdd(y) { + return nil, fmt.Errorf("ybit doesn't match oddness") + } + return y, nil +} + const ( pubkeyCompressed byte = 0x2 // y_bit + x coord pubkeyUncompressed byte = 0x4 // x coord + y coord @@ -53,25 +79,10 @@ func ParsePubKey(pubKeyStr []byte, curve *KoblitzCurve) (key *ecdsa.PublicKey, e "pubkey string: %d", pubKeyStr[0]) } pubkey.X = new(big.Int).SetBytes(pubKeyStr[1:33]) - // Y = +-sqrt(x^3 + B) - x3 := new(big.Int).Mul(pubkey.X, pubkey.X) - x3.Mul(x3, pubkey.X) - x3.Add(x3, pubkey.Curve.Params().B) - - // now calculate sqrt mod p of x2 + B - // This code used to do a full sqrt based on tonelli/shanks, - // but this was replaced by the algorithms referenced in - // https://bitcointalk.org/index.php?topic=162805.msg1712294#msg1712294 - y := new(big.Int).Exp(x3, curve.QPlus1Div4(), pubkey.Curve.Params().P) - - if ybit != isOdd(y) { - y.Sub(pubkey.Curve.Params().P, y) + pubkey.Y, err = decompressPoint(curve, pubkey.X, ybit) + if err != nil { + return nil, err } - if ybit != isOdd(y) { - return nil, fmt.Errorf("ybit doesn't match oddness") - } - - pubkey.Y = y default: // wrong! return nil, fmt.Errorf("invalid pub key length %d", len(pubKeyStr)) diff --git a/signature.go b/signature.go index 0015d5a60..e9da0dc49 100644 --- a/signature.go +++ b/signature.go @@ -5,7 +5,9 @@ package btcec import ( + "crypto/ecdsa" "crypto/elliptic" + "crypto/rand" "errors" "fmt" "math/big" @@ -213,3 +215,169 @@ func canonicalPadding(b []byte) error { return nil } } + +// hashToInt converts a hash value to an integer. There is some disagreement +// about how this is done. [NSA] suggests that this is done in the obvious +// manner, but [SECG] truncates the hash to the bit-length of the curve order +// first. We follow [SECG] because that's what OpenSSL does. Additionally, +// OpenSSL right shifts excess bits from the number if the hash is too large +// and we mirror that too. +// This is borrowed from crypto/ecdsa. +func hashToInt(hash []byte, c elliptic.Curve) *big.Int { + orderBits := c.Params().N.BitLen() + orderBytes := (orderBits + 7) / 8 + if len(hash) > orderBytes { + hash = hash[:orderBytes] + } + + ret := new(big.Int).SetBytes(hash) + excess := len(hash)*8 - orderBits + if excess > 0 { + ret.Rsh(ret, uint(excess)) + } + return ret +} + +// recoverKeyFromSignature recoves a public key from the signature "sig" on the +// given message hash "msg". Based on the algorithm found in section 5.1.5 of +// SEC 1 Ver 2.0, page 47-48 (53 and 54 in the pdf). This performs the details +// in the inner loop in Step 1. The counter provided is actually the j parameter +// of the loop * 2 - on the first iteration of j we do the R case, else the -R +// case in step 1.6. This counter is used in the bitcoin compressed signature +// format and thus we match bitcoind's behaviour here. +func recoverKeyFromSignature(curve *KoblitzCurve, sig *Signature, msg []byte, + iter int, doChecks bool) (*ecdsa.PublicKey, error) { + // 1.1 x = (n * i) + r + Rx := new(big.Int).Mul(curve.Params().N, + new(big.Int).SetInt64(int64(iter/2))) + Rx.Add(Rx, sig.R) + if Rx.Cmp(curve.Params().P) != -1 { + return nil, errors.New("calculated Rx is larger than curve P") + } + + // convert 02 to point R. (step 1.2 and 1.3). If we are on an odd + // iteration then 1.6 will be done with -R, so we calculate the other + // term when uncompressing the point. + Ry, err := decompressPoint(curve, Rx, iter%2 == 1) + if err != nil { + return nil, err + } + + // 1.4 Check n*R is point at infinity + if doChecks { + nRx, nRy := curve.ScalarMult(Rx, Ry, curve.Params().N.Bytes()) + if nRx.Sign() != 0 || nRy.Sign() != 0 { + return nil, errors.New("R*n does not equal the point at infinity") + } + } + + // 1.5 calculate e from message using the same algorithm as ecdsa + // signature calculation. + e := hashToInt(msg, curve) + + // Step 1.6.1: + // We calculate the two terms sR and eG separately multiplied by the + // inverse of r (from the signature). We then add them to calculate + // Q = r^-1(sR-eG) + invr := new(big.Int).ModInverse(sig.R, curve.Params().N) + + // first term. + invrS := new(big.Int).Mul(invr, sig.S) + invrS.Mod(invrS, curve.Params().N) + sRx, sRy := curve.ScalarMult(Rx, Ry, invrS.Bytes()) + + // second term. + e.Neg(e) + e.Mod(e, curve.Params().N) + e.Mul(e, invr) + e.Mod(e, curve.Params().N) + minuseGx, minuseGy := curve.ScalarBaseMult(e.Bytes()) + + // TODO(oga) this would be faster if we did a mult and add in one + // step to prevent the jacobian conversion back and forth. + Qx, Qy := curve.Add(sRx, sRy, minuseGx, minuseGy) + + return &ecdsa.PublicKey{ + Curve: curve, + X: Qx, + Y: Qy, + }, nil +} + +// SignCompact produces a compact signature of the data in hash with the given +// private key on the given koblitz curve. The isCompressed parameter should +// be used to detail if the given signature should reference a compressed +// public key or not. If successful the bytes of the compact signature will be +// returned in the format: +// <(byte of 27+public key solution)+4 if compressed >< padded bytes for signature R> +// where the R and S parameters are padde up to the bitlengh of the curve. +func SignCompact(curve *KoblitzCurve, key *ecdsa.PrivateKey, + hash []byte, isCompressedKey bool) ([]byte, error) { + r, s, err := ecdsa.Sign(rand.Reader, key, hash) + if err != nil { + return nil, err + } + + sig := &Signature{R: r, S: s} + // bitcoind checks the bit length of R and S here. The ecdsa signature + // algorithm returns R and S mod N therefore they will be the bitsize of + // the curve, and thus correctly sized. + for i := 0; i < (curve.H+1)*2; i++ { + pk, err := recoverKeyFromSignature(curve, sig, hash, i, true) + if err == nil && pk.X.Cmp(key.X) == 0 && pk.Y.Cmp(key.Y) == 0 { + result := make([]byte, 1, 2*(curve.BitSize/8)+1) + result[0] = 27 + byte(i) + if isCompressedKey { + result[0] += 4 + } + // Not sure this needs rounding but safer to do so. + curvelen := (curve.BitSize + 7) / 8 + + // Pad R and S to curvelen if needed. + bytelen := (sig.R.BitLen() + 7) / 8 + if bytelen < curvelen { + result = append(result, + make([]byte, curvelen-bytelen)...) + } + result = append(result, sig.R.Bytes()...) + + bytelen = (sig.S.BitLen() + 7) / 8 + if bytelen < curvelen { + result = append(result, + make([]byte, curvelen-bytelen)...) + } + result = append(result, sig.S.Bytes()...) + + return result, nil + } + } + + return nil, errors.New("no valid solution for pubkey found") +} + +// RecoverCompact verifies the compact signature "signature" of "hash" for the +// Koblitz curve in "curve". If the signature matches then the recovered public +// key will be returned as well as a boolen if the original key was compressed +// or not, else an error will be returned. +func RecoverCompact(curve *KoblitzCurve, signature, + hash []byte) (*ecdsa.PublicKey, bool, error) { + bitlen := (curve.BitSize + 7) / 8 + if len(signature) != 1+bitlen*2 { + return nil, false, errors.New("invalid compact signature size") + } + + iteration := int((signature[0] - 27) & ^byte(4)) + + // format is
+ sig := &Signature{ + R: new(big.Int).SetBytes(signature[1 : bitlen+1]), + S: new(big.Int).SetBytes(signature[bitlen+1:]), + } + // The iteration used here was encoded + key, err := recoverKeyFromSignature(curve, sig, hash, iteration, false) + if err != nil { + return nil, false, err + } + + return key, ((signature[0] - 27) & 4) == 4, nil +} diff --git a/signature_test.go b/signature_test.go index f2cf074b7..97dda5209 100644 --- a/signature_test.go +++ b/signature_test.go @@ -6,6 +6,9 @@ package btcec_test import ( "bytes" + "crypto/ecdsa" + "crypto/rand" + "fmt" "github.com/conformal/btcec" "math/big" "testing" @@ -420,3 +423,70 @@ func TestSignatureSerialize(t *testing.T) { } } } + +func testSignCompact(t *testing.T, tag string, curve *btcec.KoblitzCurve, + data []byte, isCompressed bool) { + priv, _ := ecdsa.GenerateKey(curve, rand.Reader) + + hashed := []byte("testing") + sig, err := btcec.SignCompact(curve, priv, hashed, isCompressed) + if err != nil { + t.Errorf("%s: error signing: %s", tag, err) + return + } + + pk, wasCompressed, err := btcec.RecoverCompact(curve, sig, hashed) + if err != nil { + t.Errorf("%s: error recovering: %s", tag, err) + return + } + if pk.X.Cmp(priv.X) != 0 || pk.Y.Cmp(priv.Y) != 0 { + t.Errorf("%s: recovered pubkey doesn't match original "+ + "(%v,%v) vs (%v,%v) ", tag, pk.X, pk.Y, priv.X, priv.Y) + return + } + if wasCompressed != isCompressed { + t.Errorf("%s: recovered pubkey doesn't match compressed state "+ + "(%v vs %v)", tag, isCompressed, wasCompressed) + return + } + + // If we change the compressed bit we should get the same key back, + // but the compressed flag should be reversed. + if isCompressed { + sig[0] -= 4 + } else { + sig[0] += 4 + } + + pk, wasCompressed, err = btcec.RecoverCompact(curve, sig, hashed) + if err != nil { + t.Errorf("%s: error recovering (2): %s", tag, err) + return + } + if pk.X.Cmp(priv.X) != 0 || pk.Y.Cmp(priv.Y) != 0 { + t.Errorf("%s: recovered pubkey (2) doesn't match original "+ + "(%v,%v) vs (%v,%v) ", tag, pk.X, pk.Y, priv.X, priv.Y) + return + } + if wasCompressed == isCompressed { + t.Errorf("%s: recovered pubkey doesn't match reversed "+ + "compressed state (%v vs %v)", tag, isCompressed, + wasCompressed) + return + } +} + +func TestSignCompact(t *testing.T) { + for i := 0; i < 256; i++ { + name := fmt.Sprintf("test %d", i) + data := make([]byte, 32) + _, err := rand.Read(data) + if err != nil { + t.Errorf("failed to read random data for %s", name) + continue + } + compressed := i%2 != 0 + testSignCompact(t, name, btcec.S256(), data, compressed) + } +} diff --git a/test_coverage.txt b/test_coverage.txt index 2216fd66f..a44bf43ee 100644 --- a/test_coverage.txt +++ b/test_coverage.txt @@ -10,50 +10,55 @@ github.com/conformal/btcec/field.go fieldVal.PutBytes 100.00% (32/32) github.com/conformal/btcec/btcec.go KoblitzCurve.addZ1EqualsZ2 100.00% (30/30) github.com/conformal/btcec/btcec.go KoblitzCurve.addZ1AndZ2EqualsOne 100.00% (29/29) github.com/conformal/btcec/btcec.go KoblitzCurve.addJacobian 100.00% (22/22) -github.com/conformal/btcec/btcec.go KoblitzCurve.doubleZ1EqualsOne 100.00% (18/18) github.com/conformal/btcec/btcec.go KoblitzCurve.doubleGeneric 100.00% (18/18) +github.com/conformal/btcec/btcec.go KoblitzCurve.doubleZ1EqualsOne 100.00% (18/18) github.com/conformal/btcec/signature.go Signature.Serialize 100.00% (13/13) github.com/conformal/btcec/btcec.go KoblitzCurve.fieldJacobianToBigAffine 100.00% (12/12) github.com/conformal/btcec/field.go fieldVal.MulInt 100.00% (12/12) -github.com/conformal/btcec/field.go fieldVal.Add2 100.00% (11/11) github.com/conformal/btcec/field.go fieldVal.Add 100.00% (11/11) -github.com/conformal/btcec/field.go fieldVal.NegateVal 100.00% (11/11) +github.com/conformal/btcec/field.go fieldVal.Add2 100.00% (11/11) github.com/conformal/btcec/field.go fieldVal.SetBytes 100.00% (11/11) -github.com/conformal/btcec/btcec.go KoblitzCurve.ScalarMult 100.00% (10/10) -github.com/conformal/btcec/btcec.go KoblitzCurve.Add 100.00% (10/10) +github.com/conformal/btcec/field.go fieldVal.NegateVal 100.00% (11/11) github.com/conformal/btcec/field.go fieldVal.Zero 100.00% (10/10) +github.com/conformal/btcec/btcec.go KoblitzCurve.Add 100.00% (10/10) +github.com/conformal/btcec/btcec.go KoblitzCurve.ScalarMult 100.00% (10/10) github.com/conformal/btcec/btcec.go KoblitzCurve.doubleJacobian 100.00% (9/9) -github.com/conformal/btcec/signature.go canonicalizeInt 100.00% (8/8) +github.com/conformal/btcec/btcec.go initS256 100.00% (9/9) github.com/conformal/btcec/pubkey.go PublicKey.SerializeHybrid 100.00% (8/8) -github.com/conformal/btcec/btcec.go initS256 100.00% (8/8) +github.com/conformal/btcec/signature.go canonicalizeInt 100.00% (8/8) github.com/conformal/btcec/pubkey.go PublicKey.SerializeCompressed 100.00% (7/7) github.com/conformal/btcec/btcec.go KoblitzCurve.Double 100.00% (7/7) -github.com/conformal/btcec/field.go fieldVal.SetByteSlice 100.00% (5/5) github.com/conformal/btcec/pubkey.go PublicKey.SerializeUncompressed 100.00% (5/5) +github.com/conformal/btcec/field.go fieldVal.SetByteSlice 100.00% (5/5) github.com/conformal/btcec/pubkey.go pad 100.00% (5/5) +github.com/conformal/btcec/btcec.go KoblitzCurve.bigAffineToField 100.00% (4/4) github.com/conformal/btcec/field.go fieldVal.SetHex 100.00% (4/4) github.com/conformal/btcec/signature.go canonicalPadding 100.00% (4/4) -github.com/conformal/btcec/btcec.go KoblitzCurve.bigAffineToField 100.00% (4/4) github.com/conformal/btcec/btcec.go KoblitzCurve.IsOnCurve 100.00% (4/4) github.com/conformal/btcec/field.go fieldVal.SetInt 100.00% (3/3) github.com/conformal/btcec/field.go fieldVal.Bytes 100.00% (3/3) +github.com/conformal/btcec/field.go fieldVal.IsZero 100.00% (2/2) +github.com/conformal/btcec/field.go fieldVal.AddInt 100.00% (2/2) +github.com/conformal/btcec/field.go fieldVal.Equals 100.00% (2/2) +github.com/conformal/btcec/field.go fieldVal.Set 100.00% (2/2) github.com/conformal/btcec/field.go fieldVal.String 100.00% (2/2) github.com/conformal/btcec/btcec.go S256 100.00% (2/2) -github.com/conformal/btcec/field.go fieldVal.IsZero 100.00% (2/2) -github.com/conformal/btcec/field.go fieldVal.Equals 100.00% (2/2) -github.com/conformal/btcec/field.go fieldVal.AddInt 100.00% (2/2) -github.com/conformal/btcec/field.go fieldVal.Set 100.00% (2/2) github.com/conformal/btcec/pubkey.go isOdd 100.00% (1/1) -github.com/conformal/btcec/field.go fieldVal.Mul 100.00% (1/1) -github.com/conformal/btcec/field.go fieldVal.Square 100.00% (1/1) -github.com/conformal/btcec/btcec.go KoblitzCurve.Params 100.00% (1/1) -github.com/conformal/btcec/btcec.go KoblitzCurve.ScalarBaseMult 100.00% (1/1) -github.com/conformal/btcec/btcec.go KoblitzCurve.QPlus1Div4 100.00% (1/1) -github.com/conformal/btcec/btcec.go initAll 100.00% (1/1) -github.com/conformal/btcec/field.go fieldVal.Negate 100.00% (1/1) -github.com/conformal/btcec/signature.go ParseSignature 100.00% (1/1) github.com/conformal/btcec/signature.go ParseDERSignature 100.00% (1/1) +github.com/conformal/btcec/btcec.go KoblitzCurve.ScalarBaseMult 100.00% (1/1) +github.com/conformal/btcec/field.go fieldVal.Mul 100.00% (1/1) +github.com/conformal/btcec/btcec.go KoblitzCurve.Params 100.00% (1/1) +github.com/conformal/btcec/field.go fieldVal.Square 100.00% (1/1) +github.com/conformal/btcec/field.go fieldVal.Negate 100.00% (1/1) github.com/conformal/btcec/field.go fieldVal.IsOdd 100.00% (1/1) -github.com/conformal/btcec/pubkey.go ParsePubKey 96.88% (31/32) -github.com/conformal/btcec ------------------------------------- 99.88% (846/847) +github.com/conformal/btcec/btcec.go initAll 100.00% (1/1) +github.com/conformal/btcec/btcec.go KoblitzCurve.QPlus1Div4 100.00% (1/1) +github.com/conformal/btcec/signature.go ParseSignature 100.00% (1/1) +github.com/conformal/btcec/pubkey.go ParsePubKey 96.15% (25/26) +github.com/conformal/btcec/signature.go SignCompact 90.91% (20/22) +github.com/conformal/btcec/pubkey.go decompressPoint 88.89% (8/9) +github.com/conformal/btcec/signature.go recoverKeyFromSignature 86.96% (20/23) +github.com/conformal/btcec/signature.go RecoverCompact 77.78% (7/9) +github.com/conformal/btcec/signature.go hashToInt 77.78% (7/9) +github.com/conformal/btcec ------------------------------------- 98.80% (903/914)