Skip to content

Commit

Permalink
core: implement key recover interops
Browse files Browse the repository at this point in the history
Part of #1003
  • Loading branch information
AnnaShaleva committed Jun 2, 2020
1 parent 0f6d01f commit 53bb283
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 11 deletions.
30 changes: 30 additions & 0 deletions pkg/core/interop_neo.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package core

import (
"crypto/elliptic"
"errors"
"fmt"
"math"
"math/big"

"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
Expand Down Expand Up @@ -600,6 +602,34 @@ func (ic *interopContext) contractMigrate(v *vm.VM) error {
return ic.contractDestroy(v)
}

// secp256k1Recover recovers speck256k1 public key.
func (ic *interopContext) secp256k1Recover(v *vm.VM) error {
return ic.eccRecover(keys.Secp256k1(), v)
}

// secp256r1Recover recovers speck256r1 public key.
func (ic *interopContext) secp256r1Recover(v *vm.VM) error {
return ic.eccRecover(elliptic.P256(), v)
}

// eccRecover recovers public key using ECCurve set
func (ic *interopContext) eccRecover(curve elliptic.Curve, v *vm.VM) error {
// TODO: perhaps, we don't need to reverse here, depends on the way how we store them
rBytes := append(util.ArrayReverse(v.Estack().Pop().Bytes()), 1)
sBytes := append(util.ArrayReverse(v.Estack().Pop().Bytes()), 1)
r := new(big.Int).SetBytes(rBytes)
s := new(big.Int).SetBytes(sBytes)
isEven := v.Estack().Pop().Bool()
messageHash := v.Estack().Pop().Bytes()
pKey, err := keys.KeyRecover(curve, r, s, messageHash, isEven)
if err != nil {
v.Estack().PushVal(vm.NewByteArrayItem([]byte{}))
return nil
}
v.Estack().PushVal(vm.NewByteArrayItem(pKey.Bytes()[1:]))
return nil
}

// assetCreate creates an asset.
func (ic *interopContext) assetCreate(v *vm.VM) error {
if ic.trigger != trigger.Application {
Expand Down
2 changes: 2 additions & 0 deletions pkg/core/interops.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ var neoInterops = []interopedFunction{
{Name: "Neo.Contract.GetStorageContext", Func: (*interopContext).contractGetStorageContext, Price: 1},
{Name: "Neo.Contract.IsPayable", Func: (*interopContext).contractIsPayable, Price: 1},
{Name: "Neo.Contract.Migrate", Func: (*interopContext).contractMigrate, Price: 0},
{Name: "Neo.Cryptography.Secp256k1Recover", Func: (*interopContext).secp256k1Recover, Price: 100},
{Name: "Neo.Cryptography.Secp256r1Recover", Func: (*interopContext).secp256r1Recover, Price: 100},
{Name: "Neo.Enumerator.Concat", Func: (*interopContext).enumeratorConcat, Price: 1},
{Name: "Neo.Enumerator.Create", Func: (*interopContext).enumeratorCreate, Price: 1},
{Name: "Neo.Enumerator.Next", Func: (*interopContext).enumeratorNext, Price: 1},
Expand Down
121 changes: 110 additions & 11 deletions pkg/crypto/keys/publickey.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,29 @@ func NewPublicKeyFromASN1(data []byte) (*PublicKey, error) {
}

// decodeCompressedY performs decompression of Y coordinate for given X and Y's least significant bit.
func decodeCompressedY(x *big.Int, ylsb uint) (*big.Int, error) {
c := elliptic.P256()
cp := c.Params()
three := big.NewInt(3)
/* y**2 = x**3 + a*x + b % p */
xCubed := new(big.Int).Exp(x, three, cp.P)
threeX := new(big.Int).Mul(x, three)
threeX.Mod(threeX, cp.P)
ySquared := new(big.Int).Sub(xCubed, threeX)
// We use here a short-form Weierstrass curve (https://www.hyperelliptic.org/EFD/g1p/auto-shortw.html)
// y² = x³ + ax + b. Two types of elliptic curves are supported:
// 1. Secp256k1 (Koblitz curve): y² = x³ + b,
// 2. Secp256r1 (Random curve): y² = x³ - 3x + b.
// To decode compressed curve point we perform the following operation: y = sqrt(x³ + ax + b mod p)
// where `p` denotes the order of the underlying curve field
func decodeCompressedY(x *big.Int, ylsb uint, curve elliptic.Curve) (*big.Int, error) {
var a *big.Int
switch curve.Params().Name {
case Secp256k1Curve:
a = big.NewInt(0)
case elliptic.P256().Params().Name:
a = big.NewInt(3)
default:
// actually, we do support all curves from standard elliptic package as far as
// these curves have a=-3 which is hard-coded in the package
return nil, errors.New("Secp256k1 and Secp256r1 are only supported")
}
cp := curve.Params()
xCubed := new(big.Int).Exp(x, big.NewInt(3), cp.P)
aX := new(big.Int).Mul(x, a)
aX.Mod(aX, cp.P)
ySquared := new(big.Int).Sub(xCubed, aX)
ySquared.Add(ySquared, cp.B)
ySquared.Mod(ySquared, cp.P)
y := new(big.Int).ModSqrt(ySquared, cp.P)
Expand Down Expand Up @@ -196,7 +210,7 @@ func (p *PublicKey) DecodeBinary(r *io.BinReader) {
}
x = new(big.Int).SetBytes(xbytes)
ylsb := uint(prefix & 0x1)
y, err = decodeCompressedY(x, ylsb)
y, err = decodeCompressedY(x, ylsb, p256)
if err != nil {
r.Err = err
return
Expand Down Expand Up @@ -254,7 +268,6 @@ func (p *PublicKey) Address() string {
// Verify returns true if the signature is valid and corresponds
// to the hash and public key.
func (p *PublicKey) Verify(signature []byte, hash []byte) bool {

publicKey := &ecdsa.PublicKey{}
publicKey.Curve = elliptic.P256()
publicKey.X = p.X
Expand Down Expand Up @@ -306,3 +319,89 @@ func (p *PublicKey) UnmarshalJSON(data []byte) error {

return nil
}

// KeyRecover recovers public key from the given signature (r, s) on the given message hash using given elliptic curve.
// Algorithm source: SEC 1 Ver 2.0, section 4.1.6, pages 47-48 (https://www.secg.org/sec1-v2.pdf).
// Flag isEven denotes Y's least significant bit in decompression algorithm.
func KeyRecover(curve elliptic.Curve, r, s *big.Int, messageHash []byte, isEven bool) (PublicKey, error) {
var (
res PublicKey
err error
)
if r.Cmp(big.NewInt(1)) == -1 || s.Cmp(big.NewInt(1)) == -1 {
return res, errors.New("invalid signature")
}
params := curve.Params()
// calculate h = (Q + 1 + 2 * Sqrt(Q)) / N
// actually, we can skip this step for secp256k1 and secp256r1 as far as we know cofactor of these curves (h=1)
// (see section 2.4 of http://www.secg.org/sec2-v2.pdf)
num := new(big.Int).Add(new(big.Int).Add(params.P, big.NewInt(1)), new(big.Int).Mul(big.NewInt(2), new(big.Int).Sqrt(params.P)))
h := new(big.Int).Div(num, params.N)
for i := 0; i <= int(h.Int64()); i++ {
// step 1.1: x = (n * i) + r
Rx := new(big.Int).Mul(params.N, big.NewInt(int64(i)))
Rx.Add(Rx, r)
if Rx.Cmp(params.P) == 1 {
break
}

// steps 1.2 and 1.3: get point R (Ry)
var R *big.Int
if isEven {
R, err = decodeCompressedY(Rx, 0, curve)
} else {
R, err = decodeCompressedY(Rx, 1, curve)
}
if err != nil {
return res, err
}

// step 1.4: check n*R is point at infinity
// TODO: curve.ScalarMult suppose we're dealing with a=-3 in the elliptic curve equation.
// This line will give wrong results for secp256k1 unless we define our own implementation
// of ScalarMult for secp256k1 curve with a=0.
nRx, nR := curve.ScalarMult(Rx, R, params.N.Bytes())
if nRx.Sign() != 0 || nR.Sign() != 0 {
continue
}

// step 1.5: compute e
e := hashToInt(messageHash, curve)

// step 1.6: Q = r^-1 (sR-eG)
invr := new(big.Int).ModInverse(r, params.N)
// first term.
invrS := new(big.Int).Mul(invr, s)
invrS.Mod(invrS, params.N)
// TODO: implement ScalarMult for secp256k1
sRx, sR := curve.ScalarMult(Rx, R, invrS.Bytes())
// second term.
e.Neg(e)
e.Mod(e, params.N)
e.Mul(e, invr)
e.Mod(e, params.N)
// TODO: it won't be difficult to implement ScalarBaseMult having ScalarMult for secp256k1.
minuseGx, minuseGy := curve.ScalarBaseMult(e.Bytes())
// TODO: the same thing for secp256k1.
Qx, Qy := curve.Add(sRx, sR, minuseGx, minuseGy)
res.X = Qx
res.Y = Qy
}
return res, nil
}

// copied 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
}
33 changes: 33 additions & 0 deletions pkg/crypto/keys/publickey_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package keys

import (
"crypto/elliptic"
"encoding/hex"
"encoding/json"
"math/big"
"math/rand"
"sort"
"testing"

"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/internal/testserdes"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -179,3 +182,33 @@ func TestUnmarshallJSONBadFormat(t *testing.T) {
err := json.Unmarshal([]byte(str), actual)
require.Error(t, err)
}

func TestRecoverSecp256r1Static(t *testing.T) {
//these data were taken from the reference testcase
b := []byte{123, 245, 126, 56, 3, 123, 197, 199, 26, 31, 212, 186, 120, 195, 168, 153, 57, 108, 234, 49, 107, 203, 44, 207, 185, 212, 187, 129, 74, 43, 225, 69}
privateKey, err := NewPrivateKeyFromBytes(b)
require.NoError(t, err)
message := []byte{72, 101, 108, 108, 111, 87, 111, 114, 108, 100}
r := new(big.Int).SetBytes([]byte{1, 85, 226, 63, 133, 113, 217, 188, 249, 22, 213, 203, 225, 199, 32, 131, 118, 23, 28, 101, 139, 211, 13, 111, 242, 158, 193, 227, 196, 106, 3, 4})
s := new(big.Int).SetBytes([]byte{65, 174, 206, 164, 81, 34, 76, 104, 5, 49, 51, 20, 221, 183, 157, 199, 199, 47, 78, 137, 172, 99, 212, 110, 129, 72, 236, 59, 250, 81, 200, 13})
// just ensure it's a valid signature
require.True(t, privateKey.PublicKey().Verify(append(r.Bytes(), s.Bytes()...), message))
recoveredKey, err := KeyRecover(elliptic.P256(), r, s, message, false)
require.NoError(t, err)
require.True(t, privateKey.PublicKey().Equal(&recoveredKey))
}

func TestRecoverSecp256r1(t *testing.T) {
privateKey, err := NewPrivateKey()
require.NoError(t, err)
message := []byte{72, 101, 108, 108, 111, 87, 111, 114, 108, 100}
signature := privateKey.Sign(message)
r := new(big.Int).SetBytes(signature[0:32])
s := new(big.Int).SetBytes(signature[32:64])
require.True(t, privateKey.PublicKey().Verify(append(r.Bytes(), s.Bytes()...), hash.Sha256(message).BytesBE()))
// TODO: to test this properly, we should provide correct isEven flag. This flag denotes which one of
// the two recovered R points in decodeCompressedY method should be chosen.
recoveredKey, err := KeyRecover(elliptic.P256(), r, s, hash.Sha256(message).BytesBE(), false)
require.NoError(t, err)
require.True(t, privateKey.PublicKey().Equal(&recoveredKey))
}
37 changes: 37 additions & 0 deletions pkg/crypto/keys/secp256k1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package keys

import (
"crypto/elliptic"
"math/big"
"sync"
)

// Secp256k1Curve represents the name of Secp256k1 elliptic curve, which is set to be "Secp256k1".
const Secp256k1Curve string = "Secp256k1"

type secp256k1Curve struct {
*elliptic.CurveParams
}

var (
initonce sync.Once
secp256k1 secp256k1Curve
)

// parameters' values are taken from section 2.4.1 of http://www.secg.org/sec2-v2.pdf
func initSecp256k1() {
secp256k1.CurveParams = &elliptic.CurveParams{Name: Secp256k1Curve}
secp256k1.P, _ = new(big.Int).SetString("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", 16)
secp256k1.N, _ = new(big.Int).SetString("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16)
secp256k1.B, _ = new(big.Int).SetString("0000000000000000000000000000000000000000000000000000000000000007", 16)
secp256k1.Gx, _ = new(big.Int).SetString("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", 16)
secp256k1.Gy, _ = new(big.Int).SetString("483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", 16)
secp256k1.BitSize = 256
}

// Secp256k1 returns a Curve which implements secp256k1 elliptic curve.
// The CurveParams.Name of this Curve is "Secp256k1".
func Secp256k1() elliptic.Curve {
initonce.Do(initSecp256k1)
return secp256k1
}

0 comments on commit 53bb283

Please sign in to comment.