From a3e306ff78f73e74e41a6ba0141440c0194488ac Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Mon, 13 Jul 2020 12:59:41 +0300 Subject: [PATCH] core: implement Secp256k1 Verify and CheckMultisig interops Closes #918. --- pkg/compiler/syscall.go | 1 + pkg/core/interop/crypto/ecdsa.go | 29 ++++++++++- pkg/core/interop/runtime/witness.go | 4 +- pkg/core/interop_system.go | 3 +- pkg/core/interops.go | 2 + pkg/core/native/native_neo.go | 3 +- pkg/crypto/keys/publickey.go | 34 ++++++++----- pkg/crypto/keys/publickey_test.go | 3 +- pkg/crypto/keys/sign_verify_test.go | 75 +++++++++++++++++++++++------ pkg/interop/crypto/crypto.go | 6 +++ pkg/vm/vm.go | 19 ++++---- pkg/vm/vm_test.go | 7 +-- 12 files changed, 141 insertions(+), 45 deletions(-) diff --git a/pkg/compiler/syscall.go b/pkg/compiler/syscall.go index 90d9ef1cb..21f6bda83 100644 --- a/pkg/compiler/syscall.go +++ b/pkg/compiler/syscall.go @@ -7,6 +7,7 @@ var syscalls = map[string]map[string]string{ }, "crypto": { "ECDsaSecp256r1Verify": "Neo.Crypto.VerifyWithECDsaSecp256r1", + "ECDsaSecp256k1Verify": "Neo.Crypto.VerifyWithECDsaSecp256k1", }, "enumerator": { "Concat": "System.Enumerator.Concat", diff --git a/pkg/core/interop/crypto/ecdsa.go b/pkg/core/interop/crypto/ecdsa.go index c8598b2e1..67676292c 100644 --- a/pkg/core/interop/crypto/ecdsa.go +++ b/pkg/core/interop/crypto/ecdsa.go @@ -1,9 +1,11 @@ package crypto import ( + "crypto/elliptic" "errors" "fmt" + "github.com/btcsuite/btcd/btcec" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/crypto" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" @@ -17,11 +19,22 @@ const ECDSAVerifyPrice = 1000000 // ECDSASecp256r1Verify checks ECDSA signature using Secp256r1 elliptic curve. func ECDSASecp256r1Verify(ic *interop.Context, v *vm.VM) error { + return ecdsaVerify(ic, v, elliptic.P256()) +} + +// ECDSASecp256k1Verify checks ECDSA signature using Secp256k1 elliptic curve +func ECDSASecp256k1Verify(ic *interop.Context, v *vm.VM) error { + return ecdsaVerify(ic, v, btcec.S256()) +} + +// ecdsaVerify is internal representation of ECDSASecp256k1Verify and +// ECDSASecp256r1Verify. +func ecdsaVerify(ic *interop.Context, v *vm.VM, curve elliptic.Curve) error { msg := getMessage(ic, v.Estack().Pop().Item()) hashToCheck := hash.Sha256(msg).BytesBE() keyb := v.Estack().Pop().Bytes() signature := v.Estack().Pop().Bytes() - pkey, err := keys.NewPublicKeyFromBytes(keyb) + pkey, err := keys.NewPublicKeyFromBytes(keyb, curve) if err != nil { return err } @@ -33,6 +46,18 @@ func ECDSASecp256r1Verify(ic *interop.Context, v *vm.VM) error { // ECDSASecp256r1CheckMultisig checks multiple ECDSA signatures at once using // Secp256r1 elliptic curve. func ECDSASecp256r1CheckMultisig(ic *interop.Context, v *vm.VM) error { + return ecdsaCheckMultisig(ic, v, elliptic.P256()) +} + +// ECDSASecp256k1CheckMultisig checks multiple ECDSA signatures at once using +// Secp256k1 elliptic curve. +func ECDSASecp256k1CheckMultisig(ic *interop.Context, v *vm.VM) error { + return ecdsaCheckMultisig(ic, v, btcec.S256()) +} + +// ecdsaCheckMultisig is internal representation of ECDSASecp256r1CheckMultisig and +// ECDSASecp256k1CheckMultisig +func ecdsaCheckMultisig(ic *interop.Context, v *vm.VM, curve elliptic.Curve) error { msg := getMessage(ic, v.Estack().Pop().Item()) hashToCheck := hash.Sha256(msg).BytesBE() pkeys, err := v.Estack().PopSigElements() @@ -51,7 +76,7 @@ func ECDSASecp256r1CheckMultisig(ic *interop.Context, v *vm.VM) error { if len(pkeys) < len(sigs) { return errors.New("more signatures than there are keys") } - sigok := vm.CheckMultisigPar(v, hashToCheck, pkeys, sigs) + sigok := vm.CheckMultisigPar(v, curve, hashToCheck, pkeys, sigs) v.Estack().PushVal(sigok) return nil } diff --git a/pkg/core/interop/runtime/witness.go b/pkg/core/interop/runtime/witness.go index c152a8eff..b5783db17 100644 --- a/pkg/core/interop/runtime/witness.go +++ b/pkg/core/interop/runtime/witness.go @@ -1,6 +1,8 @@ package runtime import ( + "crypto/elliptic" + "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/transaction" @@ -90,7 +92,7 @@ func CheckWitness(ic *interop.Context, v *vm.VM) error { hash, err := util.Uint160DecodeBytesBE(hashOrKey) if err != nil { var key *keys.PublicKey - key, err = keys.NewPublicKeyFromBytes(hashOrKey) + key, err = keys.NewPublicKeyFromBytes(hashOrKey, elliptic.P256()) if err != nil { return errors.New("parameter given is neither a key nor a hash") } diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 0b2905aad..003c6acda 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -1,6 +1,7 @@ package core import ( + "crypto/elliptic" "errors" "fmt" "math" @@ -492,7 +493,7 @@ func contractIsStandard(ic *interop.Context, v *vm.VM) error { // contractCreateStandardAccount calculates contract scripthash for a given public key. func contractCreateStandardAccount(ic *interop.Context, v *vm.VM) error { h := v.Estack().Pop().Bytes() - p, err := keys.NewPublicKeyFromBytes(h) + p, err := keys.NewPublicKeyFromBytes(h, elliptic.P256()) if err != nil { return err } diff --git a/pkg/core/interops.go b/pkg/core/interops.go index 815d44a3a..e9a6da597 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -139,7 +139,9 @@ var systemInterops = []interop.Function{ var neoInterops = []interop.Function{ {Name: "Neo.Crypto.VerifyWithECDsaSecp256r1", Func: crypto.ECDSASecp256r1Verify, Price: crypto.ECDSAVerifyPrice}, + {Name: "Neo.Crypto.VerifyWithECDsaSecp256k1", Func: crypto.ECDSASecp256k1Verify, Price: crypto.ECDSAVerifyPrice}, {Name: "Neo.Crypto.CheckMultisigWithECDsaSecp256r1", Func: crypto.ECDSASecp256r1CheckMultisig, Price: 0}, + {Name: "Neo.Crypto.CheckMultisigWithECDsaSecp256k1", Func: crypto.ECDSASecp256k1CheckMultisig, Price: 0}, {Name: "Neo.Crypto.SHA256", Func: crypto.Sha256, Price: 1000000}, {Name: "Neo.Native.Deploy", Func: native.Deploy, Price: 0, AllowedTriggers: trigger.Application, RequiredFlags: smartcontract.AllowModifyStates}, diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 747025315..ca88bfe97 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -1,6 +1,7 @@ package native import ( + "crypto/elliptic" "math/big" "sort" "strings" @@ -361,7 +362,7 @@ func (n *NEO) GetRegisteredValidators(d dao.DAO) ([]state.Validator, error) { } arr := make([]state.Validator, len(kvs)) for i := range kvs { - arr[i].Key, err = keys.NewPublicKeyFromBytes([]byte(kvs[i].Key)) + arr[i].Key, err = keys.NewPublicKeyFromBytes([]byte(kvs[i].Key), elliptic.P256()) if err != nil { return nil, err } diff --git a/pkg/crypto/keys/publickey.go b/pkg/crypto/keys/publickey.go index 306a007ee..003a41d62 100644 --- a/pkg/crypto/keys/publickey.go +++ b/pkg/crypto/keys/publickey.go @@ -9,6 +9,7 @@ import ( "fmt" "math/big" + "github.com/btcsuite/btcd/btcec" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/io" @@ -100,12 +101,13 @@ func NewPublicKeyFromString(s string) (*PublicKey, error) { if err != nil { return nil, err } - return NewPublicKeyFromBytes(b) + return NewPublicKeyFromBytes(b, elliptic.P256()) } -// NewPublicKeyFromBytes returns public key created from b. -func NewPublicKeyFromBytes(b []byte) (*PublicKey, error) { +// NewPublicKeyFromBytes returns public key created from b using given EC. +func NewPublicKeyFromBytes(b []byte, curve elliptic.Curve) (*PublicKey, error) { pubKey := new(PublicKey) + pubKey.Curve = curve if err := pubKey.DecodeBytes(b); err != nil { return nil, err } @@ -175,15 +177,25 @@ func NewPublicKeyFromASN1(data []byte) (*PublicKey, error) { } // decodeCompressedY performs decompression of Y coordinate for given X and Y's least significant bit. +// 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) { - c := curve - 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) + var a *big.Int + switch curve.(type) { + case *btcec.KoblitzCurve: + a = big.NewInt(0) + default: + a = big.NewInt(3) + } + 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) diff --git a/pkg/crypto/keys/publickey_test.go b/pkg/crypto/keys/publickey_test.go index 445d98eb5..c4b03c5d1 100644 --- a/pkg/crypto/keys/publickey_test.go +++ b/pkg/crypto/keys/publickey_test.go @@ -1,6 +1,7 @@ package keys import ( + "crypto/elliptic" "encoding/hex" "encoding/json" "math/rand" @@ -59,7 +60,7 @@ func TestNewPublicKeyFromBytes(t *testing.T) { require.NoError(t, err) b := priv.PublicKey().Bytes() - pub, err := NewPublicKeyFromBytes(b) + pub, err := NewPublicKeyFromBytes(b, elliptic.P256()) require.NoError(t, err) require.Equal(t, priv.PublicKey(), pub) } diff --git a/pkg/crypto/keys/sign_verify_test.go b/pkg/crypto/keys/sign_verify_test.go index 5241a2cc6..b78df4fac 100644 --- a/pkg/crypto/keys/sign_verify_test.go +++ b/pkg/crypto/keys/sign_verify_test.go @@ -1,38 +1,81 @@ package keys import ( + "crypto/ecdsa" "testing" + "github.com/btcsuite/btcd/btcec" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPubKeyVerify(t *testing.T) { var data = []byte("sample") hashedData := hash.Sha256(data) - privKey, err := NewPrivateKey() - assert.Nil(t, err) - signedData := privKey.Sign(data) - pubKey := privKey.PublicKey() - result := pubKey.Verify(signedData, hashedData.BytesBE()) - expected := true - assert.Equal(t, expected, result) + t.Run("Secp256r1", func(t *testing.T) { + privKey, err := NewPrivateKey() + assert.Nil(t, err) + signedData := privKey.Sign(data) + pubKey := privKey.PublicKey() + result := pubKey.Verify(signedData, hashedData.BytesBE()) + expected := true + assert.Equal(t, expected, result) - pubKey = &PublicKey{} - assert.False(t, pubKey.Verify(signedData, hashedData.BytesBE())) + pubKey = &PublicKey{} + assert.False(t, pubKey.Verify(signedData, hashedData.BytesBE())) + }) + + t.Run("Secp256k1", func(t *testing.T) { + privateKey, err := btcec.NewPrivateKey(btcec.S256()) + assert.Nil(t, err) + signature, err := privateKey.Sign(hashedData.BytesBE()) + require.NoError(t, err) + signedData := append(signature.R.Bytes(), signature.S.Bytes()...) + pubKey := PublicKey(ecdsa.PublicKey{ + Curve: btcec.S256(), + X: privateKey.X, + Y: privateKey.Y, + }) + require.True(t, pubKey.Verify(signedData, hashedData.BytesBE())) + + pubKey = PublicKey{} + assert.False(t, pubKey.Verify(signedData, hashedData.BytesBE())) + }) } func TestWrongPubKey(t *testing.T) { - privKey, _ := NewPrivateKey() sample := []byte("sample") hashedData := hash.Sha256(sample) - signedData := privKey.Sign(sample) - secondPrivKey, _ := NewPrivateKey() - wrongPubKey := secondPrivKey.PublicKey() + t.Run("Secp256r1", func(t *testing.T) { + privKey, _ := NewPrivateKey() + signedData := privKey.Sign(sample) - actual := wrongPubKey.Verify(signedData, hashedData.BytesBE()) - expcted := false - assert.Equal(t, expcted, actual) + secondPrivKey, _ := NewPrivateKey() + wrongPubKey := secondPrivKey.PublicKey() + + actual := wrongPubKey.Verify(signedData, hashedData.BytesBE()) + expcted := false + assert.Equal(t, expcted, actual) + }) + + t.Run("Secp256k1", func(t *testing.T) { + privateKey, err := btcec.NewPrivateKey(btcec.S256()) + assert.Nil(t, err) + signature, err := privateKey.Sign(hashedData.BytesBE()) + assert.Nil(t, err) + signedData := append(signature.R.Bytes(), signature.S.Bytes()...) + + secondPrivKey, err := btcec.NewPrivateKey(btcec.S256()) + assert.Nil(t, err) + wrongPubKey := PublicKey(ecdsa.PublicKey{ + Curve: btcec.S256(), + X: secondPrivKey.X, + Y: secondPrivKey.Y, + }) + + assert.False(t, wrongPubKey.Verify(signedData, hashedData.BytesBE())) + }) } diff --git a/pkg/interop/crypto/crypto.go b/pkg/interop/crypto/crypto.go index 0051636ba..91f932ce7 100644 --- a/pkg/interop/crypto/crypto.go +++ b/pkg/interop/crypto/crypto.go @@ -13,3 +13,9 @@ func SHA256(b []byte) []byte { func ECDsaSecp256r1Verify(msg []byte, pub []byte, sig []byte) bool { return false } + +// ECDsaSecp256k1Verify checks that sig is correct msg's signature for a given pub +// (serialized public key). It uses `Neo.Crypto.VerifyWithECDsaSecp256k1` syscall. +func ECDsaSecp256k1Verify(msg []byte, pub []byte, sig []byte) bool { + return false +} diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 630bbfc19..7ce280883 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -1,6 +1,7 @@ package vm import ( + "crypto/elliptic" "encoding/binary" "encoding/json" "fmt" @@ -1457,9 +1458,9 @@ func (v *VM) calcJumpOffset(ctx *Context, parameter []byte) (int, int, error) { } // CheckMultisigPar checks if sigs contains sufficient valid signatures. -func CheckMultisigPar(v *VM, h []byte, pkeys [][]byte, sigs [][]byte) bool { +func CheckMultisigPar(v *VM, curve elliptic.Curve, h []byte, pkeys [][]byte, sigs [][]byte) bool { if len(sigs) == 1 { - return checkMultisig1(v, h, pkeys, sigs[0]) + return checkMultisig1(v, curve, h, pkeys, sigs[0]) } k1, k2 := 0, len(pkeys)-1 @@ -1496,8 +1497,8 @@ func CheckMultisigPar(v *VM, h []byte, pkeys [][]byte, sigs [][]byte) bool { go worker(tasks, results) } - tasks <- task{pub: v.bytesToPublicKey(pkeys[k1]), signum: s1} - tasks <- task{pub: v.bytesToPublicKey(pkeys[k2]), signum: s2} + tasks <- task{pub: v.bytesToPublicKey(pkeys[k1], curve), signum: s1} + tasks <- task{pub: v.bytesToPublicKey(pkeys[k2], curve), signum: s2} sigok := true taskCount := 2 @@ -1541,7 +1542,7 @@ loop: nextKey = k2 } taskCount++ - tasks <- task{pub: v.bytesToPublicKey(pkeys[nextKey]), signum: nextSig} + tasks <- task{pub: v.bytesToPublicKey(pkeys[nextKey], curve), signum: nextSig} } close(tasks) @@ -1549,9 +1550,9 @@ loop: return sigok } -func checkMultisig1(v *VM, h []byte, pkeys [][]byte, sig []byte) bool { +func checkMultisig1(v *VM, curve elliptic.Curve, h []byte, pkeys [][]byte, sig []byte) bool { for i := range pkeys { - pkey := v.bytesToPublicKey(pkeys[i]) + pkey := v.bytesToPublicKey(pkeys[i], curve) if pkey.Verify(sig, h) { return true } @@ -1606,14 +1607,14 @@ func (v *VM) checkInvocationStackSize() { // bytesToPublicKey is a helper deserializing keys using cache and panicing on // error. -func (v *VM) bytesToPublicKey(b []byte) *keys.PublicKey { +func (v *VM) bytesToPublicKey(b []byte, curve elliptic.Curve) *keys.PublicKey { var pkey *keys.PublicKey s := string(b) if v.keys[s] != nil { pkey = v.keys[s] } else { var err error - pkey, err = keys.NewPublicKeyFromBytes(b) + pkey, err = keys.NewPublicKeyFromBytes(b, curve) if err != nil { panic(err.Error()) } diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index e076a0f71..fe324aa7c 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -2,6 +2,7 @@ package vm import ( "bytes" + "crypto/elliptic" "encoding/binary" "encoding/hex" "fmt" @@ -116,9 +117,9 @@ func TestBytesToPublicKey(t *testing.T) { assert.Equal(t, 0, len(cache)) keyHex := "03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c" keyBytes, _ := hex.DecodeString(keyHex) - key := v.bytesToPublicKey(keyBytes) + key := v.bytesToPublicKey(keyBytes, elliptic.P256()) assert.NotNil(t, key) - key2 := v.bytesToPublicKey(keyBytes) + key2 := v.bytesToPublicKey(keyBytes, elliptic.P256()) assert.Equal(t, key, key2) cache = v.GetPublicKeys() @@ -126,7 +127,7 @@ func TestBytesToPublicKey(t *testing.T) { assert.NotNil(t, cache[string(keyBytes)]) keyBytes[0] = 0xff - require.Panics(t, func() { v.bytesToPublicKey(keyBytes) }) + require.Panics(t, func() { v.bytesToPublicKey(keyBytes, elliptic.P256()) }) } func TestPushBytes1to75(t *testing.T) {