Add code to produce and verify compact signatures.

The format used is identical to that used in bitcoind.
This commit is contained in:
Owain G. Ainsworth 2014-02-11 01:43:21 +00:00
parent 218906a91e
commit ff3fac426d
5 changed files with 297 additions and 41 deletions

View file

@ -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))
}

View file

@ -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))

View file

@ -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<Rx> 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><padded bytes for signature S>
// 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 <header byte><bitlen R><bitlen S>
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
}

View file

@ -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)
}
}

View file

@ -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)